fix(UI, PdfGroups) - Schema updated, ui changed, backend Route changed
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import {
|
||||
Card,
|
||||
@@ -16,23 +16,34 @@ import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { Patient, PdfFile } from "@repo/db/types";
|
||||
|
||||
export default function DocumentsPage() {
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<number | null>(null);
|
||||
const [selectedPdfId, setSelectedPdfId] = useState<number | null>(null);
|
||||
const [expandedGroupId, setExpandedGroupId] = 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 [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
||||
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(() => {
|
||||
setSelectedGroupId(null);
|
||||
setExpandedGroupId(null);
|
||||
setLimit(5);
|
||||
setOffset(0);
|
||||
setTotalForExpandedGroup(null);
|
||||
setFileBlobUrl(null);
|
||||
setSelectedPdfId(null);
|
||||
}, [selectedPatient]);
|
||||
|
||||
const { data: groups = [] } = useQuery({
|
||||
// FETCH GROUPS for patient (includes `category` on each group)
|
||||
const { data: groups = [], isLoading: isLoadingGroups } = useQuery({
|
||||
queryKey: ["groups", selectedPatient?.id],
|
||||
enabled: !!selectedPatient,
|
||||
queryFn: async () => {
|
||||
@@ -44,18 +55,59 @@ export default function DocumentsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const { data: groupPdfs = [] } = useQuery({
|
||||
queryKey: ["groupPdfs", selectedGroupId],
|
||||
enabled: !!selectedGroupId,
|
||||
// Group groups by titleKey (use titleKey only for grouping/ordering; don't display it)
|
||||
const groupsByTitleKey = useMemo(() => {
|
||||
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 () => {
|
||||
// API should accept ?limit & ?offset and also return total count
|
||||
const res = await apiRequest(
|
||||
"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({
|
||||
mutationFn: async (id: number) => {
|
||||
await apiRequest("DELETE", `/api/documents/pdf-files/${id}`);
|
||||
@@ -63,9 +115,9 @@ export default function DocumentsPage() {
|
||||
onSuccess: () => {
|
||||
setIsDeletePdfOpen(false);
|
||||
setCurrentPdf(null);
|
||||
if (selectedGroupId != null) {
|
||||
if (expandedGroupId != null) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["groupPdfs", selectedGroupId],
|
||||
queryKey: ["groupPdfs", expandedGroupId],
|
||||
});
|
||||
}
|
||||
toast({ title: "Success", description: "PDF deleted successfully!" });
|
||||
@@ -97,7 +149,6 @@ export default function DocumentsPage() {
|
||||
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setFileBlobUrl(url);
|
||||
setSelectedPdfId(pdfId);
|
||||
};
|
||||
|
||||
const handleDownloadPdf = async (pdfId: number, filename: string) => {
|
||||
@@ -114,6 +165,25 @@ export default function DocumentsPage() {
|
||||
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 (
|
||||
<div>
|
||||
<div className="container mx-auto space-y-6">
|
||||
@@ -130,85 +200,148 @@ export default function DocumentsPage() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Document Groups for {selectedPatient.firstName}{" "}
|
||||
Document groups of Patient: {selectedPatient.firstName}{" "}
|
||||
{selectedPatient.lastName}
|
||||
</CardTitle>
|
||||
<CardDescription>Select a group to view PDFs</CardDescription>
|
||||
</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 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>PDFs in Group</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
{groupPdfs.length === 0 ? (
|
||||
<p className="text-muted-foreground">
|
||||
No PDFs found in this group.
|
||||
</p>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoadingGroups ? (
|
||||
<div>Loading groups…</div>
|
||||
) : (groups as any[]).length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No groups found.
|
||||
</div>
|
||||
) : (
|
||||
groupPdfs.map((pdf: any) => (
|
||||
<div
|
||||
key={pdf.id}
|
||||
className="flex justify-between items-center border p-2 rounded"
|
||||
>
|
||||
<span className="text-sm">{pdf.filename}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewPdf(pdf.id)}
|
||||
>
|
||||
<Eye className="w-4 h-4 text-gray-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDownloadPdf(pdf.id, pdf.filename)}
|
||||
>
|
||||
<Download className="w-4 h-4 text-blue-600" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setCurrentPdf(pdf);
|
||||
setIsDeletePdfOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash className="w-4 h-4 text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<div className="flex flex-col gap-3">
|
||||
{groupsByTitleKey.orderedKeys.map((key, idx) => {
|
||||
const bucket = groupsByTitleKey.map.get(key) ?? [];
|
||||
return (
|
||||
<div key={key}>
|
||||
{/* subtle divider between buckets (no enum text shown) */}
|
||||
{idx !== 0 && (
|
||||
<hr className="my-3 border-t border-gray-200" />
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{bucket.map((group: any) => (
|
||||
<div key={group.id} className="border rounded p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<FolderOpen className="w-5 h-5" />
|
||||
<div>
|
||||
{/* Only show the group's title string */}
|
||||
<div className="font-semibold">
|
||||
{group.title}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={
|
||||
expandedGroupId === group.id
|
||||
? "default"
|
||||
: "outline"
|
||||
}
|
||||
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>
|
||||
</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"
|
||||
onClick={() => {
|
||||
setFileBlobUrl(null);
|
||||
setSelectedPdfId(null);
|
||||
}}
|
||||
>
|
||||
✕ Close
|
||||
|
||||
Reference in New Issue
Block a user