fix(UI, PdfGroups) - Schema updated, ui changed, backend Route changed
This commit is contained in:
@@ -127,21 +127,18 @@ router.post(
|
|||||||
|
|
||||||
const groupTitle = "Insurance Claim";
|
const groupTitle = "Insurance Claim";
|
||||||
const groupTitleKey = "INSURANCE_CLAIM";
|
const groupTitleKey = "INSURANCE_CLAIM";
|
||||||
const groupCategory = "CLAIM";
|
|
||||||
|
|
||||||
// ✅ Find or create PDF group for this claim
|
// ✅ Find or create PDF group for this claim
|
||||||
let group = await storage.findPdfGroupByPatientTitleKeyAndCategory(
|
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||||
parsedPatientId,
|
parsedPatientId,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!group) {
|
if (!group) {
|
||||||
group = await storage.createPdfGroup(
|
group = await storage.createPdfGroup(
|
||||||
parsedPatientId,
|
parsedPatientId,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from "express";
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { storage } from "../storage";
|
import { storage } from "../storage";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
|
import { PdfFile } from "../../../../packages/db/types/pdf-types";
|
||||||
|
|
||||||
const upload = multer({ storage: multer.memoryStorage() });
|
const upload = multer({ storage: multer.memoryStorage() });
|
||||||
|
|
||||||
@@ -12,18 +13,17 @@ router.post(
|
|||||||
"/pdf-groups",
|
"/pdf-groups",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
const { patientId, groupTitle, groupTitleKey, groupCategory } = req.body;
|
const { patientId, groupTitle, groupTitleKey } = req.body;
|
||||||
if (!patientId || !groupTitle || groupTitleKey || !groupCategory) {
|
if (!patientId || !groupTitle || groupTitleKey) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.json({ error: "Missing title, titleKey, category, or patientId" });
|
.json({ error: "Missing title, titleKey, or patientId" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const group = await storage.createPdfGroup(
|
const group = await storage.createPdfGroup(
|
||||||
parseInt(patientId),
|
parseInt(patientId),
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json(group);
|
res.json(group);
|
||||||
@@ -90,11 +90,10 @@ router.put(
|
|||||||
return res.status(400).json({ error: "Missing ID" });
|
return res.status(400).json({ error: "Missing ID" });
|
||||||
}
|
}
|
||||||
const id = parseInt(idParam);
|
const id = parseInt(idParam);
|
||||||
const { title, category, titleKey } = req.body;
|
const { title, titleKey } = req.body;
|
||||||
|
|
||||||
const updates: any = {};
|
const updates: any = {};
|
||||||
updates.title = title;
|
updates.title = title;
|
||||||
updates.category = category;
|
|
||||||
updates.titleKey = titleKey;
|
updates.titleKey = titleKey;
|
||||||
|
|
||||||
const updated = await storage.updatePdfGroup(id, updates);
|
const updated = await storage.updatePdfGroup(id, updates);
|
||||||
@@ -168,6 +167,69 @@ router.get(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /pdf-files/group/:groupId
|
||||||
|
* Query params:
|
||||||
|
* - limit (optional, defaults to 5): number of items per page (max 1000)
|
||||||
|
* - offset (optional, defaults to 0): offset for pagination
|
||||||
|
*
|
||||||
|
* Response: { total: number, data: PdfFile[] }
|
||||||
|
*/
|
||||||
|
router.get(
|
||||||
|
"/recent-pdf-files/group/:groupId",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const rawGroupId = req.params.groupId;
|
||||||
|
if (!rawGroupId) {
|
||||||
|
return res.status(400).json({ error: "Missing groupId param" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = Number(rawGroupId);
|
||||||
|
if (Number.isNaN(groupId) || groupId <= 0) {
|
||||||
|
return res.status(400).json({ error: "Invalid groupId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse & sanitize query params
|
||||||
|
const limitQuery = req.query.limit;
|
||||||
|
const offsetQuery = req.query.offset;
|
||||||
|
|
||||||
|
const limit =
|
||||||
|
limitQuery !== undefined
|
||||||
|
? Math.min(Math.max(Number(limitQuery), 1), 1000) // 1..1000
|
||||||
|
: undefined; // if undefined -> treat as "no pagination" (return all)
|
||||||
|
const offset =
|
||||||
|
offsetQuery !== undefined ? Math.max(Number(offsetQuery), 0) : 0;
|
||||||
|
|
||||||
|
// Decide whether client asked for paginated response
|
||||||
|
const wantsPagination = typeof limit === "number";
|
||||||
|
|
||||||
|
if (wantsPagination) {
|
||||||
|
// storage.getPdfFilesByGroupId with pagination should return { total, data }
|
||||||
|
const result = await storage.getPdfFilesByGroupId(groupId, {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
withGroup: false, // do not include group relation in listing
|
||||||
|
});
|
||||||
|
|
||||||
|
// result should be { total, data }, but handle unexpected shapes defensively
|
||||||
|
if (Array.isArray(result)) {
|
||||||
|
// fallback: storage returned full array; compute total
|
||||||
|
return res.json({ total: result.length, data: result });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
|
} else {
|
||||||
|
// no limit requested -> return all files for the group
|
||||||
|
const all = (await storage.getPdfFilesByGroupId(groupId)) as PdfFile[];
|
||||||
|
return res.json({ total: all.length, data: all });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("GET /pdf-files/group/:groupId error:", err);
|
||||||
|
return res.status(500).json({ error: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/pdf-files/:id",
|
"/pdf-files/:id",
|
||||||
async (req: Request, res: Response): Promise<any> => {
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
|||||||
@@ -60,14 +60,12 @@ router.post(
|
|||||||
if (result.pdf_path && result.pdf_path.endsWith(".pdf")) {
|
if (result.pdf_path && result.pdf_path.endsWith(".pdf")) {
|
||||||
const pdfBuffer = await fs.readFile(result.pdf_path);
|
const pdfBuffer = await fs.readFile(result.pdf_path);
|
||||||
|
|
||||||
const groupTitle = "Insurance Status PDFs";
|
const groupTitle = "Eligibility Status PDFs";
|
||||||
const groupTitleKey = "INSURANCE_STATUS_PDFs";
|
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||||
const groupCategory = "ELIGIBILITY_STATUS";
|
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKeyAndCategory(
|
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||||
patient.id,
|
patient.id,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 2b: Create group if it doesn’t exist
|
// Step 2b: Create group if it doesn’t exist
|
||||||
@@ -75,8 +73,7 @@ router.post(
|
|||||||
group = await storage.createPdfGroup(
|
group = await storage.createPdfGroup(
|
||||||
patient.id,
|
patient.id,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +217,11 @@ router.post(
|
|||||||
|
|
||||||
if (pdfBuffer && generatedPdfPath) {
|
if (pdfBuffer && generatedPdfPath) {
|
||||||
const groupTitle = "Insurance Status PDFs";
|
const groupTitle = "Insurance Status PDFs";
|
||||||
const groupTitleKey = "INSURANCE_STATUS_PDFs";
|
const groupTitleKey = "CLAIM_STATUS";
|
||||||
const groupCategory = "CLAIM_STATUS";
|
|
||||||
|
|
||||||
let group = await storage.findPdfGroupByPatientTitleKeyAndCategory(
|
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||||
patient.id,
|
patient.id,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create group if missing
|
// Create group if missing
|
||||||
@@ -234,8 +229,7 @@ router.post(
|
|||||||
group = await storage.createPdfGroup(
|
group = await storage.createPdfGroup(
|
||||||
patient.id,
|
patient.id,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
groupTitleKey,
|
groupTitleKey
|
||||||
groupCategory
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
import { PdfCategory, PdfTitle } from "@repo/db/generated/prisma";
|
import { PdfTitleKey } from "@repo/db/generated/prisma";
|
||||||
import {
|
import {
|
||||||
Appointment,
|
Appointment,
|
||||||
Claim,
|
Claim,
|
||||||
@@ -150,7 +150,10 @@ export interface IStorage {
|
|||||||
pdfData: Buffer
|
pdfData: Buffer
|
||||||
): Promise<PdfFile>;
|
): Promise<PdfFile>;
|
||||||
getPdfFileById(id: number): Promise<PdfFile | undefined>;
|
getPdfFileById(id: number): Promise<PdfFile | undefined>;
|
||||||
getPdfFilesByGroupId(groupId: number): Promise<PdfFile[]>;
|
getPdfFilesByGroupId(
|
||||||
|
groupId: number,
|
||||||
|
opts?: { limit?: number; offset?: number; withGroup?: boolean }
|
||||||
|
): Promise<PdfFile[] | { total: number; data: PdfFile[] }>;
|
||||||
getRecentPdfFiles(limit: number, offset: number): Promise<PdfFile[]>;
|
getRecentPdfFiles(limit: number, offset: number): Promise<PdfFile[]>;
|
||||||
deletePdfFile(id: number): Promise<boolean>;
|
deletePdfFile(id: number): Promise<boolean>;
|
||||||
updatePdfFile(
|
updatePdfFile(
|
||||||
@@ -162,20 +165,18 @@ export interface IStorage {
|
|||||||
createPdfGroup(
|
createPdfGroup(
|
||||||
patientId: number,
|
patientId: number,
|
||||||
title: string,
|
title: string,
|
||||||
titleKey: PdfTitle,
|
titleKey: PdfTitleKey
|
||||||
category: PdfCategory
|
|
||||||
): Promise<PdfGroup>;
|
): Promise<PdfGroup>;
|
||||||
findPdfGroupByPatientTitleKeyAndCategory(
|
findPdfGroupByPatientTitleKey(
|
||||||
patientId: number,
|
patientId: number,
|
||||||
titleKey: PdfTitle,
|
titleKey: PdfTitleKey
|
||||||
category: PdfCategory
|
|
||||||
): Promise<PdfGroup | undefined>;
|
): Promise<PdfGroup | undefined>;
|
||||||
getAllPdfGroups(): Promise<PdfGroup[]>;
|
getAllPdfGroups(): Promise<PdfGroup[]>;
|
||||||
getPdfGroupById(id: number): Promise<PdfGroup | undefined>;
|
getPdfGroupById(id: number): Promise<PdfGroup | undefined>;
|
||||||
getPdfGroupsByPatientId(patientId: number): Promise<PdfGroup[]>;
|
getPdfGroupsByPatientId(patientId: number): Promise<PdfGroup[]>;
|
||||||
updatePdfGroup(
|
updatePdfGroup(
|
||||||
id: number,
|
id: number,
|
||||||
updates: Partial<Pick<PdfGroup, "title" | "category">>
|
updates: Partial<Pick<PdfGroup, "title">>
|
||||||
): Promise<PdfGroup | undefined>;
|
): Promise<PdfGroup | undefined>;
|
||||||
deletePdfGroup(id: number): Promise<boolean>;
|
deletePdfGroup(id: number): Promise<boolean>;
|
||||||
|
|
||||||
@@ -699,11 +700,70 @@ export const storage: IStorage = {
|
|||||||
return (await db.pdfFile.findUnique({ where: { id } })) ?? undefined;
|
return (await db.pdfFile.findUnique({ where: { id } })) ?? undefined;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPdfFilesByGroupId(groupId) {
|
/**
|
||||||
return db.pdfFile.findMany({
|
* getPdfFilesByGroupId: supports
|
||||||
where: { groupId },
|
* - getPdfFilesByGroupId(groupId) => Promise<PdfFile[]>
|
||||||
orderBy: { uploadedAt: "desc" },
|
* - getPdfFilesByGroupId(groupId, { limit, offset }) => Promise<{ total, data }>
|
||||||
});
|
* - getPdfFilesByGroupId(groupId, { limit, offset, withGroup: true }) => Promise<{ total, data: PdfFileWithGroup[] }>
|
||||||
|
*/
|
||||||
|
async getPdfFilesByGroupId(groupId, opts) {
|
||||||
|
// if pagination is requested (limit provided) return total + page
|
||||||
|
const wantsPagination =
|
||||||
|
!!opts &&
|
||||||
|
(typeof opts.limit === "number" || typeof opts.offset === "number");
|
||||||
|
|
||||||
|
if (wantsPagination) {
|
||||||
|
const limit = Math.min(Number(opts?.limit ?? 5), 1000);
|
||||||
|
const offset = Number(opts?.offset ?? 0);
|
||||||
|
|
||||||
|
if (opts?.withGroup) {
|
||||||
|
// return total + data with group included
|
||||||
|
const [total, data] = await Promise.all([
|
||||||
|
db.pdfFile.count({ where: { groupId } }),
|
||||||
|
db.pdfFile.findMany({
|
||||||
|
where: { groupId },
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
include: { group: true }, // only include
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { total, data };
|
||||||
|
} else {
|
||||||
|
// return total + data with limited fields via select
|
||||||
|
const [total, data] = await Promise.all([
|
||||||
|
db.pdfFile.count({ where: { groupId } }),
|
||||||
|
db.pdfFile.findMany({
|
||||||
|
where: { groupId },
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
take: limit,
|
||||||
|
skip: offset,
|
||||||
|
select: { id: true, filename: true, uploadedAt: true }, // only select
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Note: selected shape won't have all PdfFile fields; cast if needed
|
||||||
|
return { total, data: data as unknown as PdfFile[] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-paginated: return all files (keep descending order)
|
||||||
|
if (opts?.withGroup) {
|
||||||
|
const all = await db.pdfFile.findMany({
|
||||||
|
where: { groupId },
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
include: { group: true },
|
||||||
|
});
|
||||||
|
return all as PdfFile[];
|
||||||
|
} else {
|
||||||
|
const all = await db.pdfFile.findMany({
|
||||||
|
where: { groupId },
|
||||||
|
orderBy: { uploadedAt: "desc" },
|
||||||
|
// no select or include -> returns full PdfFile
|
||||||
|
});
|
||||||
|
return all as PdfFile[];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getRecentPdfFiles(limit: number, offset: number): Promise<PdfFile[]> {
|
async getRecentPdfFiles(limit: number, offset: number): Promise<PdfFile[]> {
|
||||||
@@ -739,28 +799,22 @@ export const storage: IStorage = {
|
|||||||
// PdfGroup CRUD
|
// PdfGroup CRUD
|
||||||
// ----------------------
|
// ----------------------
|
||||||
|
|
||||||
async createPdfGroup(patientId, title, titleKey, category) {
|
async createPdfGroup(patientId, title, titleKey) {
|
||||||
return db.pdfGroup.create({
|
return db.pdfGroup.create({
|
||||||
data: {
|
data: {
|
||||||
patientId,
|
patientId,
|
||||||
title,
|
title,
|
||||||
titleKey,
|
titleKey,
|
||||||
category,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
async findPdfGroupByPatientTitleKeyAndCategory(
|
async findPdfGroupByPatientTitleKey(patientId, titleKey) {
|
||||||
patientId,
|
|
||||||
titleKey,
|
|
||||||
category
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
(await db.pdfGroup.findFirst({
|
(await db.pdfGroup.findFirst({
|
||||||
where: {
|
where: {
|
||||||
patientId,
|
patientId,
|
||||||
titleKey,
|
titleKey,
|
||||||
category,
|
|
||||||
},
|
},
|
||||||
})) ?? undefined
|
})) ?? undefined
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -16,23 +16,34 @@ import { PatientTable } from "@/components/patients/patient-table";
|
|||||||
import { Patient, PdfFile } from "@repo/db/types";
|
import { Patient, PdfFile } from "@repo/db/types";
|
||||||
|
|
||||||
export default function DocumentsPage() {
|
export default function DocumentsPage() {
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
||||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
|
const [expandedGroupId, setExpandedGroupId] = useState<number | null>(null);
|
||||||
const [selectedPdfId, setSelectedPdfId] = useState<number | null>(null);
|
|
||||||
|
// pagination state for the expanded group
|
||||||
|
const [limit, setLimit] = useState<number>(5);
|
||||||
|
const [offset, setOffset] = useState<number>(0);
|
||||||
|
const [totalForExpandedGroup, setTotalForExpandedGroup] = useState<
|
||||||
|
number | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
// PDF view state - viewing / downloading
|
||||||
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
|
||||||
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
|
||||||
const [currentPdf, setCurrentPdf] = useState<PdfFile | null>(null);
|
const [currentPdf, setCurrentPdf] = useState<PdfFile | null>(null);
|
||||||
|
|
||||||
const toggleMobileMenu = () => setIsMobileMenuOpen((prev) => !prev);
|
// Delete dialog
|
||||||
|
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
||||||
|
|
||||||
|
// reset UI when patient changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedGroupId(null);
|
setExpandedGroupId(null);
|
||||||
|
setLimit(5);
|
||||||
|
setOffset(0);
|
||||||
|
setTotalForExpandedGroup(null);
|
||||||
setFileBlobUrl(null);
|
setFileBlobUrl(null);
|
||||||
setSelectedPdfId(null);
|
|
||||||
}, [selectedPatient]);
|
}, [selectedPatient]);
|
||||||
|
|
||||||
const { data: groups = [] } = useQuery({
|
// FETCH GROUPS for patient (includes `category` on each group)
|
||||||
|
const { data: groups = [], isLoading: isLoadingGroups } = useQuery({
|
||||||
queryKey: ["groups", selectedPatient?.id],
|
queryKey: ["groups", selectedPatient?.id],
|
||||||
enabled: !!selectedPatient,
|
enabled: !!selectedPatient,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -44,18 +55,59 @@ export default function DocumentsPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: groupPdfs = [] } = useQuery({
|
// Group groups by titleKey (use titleKey only for grouping/ordering; don't display it)
|
||||||
queryKey: ["groupPdfs", selectedGroupId],
|
const groupsByTitleKey = useMemo(() => {
|
||||||
enabled: !!selectedGroupId,
|
const map = new Map<string, any[]>();
|
||||||
|
for (const g of groups as any[]) {
|
||||||
|
const key = String(g.titleKey ?? "OTHER");
|
||||||
|
if (!map.has(key)) map.set(key, []);
|
||||||
|
map.get(key)!.push(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide on a stable order for titleKey buckets: prefer enum order then any extras
|
||||||
|
const preferredOrder = [
|
||||||
|
"INSURANCE_CLAIM",
|
||||||
|
"ELIGIBILITY_STATUS",
|
||||||
|
"CLAIM_STATUS",
|
||||||
|
"OTHER",
|
||||||
|
];
|
||||||
|
const orderedKeys: string[] = [];
|
||||||
|
|
||||||
|
for (const k of preferredOrder) {
|
||||||
|
if (map.has(k)) orderedKeys.push(k);
|
||||||
|
}
|
||||||
|
// append any keys that weren't in preferredOrder
|
||||||
|
for (const k of map.keys()) {
|
||||||
|
if (!orderedKeys.includes(k)) orderedKeys.push(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { map, orderedKeys };
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
// FETCH PDFs for selected group with pagination (limit & offset)
|
||||||
|
const {
|
||||||
|
data: groupPdfsResponse,
|
||||||
|
refetch: refetchGroupPdfs,
|
||||||
|
isFetching: isFetchingPdfs,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["groupPdfs", expandedGroupId, limit, offset],
|
||||||
|
enabled: !!expandedGroupId,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
// API should accept ?limit & ?offset and also return total count
|
||||||
const res = await apiRequest(
|
const res = await apiRequest(
|
||||||
"GET",
|
"GET",
|
||||||
`/api/documents/pdf-files/group/${selectedGroupId}`
|
`/api/documents/recent-pdf-files/group/${expandedGroupId}?limit=${limit}&offset=${offset}`
|
||||||
);
|
);
|
||||||
return res.json();
|
// expected shape: { data: PdfFile[], total: number }
|
||||||
|
const json = await res.json();
|
||||||
|
setTotalForExpandedGroup(json.total ?? null);
|
||||||
|
return json.data ?? [];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupPdfs: PdfFile[] = groupPdfsResponse ?? [];
|
||||||
|
|
||||||
|
// DELETE mutation
|
||||||
const deletePdfMutation = useMutation({
|
const deletePdfMutation = useMutation({
|
||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number) => {
|
||||||
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
||||||
@@ -63,9 +115,9 @@ export default function DocumentsPage() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsDeletePdfOpen(false);
|
setIsDeletePdfOpen(false);
|
||||||
setCurrentPdf(null);
|
setCurrentPdf(null);
|
||||||
if (selectedGroupId != null) {
|
if (expandedGroupId != null) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["groupPdfs", selectedGroupId],
|
queryKey: ["groupPdfs", expandedGroupId],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
toast({ title: "Success", description: "PDF deleted successfully!" });
|
toast({ title: "Success", description: "PDF deleted successfully!" });
|
||||||
@@ -97,7 +149,6 @@ export default function DocumentsPage() {
|
|||||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
setFileBlobUrl(url);
|
setFileBlobUrl(url);
|
||||||
setSelectedPdfId(pdfId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadPdf = async (pdfId: number, filename: string) => {
|
const handleDownloadPdf = async (pdfId: number, filename: string) => {
|
||||||
@@ -114,6 +165,25 @@ export default function DocumentsPage() {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Expand / collapse a group — when expanding reset pagination
|
||||||
|
const toggleExpandGroup = (groupId: number) => {
|
||||||
|
if (expandedGroupId === groupId) {
|
||||||
|
setExpandedGroupId(null);
|
||||||
|
setOffset(0);
|
||||||
|
setLimit(5);
|
||||||
|
setTotalForExpandedGroup(null);
|
||||||
|
} else {
|
||||||
|
setExpandedGroupId(groupId);
|
||||||
|
setOffset(0);
|
||||||
|
setLimit(5);
|
||||||
|
setTotalForExpandedGroup(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoadMore = () => {
|
||||||
|
setOffset((prev) => prev + limit);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="container mx-auto space-y-6">
|
<div className="container mx-auto space-y-6">
|
||||||
@@ -130,85 +200,148 @@ export default function DocumentsPage() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
Document Groups for {selectedPatient.firstName}{" "}
|
Document groups of Patient: {selectedPatient.firstName}{" "}
|
||||||
{selectedPatient.lastName}
|
{selectedPatient.lastName}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Select a group to view PDFs</CardDescription>
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
|
||||||
{groups.length === 0 ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
No groups found for this patient.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{groups.map((group: any) => (
|
|
||||||
<Button
|
|
||||||
key={group.id}
|
|
||||||
variant={
|
|
||||||
group.id === selectedGroupId ? "default" : "outline"
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedGroupId((prevId) =>
|
|
||||||
prevId === group.id ? null : group.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FolderOpen className="w-4 h-4 mr-2" />
|
|
||||||
Group - {group.title}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedGroupId && (
|
<CardContent className="space-y-4">
|
||||||
<Card>
|
{isLoadingGroups ? (
|
||||||
<CardHeader>
|
<div>Loading groups…</div>
|
||||||
<CardTitle>PDFs in Group</CardTitle>
|
) : (groups as any[]).length === 0 ? (
|
||||||
</CardHeader>
|
<div className="text-sm text-muted-foreground">
|
||||||
<CardContent className="space-y-2">
|
No groups found.
|
||||||
{groupPdfs.length === 0 ? (
|
</div>
|
||||||
<p className="text-muted-foreground">
|
|
||||||
No PDFs found in this group.
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
groupPdfs.map((pdf: any) => (
|
<div className="flex flex-col gap-3">
|
||||||
<div
|
{groupsByTitleKey.orderedKeys.map((key, idx) => {
|
||||||
key={pdf.id}
|
const bucket = groupsByTitleKey.map.get(key) ?? [];
|
||||||
className="flex justify-between items-center border p-2 rounded"
|
return (
|
||||||
>
|
<div key={key}>
|
||||||
<span className="text-sm">{pdf.filename}</span>
|
{/* subtle divider between buckets (no enum text shown) */}
|
||||||
<div className="flex gap-2">
|
{idx !== 0 && (
|
||||||
<Button
|
<hr className="my-3 border-t border-gray-200" />
|
||||||
variant="ghost"
|
)}
|
||||||
size="sm"
|
|
||||||
onClick={() => handleViewPdf(pdf.id)}
|
<div className="flex flex-col gap-2">
|
||||||
>
|
{bucket.map((group: any) => (
|
||||||
<Eye className="w-4 h-4 text-gray-600" />
|
<div key={group.id} className="border rounded p-3">
|
||||||
</Button>
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<div className="flex items-center gap-3">
|
||||||
variant="ghost"
|
<FolderOpen className="w-5 h-5" />
|
||||||
size="sm"
|
<div>
|
||||||
onClick={() => handleDownloadPdf(pdf.id, pdf.filename)}
|
{/* Only show the group's title string */}
|
||||||
>
|
<div className="font-semibold">
|
||||||
<Download className="w-4 h-4 text-blue-600" />
|
{group.title}
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
</div>
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
<div className="flex items-center gap-2">
|
||||||
setCurrentPdf(pdf);
|
<Button
|
||||||
setIsDeletePdfOpen(true);
|
size="sm"
|
||||||
}}
|
variant={
|
||||||
>
|
expandedGroupId === group.id
|
||||||
<Trash className="w-4 h-4 text-red-600" />
|
? "default"
|
||||||
</Button>
|
: "outline"
|
||||||
</div>
|
}
|
||||||
</div>
|
onClick={() => toggleExpandGroup(group.id)}
|
||||||
))
|
>
|
||||||
|
{expandedGroupId === group.id
|
||||||
|
? "Collapse"
|
||||||
|
: "Open"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* expanded content: show paginated PDFs for this group */}
|
||||||
|
{expandedGroupId === group.id && (
|
||||||
|
<div className="mt-3 space-y-3">
|
||||||
|
{isFetchingPdfs ? (
|
||||||
|
<div>Loading PDFs…</div>
|
||||||
|
) : groupPdfs.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
No PDFs in this group.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{groupPdfs.map((pdf) => (
|
||||||
|
<div
|
||||||
|
key={pdf.id}
|
||||||
|
className="flex justify-between items-center border rounded p-2"
|
||||||
|
>
|
||||||
|
<div className="text-sm">
|
||||||
|
{pdf.filename}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleViewPdf(Number(pdf.id))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handleDownloadPdf(
|
||||||
|
Number(pdf.id),
|
||||||
|
pdf.filename
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentPdf(pdf);
|
||||||
|
setIsDeletePdfOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* pagination controls */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{totalForExpandedGroup !== null &&
|
||||||
|
totalForExpandedGroup >
|
||||||
|
offset + limit && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleLoadMore}
|
||||||
|
>
|
||||||
|
Load more
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="ml-auto text-sm text-muted-foreground">
|
||||||
|
Showing{" "}
|
||||||
|
{Math.min(
|
||||||
|
offset + limit,
|
||||||
|
totalForExpandedGroup ?? 0
|
||||||
|
)}{" "}
|
||||||
|
of {totalForExpandedGroup ?? "?"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -223,7 +356,6 @@ export default function DocumentsPage() {
|
|||||||
className="ml-auto text-red-600 border-red-500 hover:bg-red-100 hover:border-red-600"
|
className="ml-auto text-red-600 border-red-500 hover:bg-red-100 hover:border-red-600"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFileBlobUrl(null);
|
setFileBlobUrl(null);
|
||||||
setSelectedPdfId(null);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✕ Close
|
✕ Close
|
||||||
|
|||||||
@@ -186,12 +186,12 @@ model InsuranceCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model PdfGroup {
|
model PdfGroup {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
titleKey PdfTitleKey?
|
titleKey PdfTitleKey @default(OTHER)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
patientId Int
|
patientId Int
|
||||||
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade)
|
||||||
pdfs PdfFile[]
|
pdfs PdfFile[]
|
||||||
|
|
||||||
@@index([patientId])
|
@@index([patientId])
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { PdfCategorySchema, PdfFileUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
import { PdfFileUncheckedCreateInputObjectSchema, PdfGroupUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
|
||||||
export type PdfGroup = z.infer<typeof PdfGroupUncheckedCreateInputObjectSchema>;
|
export type PdfGroup = z.infer<typeof PdfGroupUncheckedCreateInputObjectSchema>;
|
||||||
export type PdfFile = z.infer<typeof PdfFileUncheckedCreateInputObjectSchema>;
|
export type PdfFile = z.infer<typeof PdfFileUncheckedCreateInputObjectSchema>;
|
||||||
export type PdfCategory = z.infer<typeof PdfCategorySchema>;
|
|
||||||
|
|
||||||
export interface ClaimPdfMetadata {
|
export interface ClaimPdfMetadata {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export * from '../shared/schemas/objects/ClaimUncheckedCreateInput.schema'
|
|||||||
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/InsuranceCredentialUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/PdfFileUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/PdfGroupUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/enums/PdfCategory.schema'
|
|
||||||
export * from '../shared/schemas/enums/ClaimStatus.schema'
|
export * from '../shared/schemas/enums/ClaimStatus.schema'
|
||||||
export * from '../shared/schemas/objects/PaymentUncheckedCreateInput.schema'
|
export * from '../shared/schemas/objects/PaymentUncheckedCreateInput.schema'
|
||||||
export * from '../shared/schemas/objects/ServiceLineTransactionCreateInput.schema'
|
export * from '../shared/schemas/objects/ServiceLineTransactionCreateInput.schema'
|
||||||
|
|||||||
Reference in New Issue
Block a user