From 8adb57eb96f682bf09376c1c3e4c4b1abcb5f454 Mon Sep 17 00:00:00 2001 From: Potenz Date: Sat, 5 Jul 2025 23:49:35 +0530 Subject: [PATCH] search checkpoint 1 --- apps/Backend/src/routes/patients.ts | 58 +++++ apps/Backend/src/storage/index.ts | 50 +++++ .../components/patients/patient-search.tsx | 36 +++- .../src/components/patients/patient-table.tsx | 201 ++++++++++-------- apps/Frontend/src/pages/patients-page.tsx | 21 -- 5 files changed, 247 insertions(+), 119 deletions(-) diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts index 32280a4..497158d 100644 --- a/apps/Backend/src/routes/patients.ts +++ b/apps/Backend/src/routes/patients.ts @@ -86,6 +86,64 @@ router.get("/recent", async (req: Request, res: Response) => { } }); +router.get("/search", async (req: Request, res: Response) => { + try { + const { + name, + phone, + insuranceId, + gender, + dob, + limit = "10", + offset = "0", + } = req.query as Record; + + const filters: any = { + userId: req.user!.id, // Assumes auth middleware sets this + }; + + if (name) { + filters.OR = [ + { firstName: { contains: name, mode: "insensitive" } }, + { lastName: { contains: name, mode: "insensitive" } }, + ]; + } + + if (phone) { + filters.phone = { contains: phone, mode: "insensitive" }; + } + + if (insuranceId) { + filters.insuranceId = { contains: insuranceId, mode: "insensitive" }; + } + + if (gender) { + filters.gender = gender; + } + + if (dob) { + const parsedDate = new Date(dob); + if (!isNaN(parsedDate.getTime())) { + filters.dateOfBirth = parsedDate; + } + } + + const [patients, totalCount] = await Promise.all([ + storage.searchPatients({ + filters, + limit: parseInt(limit), + offset: parseInt(offset), + }), + storage.countPatients(filters), + ]); + + res.json({ patients, totalCount }); + } catch (error) { + console.error("Search error:", error); + res.status(500).json({ message: "Failed to search patients" }); + } +}); + // Get a single patient by ID router.get( "/:id", diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 637e8f6..a9eb2d9 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -171,6 +171,24 @@ export interface IStorage { createPatient(patient: InsertPatient): Promise; updatePatient(id: number, patient: UpdatePatient): Promise; deletePatient(id: number): Promise; + searchPatients(args: { + filters: any; + limit: number; + offset: number; + }): Promise< + { + id: number; + firstName: string | null; + lastName: string | null; + phone: string | null; + gender: string | null; + dateOfBirth: Date; + insuranceId: string | null; + insuranceProvider: string | null; + status: string; + }[] + >; + countPatients(filters: any): Promise; // optional but useful // Appointment methods getAppointment(id: number): Promise; @@ -305,10 +323,42 @@ export const storage: IStorage = { }); }, + async searchPatients({ + filters, + limit, + offset, + }: { + filters: any; + limit: number; + offset: number; + }) { + return db.patient.findMany({ + where: filters, + orderBy: { createdAt: "desc" }, + take: limit, + skip: offset, + select: { + id: true, + firstName: true, + lastName: true, + phone: true, + gender: true, + dateOfBirth: true, + insuranceId: true, + insuranceProvider: true, + status: true, + }, + }); + }, + async getTotalPatientCount(): Promise { return db.patient.count(); }, + async countPatients(filters: any) { + return db.patient.count({ where: filters }); + }, + async createPatient(patient: InsertPatient): Promise { return await db.patient.create({ data: patient as Patient }); }, diff --git a/apps/Frontend/src/components/patients/patient-search.tsx b/apps/Frontend/src/components/patients/patient-search.tsx index 3da8a92..9f810c2 100644 --- a/apps/Frontend/src/components/patients/patient-search.tsx +++ b/apps/Frontend/src/components/patients/patient-search.tsx @@ -21,7 +21,7 @@ import { export type SearchCriteria = { searchTerm: string; - searchBy: "name" | "insuranceProvider" | "phone" | "insuranceId" | "all"; + searchBy: "name" | "insuranceId" | "phone" | "gender" | "dob" | "all"; }; interface PatientSearchProps { @@ -61,7 +61,10 @@ export function PatientSearch({ setShowAdvanced(false); }; - const updateAdvancedCriteria = (field: keyof SearchCriteria, value: string) => { + const updateAdvancedCriteria = ( + field: keyof SearchCriteria, + value: string + ) => { setAdvancedCriteria({ ...advancedCriteria, [field]: value, @@ -104,7 +107,9 @@ export function PatientSearch({ @@ -132,11 +138,16 @@ export function PatientSearch({
- +
- +
); -} \ No newline at end of file +} diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index 88fe4da..78d0f08 100644 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -35,6 +35,8 @@ import { 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"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -43,7 +45,6 @@ const PatientSchema = ( }); type Patient = z.infer; - const updatePatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) @@ -56,7 +57,6 @@ const updatePatientSchema = ( type UpdatePatient = z.infer; - interface PatientApiResponse { patients: Patient[]; totalCount: number; @@ -76,7 +76,6 @@ export function PatientTable({ const { toast } = useToast(); const { user } = useAuth(); - const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false); @@ -88,96 +87,111 @@ export function PatientTable({ const patientsPerPage = 5; const offset = (currentPage - 1) * patientsPerPage; + const [isSearchActive, setIsSearchActive] = useState(false); + const [searchCriteria, setSearchCriteria] = useState( + null + ); + const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500); + const { - data: patientsData, - isLoading, - isError, - } = useQuery({ - queryKey: ["patients", currentPage], - queryFn: async () => { - const res = await apiRequest( - "GET", - `/api/patients/recent?limit=${patientsPerPage}&offset=${offset}` - ); + data: patientsData, + isLoading, + isError, +} = useQuery({ + queryKey: ["patients", currentPage, debouncedSearchCriteria?.searchTerm || "recent"], + queryFn: async () => { + const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); + const isSearch = trimmedTerm && trimmedTerm.length > 0; + + const baseUrl = isSearch + ? `/api/patients/search?term=${encodeURIComponent(trimmedTerm)}&by=${debouncedSearchCriteria!.searchBy}` + : `/api/patients/recent`; + + const hasQueryParams = baseUrl.includes("?"); + const url = `${baseUrl}${hasQueryParams ? "&" : "?"}limit=${patientsPerPage}&offset=${offset}`; + + const res = await apiRequest("GET", url); + 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(); }, - placeholderData: { - patients: [], - totalCount: 0, + onSuccess: () => { + setIsAddPatientOpen(false); + queryClient.invalidateQueries({ queryKey: ["patients", currentPage] }); + toast({ + title: "Success", + description: "Patient updated successfully!", + variant: "default", + }); + }, + onError: (error) => { + toast({ + title: "Error", + description: `Failed to update 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: ["patients", currentPage] }); - 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", currentPage] }); - 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 deletePatientMutation = useMutation({ + mutationFn: async (id: number) => { + const res = await apiRequest("DELETE", `/api/patients/${id}`); + return; + }, + onSuccess: () => { + setIsDeletePatientOpen(false); + queryClient.invalidateQueries({ queryKey: ["patients", currentPage] }); + 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); @@ -244,6 +258,19 @@ export function PatientTable({ 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} + /> diff --git a/apps/Frontend/src/pages/patients-page.tsx b/apps/Frontend/src/pages/patients-page.tsx index 1926d27..765e470 100644 --- a/apps/Frontend/src/pages/patients-page.tsx +++ b/apps/Frontend/src/pages/patients-page.tsx @@ -4,10 +4,6 @@ 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"; @@ -56,9 +52,6 @@ export default function PatientsPage() { undefined ); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [searchCriteria, setSearchCriteria] = useState( - null - ); const addPatientModalRef = useRef(null); // File upload states @@ -112,14 +105,6 @@ export default function PatientsPage() { const isLoading = addPatientMutation.isPending; - // Search handling - const handleSearch = (criteria: SearchCriteria) => { - setSearchCriteria(criteria); - }; - - const handleClearSearch = () => { - setSearchCriteria(null); - }; // File upload handling const handleFileUpload = (file: File) => { @@ -252,12 +237,6 @@ export default function PatientsPage() { - -