patient table fixed

This commit is contained in:
2025-07-11 21:22:52 +05:30
parent 8adb57eb96
commit 27dc669aae
8 changed files with 258 additions and 50 deletions

View File

@@ -6,6 +6,8 @@ import {
PatientUncheckedCreateInputObjectSchema, PatientUncheckedCreateInputObjectSchema,
} from "@repo/db/usedSchemas"; } from "@repo/db/usedSchemas";
import { z } from "zod"; import { z } from "zod";
import { extractDobParts } from "../utils/DobParts";
import { parseDobParts } from "../utils/DobPartsParsing";
const router = Router(); 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<any> => {
try { try {
const { const {
name, name,
@@ -94,14 +96,24 @@ router.get("/search", async (req: Request, res: Response) => {
insuranceId, insuranceId,
gender, gender,
dob, dob,
term,
limit = "10", limit = "10",
offset = "0", offset = "0",
} = req.query as Record<string, string>; } = req.query as Record<string, string>;
const filters: any = { 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) { if (name) {
filters.OR = [ filters.OR = [
{ firstName: { contains: name, mode: "insensitive" } }, { firstName: { contains: name, mode: "insensitive" } },
@@ -122,9 +134,70 @@ router.get("/search", async (req: Request, res: Response) => {
} }
if (dob) { if (dob) {
const parsedDate = new Date(dob); const range = parseDobParts(dob);
if (!isNaN(parsedDate.getTime())) {
filters.dateOfBirth = parsedDate; 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), storage.countPatients(filters),
]); ]);
res.json({ patients, totalCount }); return res.json({ patients, totalCount });
} catch (error) { } catch (error) {
console.error("Search error:", 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 // Create a new patient
router.post("/", async (req: Request, res: Response): Promise<any> => { router.post("/", async (req: Request, res: Response): Promise<any> => {
try { try {
@@ -187,8 +259,14 @@ router.post("/", async (req: Request, res: Response): Promise<any> => {
userId: req.user!.id, userId: req.user!.id,
}); });
// Create patient // Extract dob_* from dateOfBirth
const patient = await storage.createPatient(patientData); const dobParts = extractDobParts(new Date(patientData.dateOfBirth));
const patient = await storage.createPatient({
...patientData,
...dobParts, // adds dob_day/month/year
});
res.status(201).json(patient); res.status(201).json(patient);
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
@@ -229,11 +307,15 @@ router.put(
// Validate request body // Validate request body
const patientData = updatePatientSchema.parse(req.body); const patientData = updatePatientSchema.parse(req.body);
let dobParts = {};
if (patientData.dateOfBirth) {
dobParts = extractDobParts(new Date(patientData.dateOfBirth));
}
// Update patient // Update patient
const updatedPatient = await storage.updatePatient( const updatedPatient = await storage.updatePatient(patientId, {
patientId, ...patientData,
patientData ...dobParts,
); });
res.json(updatedPatient); res.json(updatedPatient);
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
@@ -280,7 +362,7 @@ router.delete(
// Delete patient // Delete patient
await storage.deletePatient(patientId); await storage.deletePatient(patientId);
res.status(204).send(); res.status(204).send();
} catch (error:any) { } catch (error: any) {
console.error("Delete patient error:", error); console.error("Delete patient error:", error);
res.status(500).json({ message: "Failed to delete patient" }); res.status(500).json({ message: "Failed to delete patient" });
} }

View File

@@ -0,0 +1,7 @@
export function extractDobParts(date: Date) {
return {
dob_day: date.getUTCDate(),
dob_month: date.getUTCMonth() + 1,
dob_year: date.getUTCFullYear(),
};
}

View File

@@ -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;
}

View File

@@ -36,11 +36,11 @@ export function PatientSearch({
isSearchActive, isSearchActive,
}: PatientSearchProps) { }: PatientSearchProps) {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("all"); const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("name");
const [showAdvanced, setShowAdvanced] = useState(false); const [showAdvanced, setShowAdvanced] = useState(false);
const [advancedCriteria, setAdvancedCriteria] = useState<SearchCriteria>({ const [advancedCriteria, setAdvancedCriteria] = useState<SearchCriteria>({
searchTerm: "", searchTerm: "",
searchBy: "all", searchBy: "name",
}); });
const handleSearch = () => { const handleSearch = () => {
@@ -72,7 +72,7 @@ export function PatientSearch({
}; };
return ( return (
<div className="w-full"> <div className="w-full pt-8 pb-4 px-4">
<div className="flex gap-2 mb-4"> <div className="flex gap-2 mb-4">
<div className="relative flex-1"> <div className="relative flex-1">
<Input <Input
@@ -151,7 +151,7 @@ export function PatientSearch({
} }
> >
<SelectTrigger className="col-span-3"> <SelectTrigger className="col-span-3">
<SelectValue placeholder="All Fields" /> <SelectValue placeholder="Name" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Fields</SelectItem> <SelectItem value="all">All Fields</SelectItem>

View File

@@ -37,6 +37,7 @@ import { DeleteConfirmationDialog } from "../ui/deleteDialog";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { PatientSearch, SearchCriteria } from "./patient-search"; import { PatientSearch, SearchCriteria } from "./patient-search";
import { useDebounce } from "use-debounce"; import { useDebounce } from "use-debounce";
import { cn } from "@/lib/utils";
const PatientSchema = ( const PatientSchema = (
PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any> PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject<any>
@@ -97,29 +98,59 @@ export function PatientTable({
data: patientsData, data: patientsData,
isLoading, isLoading,
isError, isError,
} = useQuery<PatientApiResponse>({ } = useQuery<PatientApiResponse, Error>({
queryKey: ["patients", currentPage, debouncedSearchCriteria?.searchTerm || "recent"], queryKey: ["patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent" }],
queryFn: async () => { queryFn: async () => {
const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim();
const isSearch = trimmedTerm && trimmedTerm.length > 0; const isSearch = trimmedTerm && trimmedTerm.length > 0;
const baseUrl = isSearch const rawSearchBy = debouncedSearchCriteria?.searchBy || "name";
? `/api/patients/search?term=${encodeURIComponent(trimmedTerm)}&by=${debouncedSearchCriteria!.searchBy}` const validSearchKeys = [
: `/api/patients/recent`; "name",
"phone",
"insuranceId",
"gender",
"dob",
"all",
];
const searchKey = validSearchKeys.includes(rawSearchBy)
? rawSearchBy
: "name";
const hasQueryParams = baseUrl.includes("?"); let url: string;
const url = `${baseUrl}${hasQueryParams ? "&" : "?"}limit=${patientsPerPage}&offset=${offset}`;
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); const res = await apiRequest("GET", url);
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.message || "Search failed");
}
return res.json(); return res.json();
}, },
placeholderData: { placeholderData: {
patients: [], patients: [],
totalCount: 0, totalCount: 0,
}, },
}); }
);
// Update patient mutation // Update patient mutation
const updatePatientMutation = useMutation({ const updatePatientMutation = useMutation({
@@ -356,14 +387,18 @@ export function PatientTable({
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <div className="col-span-1">
variant={ <span
patient.status === "active" ? "success" : "warning" className={cn(
} "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium",
className="capitalize" patient.status === "active"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
)}
> >
{patient.status} {patient.status === "active" ? "Active" : "Inactive"}
</Badge> </span>
</div>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end space-x-2"> <div className="flex justify-end space-x-2">

View File

@@ -118,7 +118,7 @@ export default function Dashboard() {
}, },
onSuccess: (newPatient) => { onSuccess: (newPatient) => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); queryClient.invalidateQueries({ queryKey: ["patients"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",

View File

@@ -69,7 +69,7 @@ export default function PatientsPage() {
}, },
onSuccess: (newPatient) => { onSuccess: (newPatient) => {
setIsAddPatientOpen(false); setIsAddPatientOpen(false);
queryClient.invalidateQueries({ queryKey: ["/api/patients/"] }); queryClient.invalidateQueries({ queryKey: ["patients"] });
toast({ toast({
title: "Success", title: "Success",
description: "Patient added successfully!", description: "Patient added successfully!",

View File

@@ -34,6 +34,9 @@ model Patient {
firstName String firstName String
lastName String lastName String
dateOfBirth DateTime @db.Date dateOfBirth DateTime @db.Date
dob_day Int?
dob_month Int?
dob_year Int?
gender String gender String
phone String phone String
email String? email String?