feat(multiple pdf uploading - patient creation) - done
This commit is contained in:
@@ -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 ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user