263 lines
8.1 KiB
TypeScript
263 lines
8.1 KiB
TypeScript
import { useState, useMemo, useRef } from "react";
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { TopAppBar } from "@/components/layout/top-app-bar";
|
|
import { Sidebar } from "@/components/layout/sidebar";
|
|
import { PatientTable } from "@/components/patients/patient-table";
|
|
import { AddPatientModal } from "@/components/patients/add-patient-modal";
|
|
import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas";
|
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { z } from "zod";
|
|
import useExtractPdfData from "@/hooks/use-extractPdfData";
|
|
import { useLocation } from "wouter";
|
|
|
|
const PatientSchema = (
|
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
|
).omit({
|
|
appointments: true,
|
|
});
|
|
type Patient = z.infer<typeof PatientSchema>;
|
|
|
|
const insertPatientSchema = (
|
|
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
|
|
).omit({
|
|
id: true,
|
|
createdAt: true,
|
|
userId: true,
|
|
});
|
|
type InsertPatient = z.infer<typeof insertPatientSchema>;
|
|
|
|
// Type for the ref to access modal methods
|
|
type AddPatientModalRef = {
|
|
shouldSchedule: boolean;
|
|
navigateToSchedule: (patientId: number) => void;
|
|
};
|
|
|
|
export default function PatientsPage() {
|
|
const { toast } = useToast();
|
|
const { user } = useAuth();
|
|
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
|
|
const [currentPatient, setCurrentPatient] = useState<Patient | undefined>(
|
|
undefined
|
|
);
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
|
|
|
|
// File upload states
|
|
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [isExtracting, setIsExtracting] = useState(false);
|
|
const { mutate: extractPdf } = useExtractPdfData();
|
|
const [location, navigate] = useLocation();
|
|
|
|
// Add patient mutation
|
|
const addPatientMutation = useMutation({
|
|
mutationFn: async (patient: InsertPatient) => {
|
|
const res = await apiRequest("POST", "/api/patients/", patient);
|
|
return res.json();
|
|
},
|
|
onSuccess: (newPatient) => {
|
|
setIsAddPatientOpen(false);
|
|
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] });
|
|
toast({
|
|
title: "Success",
|
|
description: "Patient added successfully!",
|
|
variant: "default",
|
|
});
|
|
|
|
// If the add patient modal wants to proceed to scheduling, redirect to appointments page
|
|
if (addPatientModalRef.current?.shouldSchedule) {
|
|
addPatientModalRef.current.navigateToSchedule(newPatient.id);
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
toast({
|
|
title: "Error",
|
|
description: `Failed to add patient: ${error.message}`,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
const toggleMobileMenu = () => {
|
|
setIsMobileMenuOpen(!isMobileMenuOpen);
|
|
};
|
|
|
|
const handleAddPatient = (patient: InsertPatient) => {
|
|
if (user) {
|
|
addPatientMutation.mutate({
|
|
...patient,
|
|
userId: user.id,
|
|
});
|
|
}
|
|
};
|
|
|
|
const isLoading = addPatientMutation.isPending;
|
|
|
|
|
|
// File upload handling
|
|
const handleFileUpload = (file: File) => {
|
|
setIsUploading(true);
|
|
setUploadedFile(file);
|
|
|
|
toast({
|
|
title: "File Selected",
|
|
description: `${file.name} is ready for processing.`,
|
|
variant: "default",
|
|
});
|
|
|
|
setIsUploading(false);
|
|
};
|
|
|
|
// data extraction
|
|
const handleExtract = () => {
|
|
setIsExtracting(true);
|
|
|
|
if (!uploadedFile) {
|
|
return toast({
|
|
title: "Error",
|
|
description: "Please upload a PDF",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
extractPdf(uploadedFile, {
|
|
onSuccess: (data) => {
|
|
setIsExtracting(false);
|
|
|
|
toast({
|
|
title: "Success Pdf Data Extracted",
|
|
description: `Name: ${data.name}, Member ID: ${data.memberId}, DOB: ${data.dob}`,
|
|
variant: "default",
|
|
});
|
|
|
|
const params = new URLSearchParams({
|
|
name: data.name,
|
|
memberId: data.memberId,
|
|
dob: data.dob,
|
|
});
|
|
|
|
navigate(
|
|
`/claims?name=${encodeURIComponent(data.name)}&memberId=${data.memberId}&dob=${data.dob}`
|
|
);
|
|
},
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="flex h-screen overflow-hidden bg-gray-100">
|
|
<Sidebar
|
|
isMobileOpen={isMobileMenuOpen}
|
|
setIsMobileOpen={setIsMobileMenuOpen}
|
|
/>
|
|
|
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
<TopAppBar toggleMobileMenu={toggleMobileMenu} />
|
|
|
|
<main className="flex-1 overflow-y-auto p-4">
|
|
<div className="container mx-auto space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Patients</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage patient records and information
|
|
</p>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<Button
|
|
onClick={() => {
|
|
setCurrentPatient(undefined);
|
|
setIsAddPatientOpen(true);
|
|
}}
|
|
className="gap-1"
|
|
disabled={isLoading}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
New Patient
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* File Upload Zone */}
|
|
<div className="grid gap-4 md:grid-cols-4">
|
|
<div className="md:col-span-3">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">
|
|
Upload Patient Document
|
|
</CardTitle>
|
|
<File className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<FileUploadZone
|
|
onFileUpload={handleFileUpload}
|
|
isUploading={isUploading}
|
|
acceptedFileTypes="application/pdf"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
<div className="md:col-span-1 flex items-end">
|
|
<Button
|
|
className="w-full h-12 gap-2"
|
|
disabled={!uploadedFile || isExtracting}
|
|
onClick={handleExtract}
|
|
>
|
|
{isExtracting ? (
|
|
<>
|
|
<RefreshCw className="h-4 w-4 animate-spin" />
|
|
Processing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<FilePlus className="h-4 w-4" />
|
|
Extract Info And Claim
|
|
</>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Patients Table */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Patient Records</CardTitle>
|
|
<CardDescription>
|
|
View and manage all patient information
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<PatientTable
|
|
allowDelete={true}
|
|
allowEdit={true}
|
|
allowView={true}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Add/Edit Patient Modal */}
|
|
<AddPatientModal
|
|
ref={addPatientModalRef}
|
|
open={isAddPatientOpen}
|
|
onOpenChange={setIsAddPatientOpen}
|
|
onSubmit={handleAddPatient}
|
|
isLoading={isLoading}
|
|
patient={currentPatient}
|
|
/>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|