feat(multiple pdf uploading - patient creation) - done

This commit is contained in:
2025-10-11 00:58:30 +05:30
parent 4e4c1e180f
commit a8dbd98e91

View File

@@ -2,7 +2,6 @@ import { useState, useRef, useCallback } from "react";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { PatientTable } from "@/components/patients/patient-table"; import { PatientTable } from "@/components/patients/patient-table";
import { AddPatientModal } from "@/components/patients/add-patient-modal"; import { AddPatientModal } from "@/components/patients/add-patient-modal";
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, RefreshCw, FilePlus } from "lucide-react"; import { Plus, RefreshCw, FilePlus } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -21,6 +20,10 @@ import { InsertPatient, Patient } from "@repo/db/types";
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table"; import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
import { parse } from "date-fns"; import { parse } from "date-fns";
import { formatLocalDate } from "@/utils/dateUtils"; import { formatLocalDate } from "@/utils/dateUtils";
import {
MultipleFileUploadZone,
MultipleFileUploadZoneHandle,
} from "@/components/file-upload/multiple-file-upload-zone";
// Type for the ref to access modal methods // Type for the ref to access modal methods
type AddPatientModalRef = { type AddPatientModalRef = {
@@ -40,9 +43,13 @@ export default function PatientsPage() {
const addPatientModalRef = useRef<AddPatientModalRef | null>(null); const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
// File upload states // File upload states
const [uploadedFile, setUploadedFile] = useState<File | null>(null); const uploadRef = useRef<MultipleFileUploadZoneHandle | null>(null);
const [uploadedFiles, setUploadedFiles] = useState<File[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
// extraction state (single boolean for whole process)
const [isExtracting, setIsExtracting] = useState(false); const [isExtracting, setIsExtracting] = useState(false);
const { mutate: extractPdf } = useExtractPdfData(); const { mutate: extractPdf } = useExtractPdfData();
const [location, navigate] = useLocation(); const [location, navigate] = useLocation();
@@ -91,18 +98,19 @@ export default function PatientsPage() {
const isLoading = addPatientMutation.isPending; const isLoading = addPatientMutation.isPending;
// File upload handling // Hook up file-change coming from MultipleFileUploadZone
const handleFileUpload = (file: File) => { const handleFilesChange = (files: File[]) => {
setIsUploading(true); // ensure we only keep PDFs (defensive)
setUploadedFile(file); const pdfs = files.filter((f) => f.type === "application/pdf");
if (pdfs.length !== files.length) {
toast({ toast({
title: "File Selected", title: "Non-PDF ignored",
description: `${file.name} is ready for processing.`, description:
variant: "default", "Only PDF files are accepted — other file types were ignored.",
}); variant: "destructive",
});
setIsUploading(false); }
setUploadedFiles(pdfs);
}; };
/** /**
@@ -113,9 +121,9 @@ export default function PatientsPage() {
* - shows toasts for errors, * - shows toasts for errors,
* - returns Patient on success or null on error. * - returns Patient on success or null on error.
*/ */
const extractAndEnsurePatient = const extractAndEnsurePatientForFile = useCallback(
useCallback(async (): Promise<Patient | null> => { async (file: File): Promise<Patient | null> => {
if (!uploadedFile) { if (!file) {
toast({ toast({
title: "Error", title: "Error",
description: "Please upload a PDF", description: "Please upload a PDF",
@@ -130,7 +138,7 @@ export default function PatientsPage() {
const data: { name: string; memberId: string; dob: string } = const data: { name: string; memberId: string; dob: string } =
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
try { try {
extractPdf(uploadedFile, { extractPdf(file, {
onSuccess: (d) => resolve(d as any), onSuccess: (d) => resolve(d as any),
onError: (err: any) => reject(err), onError: (err: any) => reject(err),
}); });
@@ -300,23 +308,61 @@ export default function PatientsPage() {
} finally { } finally {
setIsExtracting(false); setIsExtracting(false);
} }
}, [uploadedFile, extractPdf]); },
[extractPdf]
);
// handlers are now minimal and don't repeat error/toast logic // These two operate only when exactly one file selected
const handleExtractAndClaim = async () => { const handleExtractAndClaim = async () => {
const patient = await extractAndEnsurePatient(); if (uploadedFiles.length !== 1) return;
if (!patient) return; // error already shown in helper const patient = await extractAndEnsurePatientForFile(uploadedFiles[0]!);
if (!patient) return;
navigate(`/claims?newPatient=${patient.id}`); navigate(`/claims?newPatient=${patient.id}`);
}; };
const handleExtractAndAppointment = async () => { const handleExtractAndAppointment = async () => {
const patient = await extractAndEnsurePatient(); if (uploadedFiles.length !== 1) return;
const patient = await extractAndEnsurePatientForFile(uploadedFiles[0]!);
if (!patient) return; if (!patient) return;
navigate(`/appointments?newPatient=${patient.id}`); navigate(`/appointments?newPatient=${patient.id}`);
}; };
// Batch: iterate files one-by-one and call extractAndEnsurePatientForFile
const handleExtractAndSave = async () => { const handleExtractAndSave = async () => {
await extractAndEnsurePatient(); if (uploadedFiles.length === 0) {
toast({
title: "No files",
description: "Please upload one or more PDF files first.",
variant: "destructive",
});
return;
}
setIsExtracting(true);
// iterate serially so server isn't hit all at once and order is predictable
for (let i = 0; i < uploadedFiles.length; i++) {
const file = uploadedFiles[i]!;
toast({
title: `Processing file ${i + 1} of ${uploadedFiles.length}`,
description: file.name,
variant: "default",
});
// await each file
/* eslint-disable no-await-in-loop */
await extractAndEnsurePatientForFile(file);
/* eslint-enable no-await-in-loop */
}
setIsExtracting(false);
// optionally clear files after a successful batch run:
setUploadedFiles([]);
if (uploadRef.current) uploadRef.current.reset?.();
toast({
title: "Batch complete",
description: `Processed ${uploadedFiles.length} file(s).`,
variant: "default",
});
}; };
return ( return (
@@ -354,16 +400,19 @@ export default function PatientsPage() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<FileUploadZone <MultipleFileUploadZone
onFileUpload={handleFileUpload} ref={uploadRef}
onFilesChange={handleFilesChange}
isUploading={isUploading} isUploading={isUploading}
acceptedFileTypes="application/pdf" acceptedFileTypes="application/pdf"
maxFiles={20}
maxFileSizeMB={20}
/> />
<div className="flex flex-col-2 gap-2 mt-4"> <div className="flex flex-col-2 gap-2 mt-4">
<Button <Button
className="w-full h-12 gap-2" className="w-full h-12 gap-2"
disabled={!uploadedFile || isExtracting} disabled={uploadedFiles.length === 0 || isExtracting}
onClick={handleExtractAndSave} onClick={handleExtractAndSave}
> >
{isExtracting ? ( {isExtracting ? (
@@ -380,7 +429,7 @@ export default function PatientsPage() {
</Button> </Button>
<Button <Button
className="w-full h-12 gap-2" className="w-full h-12 gap-2"
disabled={!uploadedFile || isExtracting} disabled={uploadedFiles.length !== 1 || isExtracting}
onClick={handleExtractAndAppointment} onClick={handleExtractAndAppointment}
> >
{isExtracting ? ( {isExtracting ? (
@@ -397,7 +446,7 @@ export default function PatientsPage() {
</Button> </Button>
<Button <Button
className="w-full h-12 gap-2" className="w-full h-12 gap-2"
disabled={!uploadedFile || isExtracting} disabled={uploadedFiles.length !== 1 || isExtracting}
onClick={handleExtractAndClaim} onClick={handleExtractAndClaim}
> >
{isExtracting ? ( {isExtracting ? (