import { useEffect, useMemo, useState } from "react"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Delete, Edit, Eye, FileCheck } from "lucide-react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { PatientUncheckedCreateInputObjectSchema } from "@repo/db/usedSchemas"; import { z } from "zod"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useMutation, useQuery } from "@tanstack/react-query"; import LoadingScreen from "../ui/LoadingScreen"; import { useToast } from "@/hooks/use-toast"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AddPatientModal } from "./add-patient-modal"; import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import { useAuth } from "@/hooks/use-auth"; import { PatientSearch, SearchCriteria } from "./patient-search"; import { useDebounce } from "use-debounce"; import { cn } from "@/lib/utils"; import { Checkbox } from "../ui/checkbox"; import { formatDateToHumanReadable } from "@/utils/dateUtils"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ appointments: true, }); type Patient = z.infer; const updatePatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) .omit({ id: true, createdAt: true, userId: true, }) .partial(); type UpdatePatient = z.infer; interface PatientApiResponse { patients: Patient[]; totalCount: number; } interface PatientTableProps { allowEdit?: boolean; allowView?: boolean; allowDelete?: boolean; allowCheckbox?: boolean; allowNewClaim?: boolean; onNewClaim?: (patientId: number) => void; onSelectPatient?: (patient: Patient | null) => void; onPageChange?: (page: number) => void; onSearchChange?: (searchTerm: string) => void; } export function PatientTable({ allowEdit, allowView, allowDelete, allowCheckbox, allowNewClaim, onNewClaim, onSelectPatient, onPageChange, onSearchChange, }: PatientTableProps) { const { toast } = useToast(); const { user } = useAuth(); const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false); const [currentPatient, setCurrentPatient] = useState( undefined ); const [currentPage, setCurrentPage] = useState(1); const patientsPerPage = 5; const offset = (currentPage - 1) * patientsPerPage; const [isSearchActive, setIsSearchActive] = useState(false); const [searchCriteria, setSearchCriteria] = useState( null ); const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500); const [selectedPatientId, setSelectedPatientId] = useState( null ); const handleSelectPatient = (patient: Patient) => { const isSelected = selectedPatientId === patient.id; const newSelectedId = isSelected ? null : patient.id; setSelectedPatientId(newSelectedId); if (onSelectPatient) { onSelectPatient(isSelected ? null : patient); } }; const { data: patientsData, isLoading, isError, } = useQuery({ queryKey: [ "patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent", }, ], queryFn: async () => { const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); const isSearch = trimmedTerm && trimmedTerm.length > 0; const rawSearchBy = debouncedSearchCriteria?.searchBy || "name"; const validSearchKeys = [ "name", "phone", "insuranceId", "gender", "dob", "all", ]; const searchKey = validSearchKeys.includes(rawSearchBy) ? rawSearchBy : "name"; let url: string; if (isSearch) { const searchParams = new URLSearchParams({ limit: String(patientsPerPage), offset: String(offset), }); if (searchKey === "all") { searchParams.set("term", trimmedTerm!); } else { searchParams.set(searchKey, trimmedTerm!); } url = `/api/patients/search?${searchParams.toString()}`; } else { url = `/api/patients/recent?limit=${patientsPerPage}&offset=${offset}`; } const res = await apiRequest("GET", url); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.message || "Search failed"); } return res.json(); }, placeholderData: { patients: [], totalCount: 0, }, }); // 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: [ "patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent", }, ], }); toast({ title: "Success", description: "Patient updated successfully!", variant: "default", }); }, onError: (error) => { toast({ title: "Error", description: `Failed to update patient: ${error.message}`, variant: "destructive", }); }, }); const deletePatientMutation = useMutation({ mutationFn: async (id: number) => { const res = await apiRequest("DELETE", `/api/patients/${id}`); return; }, onSuccess: () => { setIsDeletePatientOpen(false); queryClient.invalidateQueries({ queryKey: [ "patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent", }, ], }); toast({ title: "Success", description: "Patient deleted successfully!", variant: "default", }); }, onError: (error) => { console.log(error); toast({ title: "Error", description: `Failed to delete patient: ${error.message}`, variant: "destructive", }); }, }); 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 handleDeletePatient = (patient: Patient) => { setCurrentPatient(patient); setIsDeletePatientOpen(true); }; const handleConfirmDeletePatient = async () => { if (currentPatient) { deletePatientMutation.mutate(currentPatient.id); } else { toast({ title: "Error", description: "No patient selected for deletion.", variant: "destructive", }); } }; useEffect(() => { if (onPageChange) onPageChange(currentPage); }, [currentPage, onPageChange]); useEffect(() => { const term = debouncedSearchCriteria?.searchTerm?.trim() || "recent"; if (onSearchChange) onSearchChange(term); }, [debouncedSearchCriteria, onSearchChange]); const totalPages = useMemo( () => Math.ceil((patientsData?.totalCount || 0) / patientsPerPage), [patientsData] ); const startItem = offset + 1; const endItem = Math.min( offset + patientsPerPage, patientsData?.totalCount || 0 ); const getInitials = (firstName: string, lastName: string) => { return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase(); }; const getAvatarColor = (id: number) => { const colorClasses = [ "bg-blue-500", "bg-teal-500", "bg-amber-500", "bg-rose-500", "bg-indigo-500", "bg-green-500", "bg-purple-500", ]; return colorClasses[id % colorClasses.length]; }; function getPageNumbers(current: number, total: number): (number | "...")[] { const delta = 2; const range: (number | "...")[] = []; const left = Math.max(2, current - delta); const right = Math.min(total - 1, current + delta); range.push(1); if (left > 2) range.push("..."); for (let i = left; i <= right; i++) { range.push(i); } if (right < total - 1) range.push("..."); if (total > 1) range.push(total); return range; } return (
{ setSearchCriteria(criteria); setCurrentPage(1); // reset page on new search setIsSearchActive(true); }} onClearSearch={() => { setSearchCriteria({ searchTerm: "", searchBy: "name" }); // triggers `recent` setCurrentPage(1); setIsSearchActive(false); }} isSearchActive={isSearchActive} /> {allowCheckbox && Select} Patient DOB / Gender Contact Insurance Status Actions {isLoading ? ( ) : isError ? ( Error loading patients. ) : patientsData?.patients.length === 0 ? ( No patients found. ) : ( patientsData?.patients.map((patient) => ( {allowCheckbox && ( handleSelectPatient(patient)} /> )}
{getInitials(patient.firstName, patient.lastName)}
{patient.firstName} {patient.lastName}
PID-{patient.id.toString().padStart(4, "0")}
{formatDateToHumanReadable(patient.dateOfBirth)}
{patient.gender}
{patient.phone}
{patient.email}
{patient.insuranceProvider ?? "Not specified"}
{patient.insuranceId && (
ID: {patient.insuranceId}
)}
{patient.status === "active" ? "Active" : "Inactive"}
{allowDelete && ( )} {allowEdit && ( )} {allowNewClaim && ( )} {allowView && ( )}
)) )}
{/* View Patient Modal */} Patient Details Complete information about the patient. {currentPatient && (
{currentPatient.firstName.charAt(0)} {currentPatient.lastName.charAt(0)}

{currentPatient.firstName} {currentPatient.lastName}

Patient ID: {currentPatient.id.toString().padStart(4, "0")}

Personal Information

Date of Birth:{" "} {formatDateToHumanReadable(currentPatient.dateOfBirth)}

Gender:{" "} {currentPatient.gender.charAt(0).toUpperCase() + currentPatient.gender.slice(1)}

Status:{" "} {currentPatient.status.charAt(0).toUpperCase() + currentPatient.status.slice(1)}

Contact Information

Phone:{" "} {currentPatient.phone}

Email:{" "} {currentPatient.email || "N/A"}

Address:{" "} {currentPatient.address ? ( <> {currentPatient.address} {currentPatient.city && `, ${currentPatient.city}`} {currentPatient.zipCode && ` ${currentPatient.zipCode}`} ) : ( "N/A" )}

Insurance

Provider:{" "} {currentPatient.insuranceProvider ? currentPatient.insuranceProvider === "delta" ? "Delta Dental" : currentPatient.insuranceProvider === "metlife" ? "MetLife" : currentPatient.insuranceProvider === "cigna" ? "Cigna" : currentPatient.insuranceProvider === "aetna" ? "Aetna" : currentPatient.insuranceProvider : "N/A"}

ID:{" "} {currentPatient.insuranceId || "N/A"}

Group Number:{" "} {currentPatient.groupNumber || "N/A"}

Policy Holder:{" "} {currentPatient.policyHolder || "Self"}

Medical Information

Allergies:{" "} {currentPatient.allergies || "None reported"}

Medical Conditions:{" "} {currentPatient.medicalConditions || "None reported"}

)}
{/* Add/Edit Patient Modal */} setIsDeletePatientOpen(false)} entityName={currentPatient?.name} /> {/* Pagination */} {totalPages > 1 && (
Showing {startItem}–{endItem} of {patientsData?.totalCount || 0}{" "} results
{ e.preventDefault(); if (currentPage > 1) setCurrentPage(currentPage - 1); }} className={ currentPage === 1 ? "pointer-events-none opacity-50" : "" } /> {getPageNumbers(currentPage, totalPages).map((page, idx) => ( {page === "..." ? ( ... ) : ( { e.preventDefault(); setCurrentPage(page as number); }} isActive={currentPage === page} > {page} )} ))} { e.preventDefault(); if (currentPage < totalPages) setCurrentPage(currentPage + 1); }} className={ currentPage === totalPages ? "pointer-events-none opacity-50" : "" } />
)}
); }