import { useEffect, useMemo, useState } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { Card, CardHeader, CardTitle, CardDescription, CardContent, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { toast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { Eye, Trash, Download, FolderOpen, FileText } from "lucide-react"; import { DeleteConfirmationDialog } from "@/components/ui/deleteDialog"; import { PatientTable } from "@/components/patients/patient-table"; import { Patient, PdfFile } from "@repo/db/types"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import DocumentsFilePreviewModal from "@/components/documents/file-preview-modal"; import { getPageNumbers } from "@/utils/pageNumberGenerator"; import { getPatientDocuments, deleteDocument, formatFileSize, type PatientDocument } from "@/lib/api/documents"; export default function DocumentsPage() { const [selectedPatient, setSelectedPatient] = useState(null); const [expandedGroupId, setExpandedGroupId] = useState(null); // pagination state for the expanded group const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(5); const offset = (currentPage - 1) * limit; const [totalForExpandedGroup, setTotalForExpandedGroup] = useState< number | null >(null); const [showPatientDocuments, setShowPatientDocuments] = useState(false); // Document preview state const [previewDocumentId, setPreviewDocumentId] = useState(null); const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false); // Delete document state const [deleteDocumentId, setDeleteDocumentId] = useState(null); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); // PDF state const [currentPdf, setCurrentPdf] = useState(null); // Delete dialog const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false); // reset UI when patient changes useEffect(() => { setExpandedGroupId(null); setLimit(5); setCurrentPage(1); setTotalForExpandedGroup(null); setShowPatientDocuments(false); setIsPreviewModalOpen(false); setPreviewDocumentId(null); }, [selectedPatient]); // Patient documents — React Query for caching (re-selecting same patient shows instantly) const { data: patientDocuments = [], isLoading: patientDocumentsLoading } = useQuery({ queryKey: ["patientDocuments", selectedPatient?.id], enabled: !!selectedPatient?.id, staleTime: 2 * 60 * 1000, queryFn: async () => { const response = await getPatientDocuments(selectedPatient!.id); if (!response.success) throw new Error("Failed to load documents"); return response.documents; }, }); // Derive thumbnails synchronously — no extra async round-trip const documentThumbnails = useMemo(() => { const result: { [key: number]: string } = {}; for (const doc of patientDocuments) { if (doc.mimeType.startsWith("image/")) result[doc.id] = doc.filePath; } return result; }, [patientDocuments]); // Listen for document upload events — invalidate React Query cache instead of manual refetch useEffect(() => { const refresh = () => { if (selectedPatient?.id) { queryClient.invalidateQueries({ queryKey: ["patientDocuments", selectedPatient.id] }); } }; window.addEventListener("documentUploaded", refresh); const handleStorageChange = (e: StorageEvent) => { if (e.key === "documentUploaded" && e.newValue) refresh(); }; window.addEventListener("storage", handleStorageChange); return () => { window.removeEventListener("documentUploaded", refresh); window.removeEventListener("storage", handleStorageChange); }; }, [selectedPatient]); // Handle document preview const handlePreviewDocument = (documentId: number) => { const document = patientDocuments.find(doc => doc.id === documentId); setPreviewDocumentId(documentId); setIsPreviewModalOpen(true); }; // Handle document delete const handleDeleteDocument = (documentId: number) => { setDeleteDocumentId(documentId); setIsDeleteDialogOpen(true); }; // Confirm document delete const confirmDeleteDocument = async () => { if (!deleteDocumentId) return; try { const response = await deleteDocument(deleteDocumentId); if (response.success) { toast({ title: "Success", description: "Document deleted successfully", }); // Reload documents for the current patient if (selectedPatient?.id) { await loadPatientDocuments(selectedPatient.id); } } else { throw new Error("Failed to delete document"); } } catch (error) { console.error("Failed to delete document:", error); toast({ title: "Error", description: "Failed to delete document", variant: "destructive", }); } finally { setIsDeleteDialogOpen(false); setDeleteDocumentId(null); } }; // FETCH GROUPS for patient (includes `category` on each group) const { data: groups = [], isLoading: isLoadingGroups } = useQuery({ queryKey: ["groups", selectedPatient?.id], enabled: !!selectedPatient, staleTime: 2 * 60 * 1000, queryFn: async () => { const res = await apiRequest( "GET", `/api/documents/pdf-groups/patient/${selectedPatient?.id}` ); return res.json(); }, }); // Group groups by titleKey (use titleKey only for grouping/ordering; don't display it) const groupsByTitleKey = useMemo(() => { const map = new Map(); 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", "INSURANCE_CLAIM_PREAUTH", "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, currentPage, limit], enabled: !!expandedGroupId, queryFn: async () => { // API should accept ?limit & ?offset and also return total count const res = await apiRequest( "GET", `/api/documents/recent-pdf-files/group/${expandedGroupId}?limit=${limit}&offset=${offset}` ); // 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}`); }, onSuccess: () => { setIsDeletePdfOpen(false); setCurrentPdf(null); queryClient.invalidateQueries({ queryKey: ["groupPdfs", expandedGroupId], }); toast({ title: "Success", description: "PDF deleted successfully!" }); }, onError: (error: any) => { toast({ title: "Error", description: error.message || "Failed to delete PDF", variant: "destructive", }); }, }); const handleConfirmDeletePdf = () => { if (currentPdf) { deletePdfMutation.mutate(Number(currentPdf.id)); } else { toast({ title: "Error", description: "No PDF selected for deletion.", variant: "destructive", }); } }; const handleViewPdf = (pdfId: number, filename?: string) => { setPreviewDocumentId(pdfId); setIsPreviewModalOpen(true); }; const handleDownloadPdf = async (pdfId: number, filename: string) => { const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`); const arrayBuffer = await res.arrayBuffer(); const blob = new Blob([arrayBuffer], { type: "application/pdf" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; // Expand / collapse a group — when expanding reset pagination const toggleExpandGroup = (groupId: number) => { if (expandedGroupId === groupId) { setExpandedGroupId(null); setCurrentPage(1); setLimit(5); setTotalForExpandedGroup(null); } else { setExpandedGroupId(groupId); setCurrentPage(1); setLimit(5); setTotalForExpandedGroup(null); } }; // pagintaion helper const totalPages = totalForExpandedGroup ? Math.ceil(totalForExpandedGroup / limit) : 1; const startItem = totalForExpandedGroup ? offset + 1 : 0; const endItem = totalForExpandedGroup ? Math.min(offset + limit, totalForExpandedGroup) : 0; return (

Documents

View and manage recent uploaded claim PDFs

{selectedPatient && ( Document groups of Patient: {selectedPatient.firstName}{" "} {selectedPatient.lastName} {/* Existing Groups Section */}
{/*

Document Groups

*/} {isLoadingGroups ? (
Loading groups…
) : (groups as any[]).length === 0 && patientDocuments.length === 0 && !patientDocumentsLoading ? (
No groups found.
) : (
{groupsByTitleKey.orderedKeys.map((key, idx) => { const bucket = groupsByTitleKey.map.get(key) ?? []; return (
{/* subtle divider between buckets (no enum text shown) */} {idx !== 0 && (
)}
{bucket.map((group: any) => (
{/* Only show the group's title string */}
{group.title}
{/* expanded content: show paginated PDFs for this group */} {expandedGroupId === group.id && (
{isFetchingPdfs ? (
Loading PDFs…
) : groupPdfs.length === 0 ? (
No PDFs in this group.
) : (
{groupPdfs.map((pdf) => (
{pdf.filename}
))} {/* Pagination */} {totalPages > 1 && (
Showing {startItem}–{endItem} of{" "} {totalForExpandedGroup || 0}{" "} results
{ e.preventDefault(); if (currentPage > 1) setCurrentPage( currentPage - 1 ); }} className={ currentPage === 1 ? "pointer-events-none opacity-50" : "" } /> {getPageNumbers( currentPage, totalPages ).map((page, idx) => ( {page === "..." ? ( ... ) : ( { e.preventDefault(); setCurrentPage( page as number ); }} isActive={ currentPage === page } > {page} )} ))} { e.preventDefault(); if ( currentPage < totalPages ) setCurrentPage( currentPage + 1 ); }} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : "" } />
)}
)}
)}
))}
); })} {/* Patient Documents Section - moved outside groups mapping */} {patientDocuments.length > 0 && (
{(groups as any[]).length > 0 && (
)}
Other Documents
{showPatientDocuments && (
{patientDocuments.map((document: PatientDocument) => (
{/* Thumbnail */}
{document.mimeType.startsWith('image/') && documentThumbnails[document.id] ? ( {document.originalName} ) : (
{document.mimeType.startsWith('image/') ? (
) : ( )}
)}
{/* Document Info */}

{document.originalName}

{formatFileSize(document.fileSize)}

|
{document.mimeType.startsWith('image/') ? 'Image' : document.mimeType === 'application/pdf' ? 'PDF' : document.mimeType.split('/')[1]?.toUpperCase() || 'File'}
{/* Actions */}
))}
)}
)}
)}
)} Patient Records Select a patient to view document groups setIsDeletePdfOpen(false)} entityName={`PDF #${currentPdf?.id}`} /> {/* Document Preview Modal */} { setIsPreviewModalOpen(false); setPreviewDocumentId(null); }} isPatientDocument={patientDocuments.some(doc => doc.id === previewDocumentId)} directImageUrl={patientDocuments.find(doc => doc.id === previewDocumentId)?.filePath} initialFileName={patientDocuments.find(doc => doc.id === previewDocumentId)?.originalName} /> {/* Document Delete Confirmation Dialog */} { setIsDeleteDialogOpen(false); setDeleteDocumentId(null); }} entityName={patientDocuments.find(doc => doc.id === deleteDocumentId)?.originalName || `Document #${deleteDocumentId}`} />
); }