diff --git a/apps/Backend/src/routes/patients.ts b/apps/Backend/src/routes/patients.ts index 528d9c2..6fc81b7 100644 --- a/apps/Backend/src/routes/patients.ts +++ b/apps/Backend/src/routes/patients.ts @@ -7,7 +7,6 @@ import { } from "@repo/db/usedSchemas"; import { z } from "zod"; import { extractDobParts } from "../utils/DobParts"; -import { parseDobParts } from "../utils/DobPartsParsing"; const router = Router(); @@ -134,75 +133,18 @@ router.get("/search", async (req: Request, res: Response): Promise => { } if (dob) { - 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 { + const parsed = new Date(dob); + if (isNaN(parsed.getTime())) { return res.status(400).json({ - message: `Invalid date format for DOB. Try formats like "12 March", "March", "1980", "12/03/1980", etc.`, + message: "Invalid date format for DOB. Use format: YYYY-MM-DD", }); } + // Match exact dateOfBirth (optional: adjust for timezone) + filters.dateOfBirth = parsed; } const [patients, totalCount] = await Promise.all([ - storage.searchPatients({ + storage.searchPatients({ filters, limit: parseInt(limit), offset: parseInt(offset), @@ -259,13 +201,8 @@ router.post("/", async (req: Request, res: Response): Promise => { userId: req.user!.id, }); - // Extract dob_* from dateOfBirth - const dobParts = extractDobParts(new Date(patientData.dateOfBirth)); - const patient = await storage.createPatient({ - ...patientData, - ...dobParts, // adds dob_day/month/year - }); + const patient = await storage.createPatient(patientData); res.status(201).json(patient); } catch (error) { @@ -307,15 +244,8 @@ 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, - ...dobParts, - }); + const updatedPatient = await storage.updatePatient(patientId, patientData); res.json(updatedPatient); } catch (error) { if (error instanceof z.ZodError) { diff --git a/apps/Backend/src/utils/DobPartsParsing.ts b/apps/Backend/src/utils/DobPartsParsing.ts deleted file mode 100644 index 028e49e..0000000 --- a/apps/Backend/src/utils/DobPartsParsing.ts +++ /dev/null @@ -1,81 +0,0 @@ -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-form.tsx b/apps/Frontend/src/components/patients/patient-form.tsx index 11aa9a3..7b6964c 100644 --- a/apps/Frontend/src/components/patients/patient-form.tsx +++ b/apps/Frontend/src/components/patients/patient-form.tsx @@ -21,6 +21,16 @@ import { } from "@/components/ui/select"; import { useEffect, useMemo } from "react"; import { forwardRef, useImperativeHandle } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Calendar } from "../ui/calendar"; +import { CalendarIcon } from "lucide-react"; +import { format } from "date-fns"; +import { Button } from "../ui/button"; +import { cn } from "@/lib/utils"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -116,7 +126,9 @@ export const PatientForm = forwardRef( useImperativeHandle(ref, () => ({ submit() { - (document.getElementById("patient-form") as HTMLFormElement | null)?.requestSubmit(); + ( + document.getElementById("patient-form") as HTMLFormElement | null + )?.requestSubmit(); }, })); @@ -195,9 +207,41 @@ export const PatientForm = forwardRef( render={({ field }) => ( Date of Birth * - - - + + + + + + + + { + if (date) { + const localDate = format(date, "yyyy-MM-dd"); + field.onChange(localDate); + } + }} + disabled={ + (date) => date > new Date() // Prevent future dates + } + /> + + )} @@ -319,6 +363,32 @@ export const PatientForm = forwardRef( Insurance Information
+ ( + + Status * + + + + )} + /> +
- setSearchTerm(e.target.value)} - className="pr-10" - onKeyDown={(e) => { - if (e.key === "Enter") { - handleSearch(); - } - }} - /> + {searchBy === "dob" ? ( + + + + + + { + if (date) { + const formattedDate = format(date, "yyyy-MM-dd"); + setSearchTerm(String(formattedDate)); + } + }} + disabled={(date) => date > new Date()} + /> + + + ) : ( + setSearchTerm(e.target.value)} + className="pr-10" + onKeyDown={(e) => { + if (e.key === "Enter") { + handleSearch(); + } + }} + /> + )} {searchTerm && ( )} + + + + { + if (date) { + const formattedDate = format(date, "yyyy-MM-dd"); + updateAdvancedCriteria( + "searchTerm", + String(formattedDate) + ); + } + }} + disabled={(date) => date > new Date()} + /> + + + ) : ( + + updateAdvancedCriteria("searchTerm", e.target.value) + } + placeholder="Enter search term..." + /> + )}
diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index 73293de..1bca632 100644 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -10,7 +10,6 @@ import { import { Button } from "@/components/ui/button"; import { Delete, Edit, Eye } from "lucide-react"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Badge } from "@/components/ui/badge"; import { Pagination, PaginationContent, @@ -38,6 +37,7 @@ 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"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject @@ -67,12 +67,16 @@ interface PatientTableProps { allowEdit?: boolean; allowView?: boolean; allowDelete?: boolean; + allowCheckbox?: boolean; + onSelectPatient?: (patient: Patient) => void; } export function PatientTable({ allowEdit, allowView, allowDelete, + allowCheckbox, + onSelectPatient, }: PatientTableProps) { const { toast } = useToast(); const { user } = useAuth(); @@ -94,12 +98,32 @@ export function PatientTable({ ); 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 (!isSelected && onSelectPatient) { + onSelectPatient(patient); + } + }; + const { data: patientsData, isLoading, isError, } = useQuery({ - queryKey: ["patients", { page: currentPage, search: debouncedSearchCriteria?.searchTerm || "recent" }], + queryKey: [ + "patients", + { + page: currentPage, + search: debouncedSearchCriteria?.searchTerm || "recent", + }, + ], queryFn: async () => { const trimmedTerm = debouncedSearchCriteria?.searchTerm?.trim(); const isSearch = trimmedTerm && trimmedTerm.length > 0; @@ -149,8 +173,7 @@ export function PatientTable({ patients: [], totalCount: 0, }, - } -); + }); // Update patient mutation const updatePatientMutation = useMutation({ @@ -166,7 +189,15 @@ export function PatientTable({ }, onSuccess: () => { setIsAddPatientOpen(false); - queryClient.invalidateQueries({ queryKey: ["patients", currentPage] }); + queryClient.invalidateQueries({ + queryKey: [ + "patients", + { + page: currentPage, + search: debouncedSearchCriteria?.searchTerm || "recent", + }, + ], + }); toast({ title: "Success", description: "Patient updated successfully!", @@ -189,7 +220,15 @@ export function PatientTable({ }, onSuccess: () => { setIsDeletePatientOpen(false); - queryClient.invalidateQueries({ queryKey: ["patients", currentPage] }); + queryClient.invalidateQueries({ + queryKey: [ + "patients", + { + page: currentPage, + search: debouncedSearchCriteria?.searchTerm || "recent", + }, + ], + }); toast({ title: "Success", description: "Patient deleted successfully!", @@ -305,6 +344,7 @@ export function PatientTable({ + {allowCheckbox && Select} Patient DOB / Gender Contact @@ -344,6 +384,15 @@ export function PatientTable({ ) : ( patientsData?.patients.map((patient) => ( + {allowCheckbox && ( + + handleSelectPatient(patient)} + /> + + )} +
{patient.status === "active" ? "Active" : "Inactive"} @@ -498,7 +547,7 @@ export function PatientTable({ className={`${ currentPatient.status === "active" ? "text-green-600" - : "text-amber-600" + : "text-red-600" } font-medium`} > {currentPatient.status.charAt(0).toUpperCase() + diff --git a/apps/Frontend/src/components/ui/calendar.tsx b/apps/Frontend/src/components/ui/calendar.tsx index 7f955ef..a4644a6 100644 --- a/apps/Frontend/src/components/ui/calendar.tsx +++ b/apps/Frontend/src/components/ui/calendar.tsx @@ -3,21 +3,24 @@ import { DayPicker } from "react-day-picker"; import type { DateRange } from "react-day-picker"; import "react-day-picker/style.css"; -type BaseProps = Omit, 'mode' | 'selected' | 'onSelect'>; +type BaseProps = Omit< + React.ComponentProps, + "mode" | "selected" | "onSelect" +>; type CalendarProps = | (BaseProps & { - mode: 'single'; + mode: "single"; selected?: Date; onSelect?: (date: Date | undefined) => void; }) | (BaseProps & { - mode: 'range'; + mode: "range"; selected?: DateRange; onSelect?: (range: DateRange | undefined) => void; }) | (BaseProps & { - mode: 'multiple'; + mode: "multiple"; selected?: Date[]; onSelect?: (dates: Date[] | undefined) => void; }); @@ -25,7 +28,8 @@ type CalendarProps = export function Calendar(props: CalendarProps) { const { mode, selected, onSelect, className, ...rest } = props; - const [internalSelected, setInternalSelected] = useState(selected); + const [internalSelected, setInternalSelected] = + useState(selected); useEffect(() => { setInternalSelected(selected); @@ -38,23 +42,24 @@ export function Calendar(props: CalendarProps) { return (
- {mode === 'single' && ( + {mode === "single" && ( void} + captionLayout="dropdown" // ✅ Enables month/year dropdown {...rest} /> )} - {mode === 'range' && ( + {mode === "range" && ( )} - {mode === 'multiple' && ( + {mode === "multiple" && ( @@ -40,53 +28,59 @@ type Patient = z.infer; export default function InsuranceEligibilityPage() { const { user } = useAuth(); + const [selectedPatient, setSelectedPatient] = useState(null); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - const [searchTerm, setSearchTerm] = useState(""); - const [searchField, setSearchField] = useState("all"); - const [currentPage, setCurrentPage] = useState(1); - const itemsPerPage = 5; - + const toggleMobileMenu = () => { + setIsMobileMenuOpen(!isMobileMenuOpen); + }; + // Insurance eligibility check form fields const [memberId, setMemberId] = useState(""); const [dateOfBirth, setDateOfBirth] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); - + // Selected patient for insurance check - const [selectedPatientId, setSelectedPatientId] = useState(null); - + const [selectedPatientId, setSelectedPatientId] = useState( + null + ); + // Insurance automation states - const [isCredentialsModalOpen, setIsCredentialsModalOpen] = useState(false); const [selectedProvider, setSelectedProvider] = useState(""); const { toast } = useToast(); // Insurance eligibility check mutation const checkInsuranceMutation = useMutation({ - mutationFn: async ({ provider, patientId, credentials }: { + mutationFn: async ({ + provider, + patientId, + credentials, + }: { provider: string; patientId: number; credentials: { username: string; password: string }; }) => { - const response = await fetch('/api/insurance/check', { - method: 'POST', + const response = await fetch("/api/insurance/check", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ provider, patientId, credentials }), }); - + if (!response.ok) { - throw new Error('Failed to check insurance'); + throw new Error("Failed to check insurance"); } - + return response.json(); }, onSuccess: (result) => { toast({ title: "Insurance Check Complete", - description: result.isEligible ? - `Patient is eligible. Plan: ${result.planName}` : - "Patient eligibility could not be verified", + description: result.isEligible + ? `Patient is eligible. Plan: ${result.planName}` + : "Patient eligibility could not be verified", }); }, onError: (error: any) => { @@ -98,150 +92,41 @@ export default function InsuranceEligibilityPage() { }, }); - // Fetch patients - const { - data: patients = [], - isLoading: isLoadingPatients, - } = useQuery({ - queryKey: ["/api/patients"], - enabled: !!user, - }); - - // Filter patients based on search - const filteredPatients = patients.filter(patient => { - if (!searchTerm) return true; - - const searchLower = searchTerm.toLowerCase(); - const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase(); - const patientId = `PID-${patient.id.toString().padStart(4, '0')}`; - - switch (searchField) { - case "name": - return fullName.includes(searchLower); - case "id": - return patientId.toLowerCase().includes(searchLower); - case "phone": - return patient.phone?.toLowerCase().includes(searchLower) || false; - case "all": - default: - return ( - fullName.includes(searchLower) || - patientId.toLowerCase().includes(searchLower) || - patient.phone?.toLowerCase().includes(searchLower) || - patient.email?.toLowerCase().includes(searchLower) || - false - ); - } - }); - - // Pagination - const totalPages = Math.ceil(filteredPatients.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const currentPatients = filteredPatients.slice(startIndex, endIndex); - - const toggleMobileMenu = () => { - setIsMobileMenuOpen(!isMobileMenuOpen); - }; - - const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' - }); - }; - - const getPatientInitials = (firstName: string, lastName: string) => { - return `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase(); - }; - - // Toggle patient selection - const togglePatientSelection = (patientId: number) => { - setSelectedPatientId(selectedPatientId === patientId ? null : patientId); - }; - // Handle insurance provider button clicks const handleProviderClick = (providerName: string) => { if (!selectedPatientId) { toast({ title: "No Patient Selected", - description: "Please select a patient first by checking the box next to their name.", + description: + "Please select a patient first by checking the box next to their name.", variant: "destructive", }); return; } - + setSelectedProvider(providerName); - setIsCredentialsModalOpen(true); - }; - - // Handle credentials submission and start automation - const handleCredentialsSubmit = (credentials: { username: string; password: string }) => { - if (selectedPatientId && selectedProvider) { - // Use the provided MH credentials for MH provider - const finalCredentials = selectedProvider.toLowerCase() === 'mh' ? { - username: 'kqkgaox@yahoo.com', - password: 'Lex123456' - } : credentials; - - checkInsuranceMutation.mutate({ - provider: selectedProvider, - patientId: selectedPatientId, - credentials: finalCredentials, - }); - } - setIsCredentialsModalOpen(false); - }; - - const handleEligibilityCheck = () => { - // TODO: Implement insurance eligibility check - console.log("Checking MH eligibility:", { memberId, dateOfBirth, firstName, lastName }); - }; - - const handleDeltaMACheck = () => { - // TODO: Implement Delta MA eligibility check - console.log("Checking Delta MA eligibility:", { memberId, dateOfBirth, firstName, lastName }); - }; - - const handleMetlifeDentalCheck = () => { - // TODO: Implement Metlife Dental eligibility check - console.log("Checking Metlife Dental eligibility:", { memberId, dateOfBirth, firstName, lastName }); - }; - - const handlePatientSelect = (patientId: number) => { - const patient = patients.find(p => p.id === patientId); - if (patient) { - setSelectedPatientId(patientId); - // Auto-fill form fields with selected patient data - setMemberId(patient.insuranceId || ""); - setDateOfBirth(patient.dateOfBirth || ""); - setFirstName(patient.firstName || ""); - setLastName(patient.lastName || ""); - } else { - setSelectedPatientId(null); - // Clear form fields - setMemberId(""); - setDateOfBirth(""); - setFirstName(""); - setLastName(""); - } }; return (
- - + +
- +
{/* Header */}
-

Insurance Eligibility

-

Check insurance eligibility and view patient information

+

+ Insurance Eligibility +

+

+ Check insurance eligibility and view patient information +

{/* Insurance Eligibility Check Form */} @@ -288,236 +173,43 @@ export default function InsuranceEligibilityPage() { />
-
-
-
- -
-
+
+
- {/* Search and Filters */} - - -
-
- - setSearchTerm(e.target.value)} - className="pl-10" - /> -
-
- - -
-
-
-
- - {/* Patient List */} + {/* Patients Table */} - - {isLoadingPatients ? ( -
Loading patients...
- ) : ( - <> - {/* Table Header */} -
-
Select
-
Patient
-
DOB / Gender
-
Contact
-
Insurance
-
Status
-
Actions
-
- - {/* Table Rows */} - {currentPatients.length === 0 ? ( -
- {searchTerm ? "No patients found matching your search." : "No patients available."} -
- ) : ( - currentPatients.map((patient) => ( -
- {/* Select Checkbox */} -
- { - if (checked) { - handlePatientSelect(patient.id); - } else { - handlePatientSelect(0); // This will clear the selection - } - }} - /> -
- - {/* Patient Info */} -
-
- {getPatientInitials(patient.firstName, patient.lastName)} -
-
-
- {patient.firstName} {patient.lastName} -
-
- PID-{patient.id.toString().padStart(4, '0')} -
-
-
- - {/* DOB / Gender */} -
-
- {formatDate(patient.dateOfBirth)} -
-
- {patient.gender} -
-
- - {/* Contact */} -
-
- {patient.phone || 'Not provided'} -
-
- {patient.email || 'No email'} -
-
- - {/* Insurance */} -
-
- {patient.insuranceProvider ? - `${patient.insuranceProvider.charAt(0).toUpperCase()}${patient.insuranceProvider.slice(1)}` : - 'Not specified' - } -
-
- ID: {patient.insuranceId || 'N/A'} -
-
- - {/* Status */} -
- - {patient.status === 'active' ? 'Active' : 'Inactive'} - -
- - {/* Actions */} -
-
- - -
-
-
- )) - )} - - {/* Pagination */} - {totalPages > 1 && ( -
-
- Showing {startIndex + 1} to {Math.min(endIndex, filteredPatients.length)} of {filteredPatients.length} results -
-
- - - {/* Page Numbers */} - {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => ( - - ))} - - -
-
- )} - - )} + + Patient Records + + Select Patients and Check Their Eligibility + + + +
- - {/* Credentials Modal */} - setIsCredentialsModalOpen(false)} - onSubmit={handleCredentialsSubmit} - providerName={selectedProvider} - isLoading={checkInsuranceMutation.isPending} - />
); -} \ No newline at end of file +} diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 0993647..218e833 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -34,9 +34,6 @@ 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?