initial commit
This commit is contained in:
785
apps/Frontend/src/pages/documents-page.tsx
Executable file
785
apps/Frontend/src/pages/documents-page.tsx
Executable file
@@ -0,0 +1,785 @@
|
||||
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,
|
||||
viewDocument,
|
||||
downloadDocument,
|
||||
formatFileSize,
|
||||
type PatientDocument
|
||||
} from "@/lib/api/documents";
|
||||
|
||||
export default function DocumentsPage() {
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [expandedGroupId, setExpandedGroupId] = useState<number | null>(null);
|
||||
|
||||
// pagination state for the expanded group
|
||||
// pagination state
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [limit, setLimit] = useState<number>(5);
|
||||
const offset = (currentPage - 1) * limit;
|
||||
const [totalForExpandedGroup, setTotalForExpandedGroup] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
// Patient documents state
|
||||
const [patientDocuments, setPatientDocuments] = useState<PatientDocument[]>([]);
|
||||
const [patientDocumentsLoading, setPatientDocumentsLoading] = useState(false);
|
||||
const [showPatientDocuments, setShowPatientDocuments] = useState(false);
|
||||
const [documentThumbnails, setDocumentThumbnails] = useState<{ [key: number]: string }>({});
|
||||
|
||||
// Document preview state
|
||||
const [previewDocumentId, setPreviewDocumentId] = useState<number | null>(null);
|
||||
const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false);
|
||||
|
||||
// Delete document state
|
||||
const [deleteDocumentId, setDeleteDocumentId] = useState<number | null>(null);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
|
||||
// PDF state
|
||||
const [currentPdf, setCurrentPdf] = useState<PdfFile | null>(null);
|
||||
|
||||
// Delete dialog
|
||||
const [isDeletePdfOpen, setIsDeletePdfOpen] = useState(false);
|
||||
|
||||
// reset UI when patient changes
|
||||
useEffect(() => {
|
||||
setExpandedGroupId(null);
|
||||
setLimit(5);
|
||||
setCurrentPage(1);
|
||||
setTotalForExpandedGroup(null);
|
||||
setShowPatientDocuments(false); // Reset documents toggle
|
||||
|
||||
// close the preview modal
|
||||
setIsPreviewModalOpen(false);
|
||||
setPreviewDocumentId(null);
|
||||
|
||||
// Load patient documents when patient is selected
|
||||
if (selectedPatient?.id) {
|
||||
console.log("Patient selected, loading documents for:", selectedPatient.id);
|
||||
loadPatientDocuments(selectedPatient.id);
|
||||
} else {
|
||||
console.log("No patient selected, clearing documents");
|
||||
setPatientDocuments([]);
|
||||
}
|
||||
}, [selectedPatient]);
|
||||
|
||||
// Load patient documents function
|
||||
const loadPatientDocuments = async (patientId: number) => {
|
||||
try {
|
||||
setPatientDocumentsLoading(true);
|
||||
console.log("Loading documents for patient:", patientId);
|
||||
const response = await getPatientDocuments(patientId);
|
||||
console.log("Loaded documents:", response);
|
||||
if (response.success) {
|
||||
setPatientDocuments(response.documents);
|
||||
// Load thumbnails for image documents
|
||||
loadDocumentThumbnails(response.documents);
|
||||
} else {
|
||||
throw new Error("Failed to load documents");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load patient documents:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to load patient documents",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setPatientDocumentsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load thumbnails for image documents
|
||||
const loadDocumentThumbnails = async (documents: PatientDocument[]) => {
|
||||
const thumbnails: { [key: number]: string } = {};
|
||||
|
||||
for (const document of documents) {
|
||||
if (document.mimeType.startsWith('image/')) {
|
||||
try {
|
||||
// Use the document's filePath as the thumbnail URL
|
||||
thumbnails[document.id] = document.filePath;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load thumbnail for document ${document.id}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDocumentThumbnails(thumbnails);
|
||||
};
|
||||
|
||||
// Refresh patient documents (for after upload)
|
||||
const refreshPatientDocuments = async () => {
|
||||
if (selectedPatient?.id) {
|
||||
await loadPatientDocuments(selectedPatient.id);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for document upload events
|
||||
useEffect(() => {
|
||||
const handleDocumentUpload = (event: CustomEvent) => {
|
||||
console.log('Document upload event received:', event.detail);
|
||||
refreshPatientDocuments();
|
||||
};
|
||||
|
||||
// Add event listener for document uploads
|
||||
window.addEventListener('documentUploaded', handleDocumentUpload as EventListener);
|
||||
|
||||
// Also listen for storage events (for cross-tab communication)
|
||||
const handleStorageChange = (e: StorageEvent) => {
|
||||
if (e.key === 'documentUploaded' && e.newValue) {
|
||||
refreshPatientDocuments();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorageChange);
|
||||
|
||||
// Cleanup listeners
|
||||
return () => {
|
||||
window.removeEventListener('documentUploaded', handleDocumentUpload as EventListener);
|
||||
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,
|
||||
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<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",
|
||||
"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 (
|
||||
<div>
|
||||
<div className="container mx-auto space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Documents</h1>
|
||||
<p className="text-muted-foreground">
|
||||
View and manage recent uploaded claim PDFs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedPatient && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
Document groups of Patient: {selectedPatient.firstName}{" "}
|
||||
{selectedPatient.lastName}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
|
||||
|
||||
{/* Existing Groups Section */}
|
||||
<div>
|
||||
{/* <h4 className="text-lg font-semibold mb-3">Document Groups</h4> */}
|
||||
{isLoadingGroups || patientDocumentsLoading ? (
|
||||
<div>Loading groups…</div>
|
||||
) : (groups as any[]).length === 0 && patientDocuments.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
No groups found.
|
||||
</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),
|
||||
pdf.filename
|
||||
)
|
||||
}
|
||||
>
|
||||
<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 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="bg-white px-4 py-3 border-t border-gray-200 rounded">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="text-sm text-muted-foreground mb-2 sm:mb-0 whitespace-nowrap">
|
||||
Showing {startItem}–{endItem} of{" "}
|
||||
{totalForExpandedGroup || 0}{" "}
|
||||
results
|
||||
</div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (currentPage > 1)
|
||||
setCurrentPage(
|
||||
currentPage - 1
|
||||
);
|
||||
}}
|
||||
className={
|
||||
currentPage === 1
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{getPageNumbers(
|
||||
currentPage,
|
||||
totalPages
|
||||
).map((page, idx) => (
|
||||
<PaginationItem key={idx}>
|
||||
{page === "..." ? (
|
||||
<span className="px-2 text-gray-500">
|
||||
...
|
||||
</span>
|
||||
) : (
|
||||
<PaginationLink
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCurrentPage(
|
||||
page as number
|
||||
);
|
||||
}}
|
||||
isActive={
|
||||
currentPage === page
|
||||
}
|
||||
>
|
||||
{page}
|
||||
</PaginationLink>
|
||||
)}
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (
|
||||
currentPage < totalPages
|
||||
)
|
||||
setCurrentPage(
|
||||
currentPage + 1
|
||||
);
|
||||
}}
|
||||
className={
|
||||
currentPage === totalPages
|
||||
? "pointer-events-none opacity-50"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Patient Documents Section - moved outside groups mapping */}
|
||||
{patientDocuments.length > 0 && (
|
||||
<div>
|
||||
<div>
|
||||
{(groups as any[]).length > 0 && (
|
||||
<hr className="my-3 border-t border-gray-200" />
|
||||
)}
|
||||
</div>
|
||||
<div 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>
|
||||
<div className="font-semibold">
|
||||
Other Documents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant={showPatientDocuments ? "default" : "outline"}
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => setShowPatientDocuments(!showPatientDocuments)}
|
||||
>
|
||||
{showPatientDocuments ? (
|
||||
<>
|
||||
<span className="">Collapse</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Open</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showPatientDocuments && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-3">
|
||||
{patientDocuments.map((document: PatientDocument) => (
|
||||
<div
|
||||
key={document.id}
|
||||
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start space-x-3">
|
||||
{/* Thumbnail */}
|
||||
<div className="flex-shrink-0">
|
||||
{document.mimeType.startsWith('image/') && documentThumbnails[document.id] ? (
|
||||
<img
|
||||
src={documentThumbnails[document.id]}
|
||||
alt={document.originalName}
|
||||
className="w-10 h-10 object-cover rounded-lg border border-gray-200"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg border border-gray-200 flex items-center justify-center">
|
||||
{document.mimeType.startsWith('image/') ? (
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
) : (
|
||||
<FileText className="h-6 w-6 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-gray-900 truncate">
|
||||
{document.originalName}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1 mt-1">
|
||||
<p className="text-xs text-gray-500">
|
||||
{formatFileSize(document.fileSize)}
|
||||
</p>
|
||||
<span className="text-[12px]">|</span>
|
||||
<div className="flex">
|
||||
<span className={`inline-flex items-center text-xs font-medium ${document.mimeType.startsWith('image/')
|
||||
? 'text-green-800'
|
||||
: document.mimeType === 'application/pdf'
|
||||
? 'text-red-800'
|
||||
: 'text-blue-800'
|
||||
}`}>
|
||||
{document.mimeType.startsWith('image/') ? 'Image' :
|
||||
document.mimeType === 'application/pdf' ? 'PDF' :
|
||||
document.mimeType.split('/')[1]?.toUpperCase() || 'File'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center space-x-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handlePreviewDocument(document.id)}
|
||||
className="h-8 w-8 text-blue-600 hover:text-blue-800 hover:bg-blue-50"
|
||||
title="View Document"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={async () => {
|
||||
const doc = patientDocuments.find(doc => doc.id === document.id);
|
||||
if (doc?.filePath) {
|
||||
try {
|
||||
// Fetch the file as a blob to force download
|
||||
const response = await fetch(doc.filePath);
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = window.document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = doc.originalName;
|
||||
window.document.body.appendChild(link);
|
||||
link.click();
|
||||
window.document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-8 w-8 text-green-600 hover:text-green-800 hover:bg-green-50"
|
||||
title="Download Document"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDeleteDocument(document.id)}
|
||||
className="h-8 w-8 text-red-600 hover:text-red-800 hover:bg-red-50"
|
||||
title="Delete Document"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>
|
||||
Select a patient to view document groups
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView
|
||||
allowDelete
|
||||
allowCheckbox
|
||||
allowEdit
|
||||
onSelectPatient={setSelectedPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={isDeletePdfOpen}
|
||||
onConfirm={handleConfirmDeletePdf}
|
||||
onCancel={() => setIsDeletePdfOpen(false)}
|
||||
entityName={`PDF #${currentPdf?.id}`}
|
||||
/>
|
||||
|
||||
{/* Document Preview Modal */}
|
||||
<DocumentsFilePreviewModal
|
||||
fileId={previewDocumentId}
|
||||
isOpen={isPreviewModalOpen}
|
||||
onClose={() => {
|
||||
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 */}
|
||||
<DeleteConfirmationDialog
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onConfirm={confirmDeleteDocument}
|
||||
onCancel={() => {
|
||||
setIsDeleteDialogOpen(false);
|
||||
setDeleteDocumentId(null);
|
||||
}}
|
||||
entityName={patientDocuments.find(doc => doc.id === deleteDocumentId)?.originalName || `Document #${deleteDocumentId}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user