search checkpoint 1

This commit is contained in:
2025-07-05 23:49:35 +05:30
parent 015d677c7e
commit 8adb57eb96
5 changed files with 247 additions and 119 deletions

View File

@@ -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<string, string>;
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 // Get a single patient by ID
router.get( router.get(
"/:id", "/:id",

View File

@@ -171,6 +171,24 @@ export interface IStorage {
createPatient(patient: InsertPatient): Promise<Patient>; createPatient(patient: InsertPatient): Promise<Patient>;
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>; updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
deletePatient(id: number): Promise<void>; deletePatient(id: number): Promise<void>;
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<number>; // optional but useful
// Appointment methods // Appointment methods
getAppointment(id: number): Promise<Appointment | undefined>; getAppointment(id: number): Promise<Appointment | undefined>;
@@ -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<number> { async getTotalPatientCount(): Promise<number> {
return db.patient.count(); return db.patient.count();
}, },
async countPatients(filters: any) {
return db.patient.count({ where: filters });
},
async createPatient(patient: InsertPatient): Promise<Patient> { async createPatient(patient: InsertPatient): Promise<Patient> {
return await db.patient.create({ data: patient as Patient }); return await db.patient.create({ data: patient as Patient });
}, },

View File

@@ -21,7 +21,7 @@ import {
export type SearchCriteria = { export type SearchCriteria = {
searchTerm: string; searchTerm: string;
searchBy: "name" | "insuranceProvider" | "phone" | "insuranceId" | "all"; searchBy: "name" | "insuranceId" | "phone" | "gender" | "dob" | "all";
}; };
interface PatientSearchProps { interface PatientSearchProps {
@@ -61,7 +61,10 @@ export function PatientSearch({
setShowAdvanced(false); setShowAdvanced(false);
}; };
const updateAdvancedCriteria = (field: keyof SearchCriteria, value: string) => { const updateAdvancedCriteria = (
field: keyof SearchCriteria,
value: string
) => {
setAdvancedCriteria({ setAdvancedCriteria({
...advancedCriteria, ...advancedCriteria,
[field]: value, [field]: value,
@@ -104,7 +107,9 @@ export function PatientSearch({
<Select <Select
value={searchBy} value={searchBy}
onValueChange={(value) => setSearchBy(value as SearchCriteria["searchBy"])} onValueChange={(value) =>
setSearchBy(value as SearchCriteria["searchBy"])
}
> >
<SelectTrigger className="w-[180px]"> <SelectTrigger className="w-[180px]">
<SelectValue placeholder="Search by..." /> <SelectValue placeholder="Search by..." />
@@ -113,8 +118,9 @@ export function PatientSearch({
<SelectItem value="all">All Fields</SelectItem> <SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem> <SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem> <SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceProvider">Insurance Provider</SelectItem> <SelectItem value="insuranceId">InsuranceId</SelectItem>
<SelectItem value="insuranceId">Insurance ID</SelectItem> <SelectItem value="gender">Gender</SelectItem>
<SelectItem value="dob">DOB</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -132,11 +138,16 @@ export function PatientSearch({
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<label className="text-right text-sm font-medium">Search by</label> <label className="text-right text-sm font-medium">
Search by
</label>
<Select <Select
value={advancedCriteria.searchBy} value={advancedCriteria.searchBy}
onValueChange={(value) => onValueChange={(value) =>
updateAdvancedCriteria("searchBy", value as SearchCriteria["searchBy"]) updateAdvancedCriteria(
"searchBy",
value as SearchCriteria["searchBy"]
)
} }
> >
<SelectTrigger className="col-span-3"> <SelectTrigger className="col-span-3">
@@ -146,14 +157,17 @@ export function PatientSearch({
<SelectItem value="all">All Fields</SelectItem> <SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem> <SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem> <SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceProvider">Insurance Provider</SelectItem> <SelectItem value="insuranceId">InsuranceId</SelectItem>
<SelectItem value="insuranceId">Insurance ID</SelectItem> <SelectItem value="gender">Gender</SelectItem>
<SelectItem value="dob">DOB</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid grid-cols-4 items-center gap-4"> <div className="grid grid-cols-4 items-center gap-4">
<label className="text-right text-sm font-medium">Search term</label> <label className="text-right text-sm font-medium">
Search term
</label>
<Input <Input
className="col-span-3" className="col-span-3"
value={advancedCriteria.searchTerm} value={advancedCriteria.searchTerm}
@@ -182,4 +196,4 @@ export function PatientSearch({
</div> </div>
</div> </div>
); );
} }

View File

@@ -35,6 +35,8 @@ import {
import { AddPatientModal } from "./add-patient-modal"; import { AddPatientModal } from "./add-patient-modal";
import { DeleteConfirmationDialog } from "../ui/deleteDialog"; import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { PatientSearch, SearchCriteria } from "./patient-search";
import { useDebounce } from "use-debounce";
const PatientSchema = ( const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -43,7 +45,6 @@ const PatientSchema = (
}); });
type Patient = z.infer<typeof PatientSchema>; type Patient = z.infer<typeof PatientSchema>;
const updatePatientSchema = ( const updatePatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
) )
@@ -56,7 +57,6 @@ const updatePatientSchema = (
type UpdatePatient = z.infer<typeof updatePatientSchema>; type UpdatePatient = z.infer<typeof updatePatientSchema>;
interface PatientApiResponse { interface PatientApiResponse {
patients: Patient[]; patients: Patient[];
totalCount: number; totalCount: number;
@@ -76,7 +76,6 @@ export function PatientTable({
const { toast } = useToast(); const { toast } = useToast();
const { user } = useAuth(); const { user } = useAuth();
const [isAddPatientOpen, setIsAddPatientOpen] = useState(false); const [isAddPatientOpen, setIsAddPatientOpen] = useState(false);
const [isViewPatientOpen, setIsViewPatientOpen] = useState(false); const [isViewPatientOpen, setIsViewPatientOpen] = useState(false);
const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false); const [isDeletePatientOpen, setIsDeletePatientOpen] = useState(false);
@@ -88,96 +87,111 @@ export function PatientTable({
const patientsPerPage = 5; const patientsPerPage = 5;
const offset = (currentPage - 1) * patientsPerPage; const offset = (currentPage - 1) * patientsPerPage;
const [isSearchActive, setIsSearchActive] = useState(false);
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(
null
);
const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500);
const { const {
data: patientsData, data: patientsData,
isLoading, isLoading,
isError, isError,
} = useQuery<PatientApiResponse>({ } = useQuery<PatientApiResponse>({
queryKey: ["patients", currentPage], queryKey: ["patients", currentPage, debouncedSearchCriteria?.searchTerm || "recent"],
queryFn: async () => { queryFn: async () => {
const res = await apiRequest( const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim();
"GET", const isSearch = trimmedTerm && trimmedTerm.length > 0;
`/api/patients/recent?limit=${patientsPerPage}&offset=${offset}`
); 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(); return res.json();
}, },
placeholderData: { onSuccess: () => {
patients: [], setIsAddPatientOpen(false);
totalCount: 0, 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 deletePatientMutation = useMutation({
const updatePatientMutation = useMutation({ mutationFn: async (id: number) => {
mutationFn: async ({ const res = await apiRequest("DELETE", `/api/patients/${id}`);
id, return;
patient, },
}: { onSuccess: () => {
id: number; setIsDeletePatientOpen(false);
patient: UpdatePatient; queryClient.invalidateQueries({ queryKey: ["patients", currentPage] });
}) => { toast({
const res = await apiRequest("PUT", `/api/patients/${id}`, patient); title: "Success",
return res.json(); description: "Patient deleted successfully!",
}, variant: "default",
onSuccess: () => { });
setIsAddPatientOpen(false); },
queryClient.invalidateQueries({ queryKey: ["patients", currentPage] }); onError: (error) => {
toast({ console.log(error);
title: "Success", toast({
description: "Patient updated successfully!", title: "Error",
variant: "default", description: `Failed to delete patient: ${error.message}`,
}); variant: "destructive",
}, });
onError: (error) => { },
toast({ });
title: "Error",
description: `Failed to update patient: ${error.message}`, const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
variant: "destructive", if (currentPatient && user) {
}); const { id, ...sanitizedPatient } = patient;
}, updatePatientMutation.mutate({
}); id: currentPatient.id,
patient: sanitizedPatient,
const deletePatientMutation = useMutation({ });
mutationFn: async (id: number) => { } else {
const res = await apiRequest("DELETE", `/api/patients/${id}`); console.error("No current patient or user found for update");
return; toast({
}, title: "Error",
onSuccess: () => { description: "Cannot update patient: No patient or user found",
setIsDeletePatientOpen(false); variant: "destructive",
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) => { const handleEditPatient = (patient: Patient) => {
setCurrentPatient(patient); setCurrentPatient(patient);
setIsAddPatientOpen(true); setIsAddPatientOpen(true);
@@ -244,6 +258,19 @@ export function PatientTable({
return ( return (
<div className="bg-white shadow rounded-lg overflow-hidden"> <div className="bg-white shadow rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<PatientSearch
onSearch={(criteria) => {
setSearchCriteria(criteria);
setCurrentPage(1); // reset page on new search
setIsSearchActive(true);
}}
onClearSearch={() => {
setSearchCriteria({ searchTerm: "", searchBy: "name" }); // triggers `recent`
setCurrentPage(1);
setIsSearchActive(false);
}}
isSearchActive={isSearchActive}
/>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>

View File

@@ -4,10 +4,6 @@ import { TopAppBar } from "@/components/layout/top-app-bar";
import { Sidebar } from "@/components/layout/sidebar"; import { Sidebar } from "@/components/layout/sidebar";
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 {
PatientSearch,
SearchCriteria,
} from "@/components/patients/patient-search";
import { FileUploadZone } from "@/components/file-upload/file-upload-zone"; import { FileUploadZone } from "@/components/file-upload/file-upload-zone";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Plus, RefreshCw, File, FilePlus } from "lucide-react"; import { Plus, RefreshCw, File, FilePlus } from "lucide-react";
@@ -56,9 +52,6 @@ export default function PatientsPage() {
undefined undefined
); );
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchCriteria, setSearchCriteria] = useState<SearchCriteria | null>(
null
);
const addPatientModalRef = useRef<AddPatientModalRef | null>(null); const addPatientModalRef = useRef<AddPatientModalRef | null>(null);
// File upload states // File upload states
@@ -112,14 +105,6 @@ export default function PatientsPage() {
const isLoading = addPatientMutation.isPending; const isLoading = addPatientMutation.isPending;
// Search handling
const handleSearch = (criteria: SearchCriteria) => {
setSearchCriteria(criteria);
};
const handleClearSearch = () => {
setSearchCriteria(null);
};
// File upload handling // File upload handling
const handleFileUpload = (file: File) => { const handleFileUpload = (file: File) => {
@@ -252,12 +237,6 @@ export default function PatientsPage() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<PatientSearch
onSearch={handleSearch}
onClearSearch={handleClearSearch}
isSearchActive={!!searchCriteria}
/>
<PatientTable <PatientTable
allowDelete={true} allowDelete={true}
allowEdit={true} allowEdit={true}