diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts index 497158d..528d9c2 100644 --- a/apps/Backend/src/routes/patients.ts +++ b/apps/Backend/src/routes/patients.ts @@ -6,6 +6,8 @@ import { PatientUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; +import { extractDobParts } from "../utils/DobParts"; +import { parseDobParts } from "../utils/DobPartsParsing"; const router = Router(); @@ -86,7 +88,7 @@ router.get("/recent", async (req: Request, res: Response) => { } }); -router.get("/search", async (req: Request, res: Response) => { +router.get("/search", async (req: Request, res: Response): Promise => { try { const { name, @@ -94,14 +96,24 @@ router.get("/search", async (req: Request, res: Response) => { insuranceId, gender, dob, + term, limit = "10", offset = "0", } = req.query as Record; const filters: any = { - userId: req.user!.id, // Assumes auth middleware sets this + userId: req.user!.id, }; + if (term) { + filters.OR = [ + { firstName: { contains: term, mode: "insensitive" } }, + { lastName: { contains: term, mode: "insensitive" } }, + { phone: { contains: term, mode: "insensitive" } }, + { insuranceId: { contains: term, mode: "insensitive" } }, + ]; + } + if (name) { filters.OR = [ { firstName: { contains: name, mode: "insensitive" } }, @@ -122,9 +134,70 @@ router.get("/search", async (req: Request, res: Response) => { } if (dob) { - const parsedDate = new Date(dob); - if (!isNaN(parsedDate.getTime())) { - filters.dateOfBirth = parsedDate; + const range = parseDobParts(dob); + + if (range) { + if (!filters.AND) filters.AND = []; + + // Check what kind of match this was + const fromYear = range.from.getUTCFullYear(); + const toYear = range.to.getUTCFullYear(); + const fromMonth = range.from.getUTCMonth() + 1; // Prisma months: 1-12 + const toMonth = range.to.getUTCMonth() + 1; + const fromDay = range.from.getUTCDate(); + const toDay = range.to.getUTCDate(); + + const isFullDate = + fromYear === toYear && fromMonth === toMonth && fromDay === toDay; + + const isDayMonthOnly = + fromYear === 1900 && + toYear === 2100 && + fromMonth === toMonth && + fromDay === toDay; + + const isMonthOnly = + fromYear === 1900 && + toYear === 2100 && + fromDay === 1 && + toDay >= 28 && + fromMonth === toMonth && + toMonth === toMonth; + + const isYearOnly = fromMonth === 1 && toMonth === 12; + + if (isFullDate) { + filters.AND.push({ + dob_day: fromDay, + dob_month: fromMonth, + dob_year: fromYear, + }); + } else if (isDayMonthOnly) { + filters.AND.push({ + dob_day: fromDay, + dob_month: fromMonth, + }); + } else if (isMonthOnly) { + filters.AND.push({ + dob_month: fromMonth, + }); + } else if (isYearOnly) { + filters.AND.push({ + dob_year: fromYear, + }); + } else { + // Fallback: search via dateOfBirth range + filters.AND.push({ + dateOfBirth: { + gte: range.from, + lte: range.to, + }, + }); + } + } else { + return res.status(400).json({ + message: `Invalid date format for DOB. Try formats like "12 March", "March", "1980", "12/03/1980", etc.`, + }); } } @@ -137,10 +210,10 @@ router.get("/search", async (req: Request, res: Response) => { storage.countPatients(filters), ]); - res.json({ patients, totalCount }); + return res.json({ patients, totalCount }); } catch (error) { console.error("Search error:", error); - res.status(500).json({ message: "Failed to search patients" }); + return res.status(500).json({ message: "Failed to search patients" }); } }); @@ -177,7 +250,6 @@ router.get( } ); - // Create a new patient router.post("/", async (req: Request, res: Response): Promise => { try { @@ -187,8 +259,14 @@ router.post("/", async (req: Request, res: Response): Promise => { userId: req.user!.id, }); - // Create patient - const patient = await storage.createPatient(patientData); + // Extract dob_* from dateOfBirth + const dobParts = extractDobParts(new Date(patientData.dateOfBirth)); + + const patient = await storage.createPatient({ + ...patientData, + ...dobParts, // adds dob_day/month/year + }); + res.status(201).json(patient); } catch (error) { if (error instanceof z.ZodError) { @@ -229,11 +307,15 @@ router.put( // Validate request body const patientData = updatePatientSchema.parse(req.body); + let dobParts = {}; + if (patientData.dateOfBirth) { + dobParts = extractDobParts(new Date(patientData.dateOfBirth)); + } // Update patient - const updatedPatient = await storage.updatePatient( - patientId, - patientData - ); + const updatedPatient = await storage.updatePatient(patientId, { + ...patientData, + ...dobParts, + }); res.json(updatedPatient); } catch (error) { if (error instanceof z.ZodError) { @@ -280,7 +362,7 @@ router.delete( // Delete patient await storage.deletePatient(patientId); res.status(204).send(); - } catch (error:any) { + } catch (error: any) { console.error("Delete patient error:", error); res.status(500).json({ message: "Failed to delete patient" }); } diff --git a/apps/Backend/src/utils/DobParts.ts b/apps/Backend/src/utils/DobParts.ts new file mode 100644 index 0000000..1076e5f --- /dev/null +++ b/apps/Backend/src/utils/DobParts.ts @@ -0,0 +1,7 @@ +export function extractDobParts(date: Date) { + return { + dob_day: date.getUTCDate(), + dob_month: date.getUTCMonth() + 1, + dob_year: date.getUTCFullYear(), + }; +} diff --git a/apps/Backend/src/utils/DobPartsParsing.ts b/apps/Backend/src/utils/DobPartsParsing.ts new file mode 100644 index 0000000..028e49e --- /dev/null +++ b/apps/Backend/src/utils/DobPartsParsing.ts @@ -0,0 +1,81 @@ +export function parseDobParts(input: string): { from: Date; to: Date } | null { + if (!input || typeof input !== "string") return null; + + const parts = input.trim().split(/[\s/-]+/).filter(Boolean); + + if (parts.length === 1) { + const part = parts[0]?.toLowerCase(); + + // Year + if (part && /^\d{4}$/.test(part)) { + const year = parseInt(part); + return { + from: new Date(Date.UTC(year, 0, 1)), + to: new Date(Date.UTC(year, 11, 31, 23, 59, 59)), + }; + } + + // Month + const month = part ? parseMonth(part) : null; + if (month !== null) { + return { + from: new Date(Date.UTC(1900, month, 1)), + to: new Date(Date.UTC(2100, month + 1, 0, 23, 59, 59)), + }; + } + + return null; + } + + if (parts.length === 2) { + const [part1, part2] = parts; + const day = tryParseInt(part1); + const month = part2 ? parseMonth(part2) : null; + + if (day !== null && month !== null) { + return { + from: new Date(Date.UTC(1900, month, day)), + to: new Date(Date.UTC(2100, month, day, 23, 59, 59)), + }; + } + + return null; + } + + if (parts.length === 3) { + const [part1, part2, part3] = parts; + const day = tryParseInt(part1); + const month = part2 ? parseMonth(part2) : null; + const year = tryParseInt(part3); + + if (day !== null && month !== null && year !== null) { + return { + from: new Date(Date.UTC(year, month, day)), + to: new Date(Date.UTC(year, month, day, 23, 59, 59)), + }; + } + + return null; + } + + return null; +} + +function parseMonth(input: string): number | null { + const normalized = input.toLowerCase(); + const months = [ + "january", "february", "march", "april", "may", "june", + "july", "august", "september", "october", "november", "december", + ]; + + const index = months.findIndex( + (m) => m === normalized || m.startsWith(normalized) + ); + return index !== -1 ? index : null; +} + +function tryParseInt(value?: string): number | null { + if (!value) return null; + const parsed = parseInt(value); + return isNaN(parsed) ? null : parsed; +} diff --git a/apps/Frontend/src/components/patients/patient-search.tsx b/apps/Frontend/src/components/patients/patient-search.tsx index 9f810c2..f540f6c 100644 --- a/apps/Frontend/src/components/patients/patient-search.tsx +++ b/apps/Frontend/src/components/patients/patient-search.tsx @@ -36,11 +36,11 @@ export function PatientSearch({ isSearchActive, }: PatientSearchProps) { const [searchTerm, setSearchTerm] = useState(""); - const [searchBy, setSearchBy] = useState("all"); + const [searchBy, setSearchBy] = useState("name"); const [showAdvanced, setShowAdvanced] = useState(false); const [advancedCriteria, setAdvancedCriteria] = useState({ searchTerm: "", - searchBy: "all", + searchBy: "name", }); const handleSearch = () => { @@ -72,7 +72,7 @@ export function PatientSearch({ }; return ( -
+
- + All Fields diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index 78d0f08..73293de 100644 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -37,6 +37,7 @@ 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"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -94,32 +95,62 @@ export function PatientTable({ const [debouncedSearchCriteria] = useDebounce(searchCriteria, 500); const { - data: patientsData, - isLoading, - isError, -} = useQuery({ - queryKey: ["patients", currentPage, debouncedSearchCriteria?.searchTerm || "recent"], - queryFn: async () => { - const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); - const isSearch = trimmedTerm && trimmedTerm.length > 0; + 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 baseUrl = isSearch - ? `/api/patients/search?term=${encodeURIComponent(trimmedTerm)}&by=${debouncedSearchCriteria!.searchBy}` - : `/api/patients/recent`; + const rawSearchBy = debouncedSearchCriteria?.searchBy || "name"; + const validSearchKeys = [ + "name", + "phone", + "insuranceId", + "gender", + "dob", + "all", + ]; + const searchKey = validSearchKeys.includes(rawSearchBy) + ? rawSearchBy + : "name"; - const hasQueryParams = baseUrl.includes("?"); - const url = `${baseUrl}${hasQueryParams ? "&" : "?"}limit=${patientsPerPage}&offset=${offset}`; + let url: string; - const res = await apiRequest("GET", url); - return res.json(); - }, - placeholderData: { - patients: [], - totalCount: 0, - }, -}); + 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({ @@ -356,14 +387,18 @@ export function PatientTable({ )} - - {patient.status} - +
+ + {patient.status === "active" ? "Active" : "Inactive"} + +
diff --git a/apps/Frontend/src/pages/dashboard.tsx b/apps/Frontend/src/pages/dashboard.tsx index 543ba14..2290734 100644 --- a/apps/Frontend/src/pages/dashboard.tsx +++ b/apps/Frontend/src/pages/dashboard.tsx @@ -118,7 +118,7 @@ export default function Dashboard() { }, onSuccess: (newPatient) => { setIsAddPatientOpen(false); - queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); + queryClient.invalidateQueries({ queryKey: ["patients"] }); toast({ title: "Success", description: "Patient added successfully!", diff --git a/apps/Frontend/src/pages/patients-page.tsx b/apps/Frontend/src/pages/patients-page.tsx index 765e470..e69ebcd 100644 --- a/apps/Frontend/src/pages/patients-page.tsx +++ b/apps/Frontend/src/pages/patients-page.tsx @@ -69,7 +69,7 @@ export default function PatientsPage() { }, onSuccess: (newPatient) => { setIsAddPatientOpen(false); - queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); + queryClient.invalidateQueries({ queryKey: ["patients"] }); toast({ title: "Success", description: "Patient added successfully!", diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 218e833..0993647 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -34,6 +34,9 @@ model Patient { firstName String lastName String dateOfBirth DateTime @db.Date + dob_day Int? + dob_month Int? + dob_year Int? gender String phone String email String?