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 { PatientSearch, SearchCriteria, } from "@/components/patients/patient-search"; 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/shared/schemas"; // import { Patient, InsertPatient, UpdatePatient } from "@repo/db/shared/schemas"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useAuth } from "@/hooks/use-auth"; import { z } from "zod"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ appointments: true, }); type Patient = z.infer; const insertPatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ id: true, createdAt: true, userId: true, }); type InsertPatient = z.infer; const updatePatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) .omit({ id: true, createdAt: true, userId: true, }) .partial(); type UpdatePatient = z.infer; // 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 [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [currentPatient, setCurrentPatient] = useState( undefined ); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [searchCriteria, setSearchCriteria] = useState( null ); const addPatientModalRef = useRef(null); // File upload states const [uploadedFile, setUploadedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); const [isExtracting, setIsExtracting] = useState(false); const [extractedInfo, setExtractedInfo] = useState(null); // Fetch patients const { data: patients = [], isLoading: isLoadingPatients, refetch: refetchPatients, } = useQuery({ queryKey: ["/api/patients/"], queryFn: async () => { const res = await apiRequest("GET", "/api/patients/"); return res.json(); }, enabled: !!user, }); // 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", }); }, }); // Update patient mutation const updatePatientMutation = useMutation({ mutationFn: async ({ id, patient, }: { id: number; patient: UpdatePatient; }) => { const res = await apiRequest("PUT", `/api/patients/${id}`, patient); return res.json(); }, onSuccess: () => { setIsAddPatientOpen(false); queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); toast({ title: "Success", description: "Patient updated successfully!", variant: "default", }); }, onError: (error) => { toast({ title: "Error", description: `Failed to update patient: ${error.message}`, variant: "destructive", }); }, }); const toggleMobileMenu = () => { setIsMobileMenuOpen(!isMobileMenuOpen); }; const handleAddPatient = (patient: InsertPatient) => { // Add userId to the patient data if (user) { addPatientMutation.mutate({ ...patient, userId: user.id, }); } }; const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => { if (currentPatient && user) { const { id, ...sanitizedPatient } = patient; updatePatientMutation.mutate({ id: currentPatient.id, patient: sanitizedPatient, }); } else { console.error("No current patient or user found for update"); toast({ title: "Error", description: "Cannot update patient: No patient or user found", variant: "destructive", }); } }; const handleEditPatient = (patient: Patient) => { setCurrentPatient(patient); setIsAddPatientOpen(true); }; const handleViewPatient = (patient: Patient) => { setCurrentPatient(patient); setIsViewPatientOpen(true); }; const isLoading = isLoadingPatients || addPatientMutation.isPending || updatePatientMutation.isPending; // Search handling const handleSearch = (criteria: SearchCriteria) => { setSearchCriteria(criteria); }; const handleClearSearch = () => { setSearchCriteria(null); }; // File upload handling const handleFileUpload = (file: File) => { setUploadedFile(file); setIsUploading(false); // In a real implementation, this would be set to true during upload toast({ title: "File Selected", description: `${file.name} is ready for processing.`, variant: "default", }); }; // Process file and extract patient information const handleExtractInfo = async () => { if (!uploadedFile) { toast({ title: "No file selected", description: "Please select a file first.", variant: "destructive", }); return; } setIsExtracting(true); try { // Read the file as base64 const reader = new FileReader(); // Set up a Promise to handle file reading const fileReadPromise = new Promise((resolve, reject) => { reader.onload = (event) => { if (event.target && typeof event.target.result === "string") { resolve(event.target.result); } else { reject(new Error("Failed to read file as base64")); } }; reader.onerror = () => { reject(new Error("Error reading file")); }; // Read the file as a data URL (base64) reader.readAsDataURL(uploadedFile); }); // Get the base64 data const base64Data = await fileReadPromise; // Send file to server as base64 const response = await fetch("/api/upload-file", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ pdfData: base64Data, filename: uploadedFile.name, }), credentials: "include", }); if (!response.ok) { throw new Error( `Server returned ${response.status}: ${response.statusText}` ); } const data = await response.json(); if (data.success) { // Only keep firstName, lastName, dateOfBirth, and insuranceId from the extracted info const simplifiedInfo = { firstName: data.extractedInfo.firstName, lastName: data.extractedInfo.lastName, dateOfBirth: data.extractedInfo.dateOfBirth, insuranceId: data.extractedInfo.insuranceId, }; setExtractedInfo(simplifiedInfo); // Show success message toast({ title: "Information Extracted", description: "Basic patient information (name, DOB, ID) has been extracted successfully.", variant: "default", }); // Open patient form pre-filled with extracted data setCurrentPatient(undefined); // Pre-fill the form by opening the modal with the extracted information setTimeout(() => { setIsAddPatientOpen(true); }, 500); } else { throw new Error(data.message || "Failed to extract information"); } } catch (error) { console.error("Error extracting information:", error); toast({ title: "Error", description: error instanceof Error ? error.message : "Failed to extract information from file", variant: "destructive", }); } finally { setIsExtracting(false); } }; // Filter patients based on search criteria const filteredPatients = useMemo(() => { if (!searchCriteria || !searchCriteria.searchTerm) { return patients; } const term = searchCriteria.searchTerm.toLowerCase(); return patients.filter((patient) => { switch (searchCriteria.searchBy) { case "name": return ( patient.firstName.toLowerCase().includes(term) || patient.lastName.toLowerCase().includes(term) ); case "phone": return patient.phone.toLowerCase().includes(term); case "insuranceProvider": return patient.insuranceProvider?.toLowerCase().includes(term); case "insuranceId": return patient.insuranceId?.toLowerCase().includes(term); case "all": default: return ( patient.firstName.toLowerCase().includes(term) || patient.lastName.toLowerCase().includes(term) || patient.phone.toLowerCase().includes(term) || patient.email?.toLowerCase().includes(term) || patient.address?.toLowerCase().includes(term) || patient.city?.toLowerCase().includes(term) || patient.insuranceProvider?.toLowerCase().includes(term) || patient.insuranceId?.toLowerCase().includes(term) ); } }); }, [patients, searchCriteria]); return (

Patients

Manage patient records and information

{/* File Upload Zone */}
Upload Patient Document
{/* Patients Table */} Patient Records View and manage all patient information {searchCriteria && (

Found {filteredPatients.length} {filteredPatients.length === 1 ? " patient" : " patients"} {searchCriteria.searchBy !== "all" ? ` with ${searchCriteria.searchBy}` : ""} matching "{searchCriteria.searchTerm}"

)}
{/* Add/Edit Patient Modal */}
); }