initial commit

This commit is contained in:
2026-04-04 22:13:55 -04:00
commit 5d77e207c9
10181 changed files with 522212 additions and 0 deletions

View File

@@ -0,0 +1,184 @@
import {
useState,
forwardRef,
useImperativeHandle,
useEffect,
useRef,
} from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogContent,
DialogFooter,
} from "@/components/ui/dialog";
import { PatientForm, PatientFormRef } from "./patient-form";
import { X, Calendar } from "lucide-react";
import { useLocation } from "wouter";
import { InsertPatient, Patient, UpdatePatient } from "@repo/db/types";
interface AddPatientModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: InsertPatient | (UpdatePatient & { id?: number })) => void;
isLoading: boolean;
patient?: Patient;
extractedInfo?: {
firstName: string;
lastName: string;
dateOfBirth: string;
insuranceId: string;
};
}
// Define the ref type
export type AddPatientModalRef = {
shouldSchedule: boolean;
shouldClaim: boolean;
navigateToSchedule: (patientId: number) => void;
navigateToClaim: (patientId: number) => void;
};
export const AddPatientModal = forwardRef<
AddPatientModalRef,
AddPatientModalProps
>(function AddPatientModal(props, ref) {
const { open, onOpenChange, onSubmit, isLoading, patient, extractedInfo } =
props;
const [formData, setFormData] = useState<
InsertPatient | UpdatePatient | null
>(null);
const isEditing = !!patient;
const [, navigate] = useLocation();
const [saveAndSchedule, setSaveAndSchedule] = useState(false);
const [saveAndClaim, setSaveAndClaim] = useState(false);
const patientFormRef = useRef<PatientFormRef>(null); // Ref for PatientForm
// Set up the imperativeHandle to expose functionality to the parent component
useEffect(() => {
if (isEditing && patient) {
const { id, userId, createdAt, ...sanitized } = patient;
setFormData(sanitized); // Update the form data with the patient data for editing
} else {
setFormData(null); // Reset form data when not editing
}
}, [isEditing, patient]);
useImperativeHandle(ref, () => ({
shouldSchedule: saveAndSchedule,
shouldClaim: saveAndClaim, // ✅ NEW
navigateToSchedule: (patientId: number) => {
navigate(`/appointments?newPatient=${patientId}`);
},
navigateToClaim: (patientId: number) => {
// ✅ NEW
navigate(`/claims?newPatient=${patientId}`);
},
}));
const handleFormSubmit = (data: InsertPatient | UpdatePatient) => {
if (patient && patient.id) {
onSubmit({ ...data, id: patient.id });
} else {
onSubmit(data);
}
};
const handleSaveAndSchedule = () => {
setSaveAndClaim(false); // ensure only one flag at a time
setSaveAndSchedule(true);
patientFormRef.current?.submit();
};
const handleSaveAndClaim = () => {
setSaveAndSchedule(false); // ensure only one flag at a time
setSaveAndClaim(true);
patientFormRef.current?.submit();
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[800px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle>
{isEditing ? "Edit Patient" : "Add New Patient"}
</DialogTitle>
<Button
variant="ghost"
size="icon"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
<DialogDescription>
{isEditing
? "Update patient information in the form below."
: "Fill out the patient information to add them to your records."}
</DialogDescription>
</DialogHeader>
<PatientForm
ref={patientFormRef}
patient={patient}
extractedInfo={extractedInfo}
onSubmit={handleFormSubmit}
/>
<DialogFooter className="mt-6">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
{!isEditing && (
<Button
variant="outline"
className="gap-1"
onClick={handleSaveAndClaim}
disabled={isLoading}
>
<Calendar className="h-4 w-4" />
Save & Claim/PreAuth
</Button>
)}
{!isEditing && (
<Button
variant="outline"
className="gap-1"
onClick={() => {
handleSaveAndSchedule();
}}
disabled={isLoading}
>
<Calendar className="h-4 w-4" />
Save & Schedule
</Button>
)}
<Button
type="button"
form="patient-form"
onClick={() => {
if (patientFormRef.current) {
patientFormRef.current.submit();
}
}}
disabled={isLoading}
>
{isLoading
? patient
? "Updating..."
: "Saving..."
: patient
? "Update Patient"
: "Save Patient"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
});

View File

@@ -0,0 +1,366 @@
import React, { useEffect, useMemo, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { apiRequest } from "@/lib/queryClient";
import LoadingScreen from "../ui/LoadingScreen";
import { useToast } from "@/hooks/use-toast";
import { useLocation } from "wouter";
import { FinancialRow } from "@repo/db/types";
import { getPageNumbers } from "@/utils/pageNumberGenerator";
export function PatientFinancialsModal({
patientId,
open,
onOpenChange,
}: {
patientId: number | null;
open: boolean;
onOpenChange: (v: boolean) => void;
}) {
const [rows, setRows] = useState<FinancialRow[]>([]);
const [loading, setLoading] = useState(false);
const [limit, setLimit] = useState<number>(50);
const [offset, setOffset] = useState<number>(0);
const [totalCount, setTotalCount] = useState<number>(0);
const [, navigate] = useLocation();
const { toast } = useToast();
// patient summary to show in header
const [patientName, setPatientName] = useState<string | null>(null);
const [patientPID, setPatientPID] = useState<number | null>(null);
useEffect(() => {
if (!open || !patientId) return;
fetchPatient();
fetchRows();
}, [open, patientId, limit, offset]);
async function fetchPatient() {
try {
const res = await apiRequest("GET", `/api/patients/${patientId}`);
if (!res.ok) {
return;
}
const patient = await res.json();
setPatientName(`${patient.firstName} ${patient.lastName}`);
setPatientPID(patient.id);
} catch (err) {
console.error("Failed to fetch patient", err);
}
}
async function fetchRows() {
if (!patientId) return;
setLoading(true);
try {
const url = `/api/patients/${patientId}/financials?limit=${limit}&offset=${offset}`;
const res = await apiRequest("GET", url);
if (!res.ok) {
const err = await res.json();
throw new Error(err.message || "Failed to load");
}
const data = await res.json();
setRows(data.rows || []);
setTotalCount(Number(data.totalCount || 0));
} catch (err: any) {
console.error(err);
toast?.({
title: "Error",
description: err.message || "Failed to load financials",
variant: "destructive",
});
} finally {
setLoading(false);
}
}
function gotoRow(r: FinancialRow) {
const openInNewTab = (url: string) => {
if (typeof window !== "undefined") {
window.open(url, "_blank", "noopener,noreferrer");
} else {
// fallback for non-browser env (shouldn't happen in the client)
navigate(url);
}
};
const makePaymentUrl = (id: number) => `/payments?paymentId=${id}`;
if (r.linked_payment_id) {
openInNewTab(makePaymentUrl(r.linked_payment_id));
return;
}
if (r.type === "PAYMENT") {
openInNewTab(makePaymentUrl(r.id));
return;
}
}
const currentPage = Math.floor(offset / limit) + 1;
const totalPages = Math.max(1, Math.ceil(totalCount / limit));
function setPage(page: number) {
if (page < 1) page = 1;
if (page > totalPages) page = totalPages;
setOffset((page - 1) * limit);
}
const startItem = useMemo(
() => Math.min(offset + 1, totalCount || 0),
[offset, totalCount]
);
const endItem = useMemo(
() => Math.min(offset + limit, totalCount || 0),
[offset, limit, totalCount]
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl w-[95%] p-0 overflow-hidden">
<div className="border-b px-6 py-4">
<div className="flex items-start justify-between gap-4">
<div>
<DialogTitle className="text-lg">Financials</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
{patientName ? (
<>
<span className="font-medium">{patientName}</span>{" "}
{patientPID && (
<span className="text-muted-foreground">
PID-{String(patientPID).padStart(4, "0")}
</span>
)}
</>
) : (
"Claims, payments and balances for this patient."
)}
</DialogDescription>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
>
Close
</Button>
</div>
</div>
</div>
<div className="px-6 py-4">
<div className="border rounded-md overflow-hidden">
<div className="max-h-[56vh] overflow-auto">
<Table className="min-w-full">
<TableHeader className="sticky top-0 bg-white z-10">
<TableRow>
<TableHead className="w-24">Type</TableHead>
<TableHead className="w-36">Date</TableHead>
<TableHead>Procedures Codes</TableHead>
<TableHead className="w-28">Tooth Number</TableHead>
<TableHead className="text-right w-28">Billed</TableHead>
<TableHead className="text-right w-28">Paid</TableHead>
<TableHead className="text-right w-28">Adjusted</TableHead>
<TableHead className="text-right w-28">Total Due</TableHead>
<TableHead className="w-28">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={9} className="text-center py-12">
<LoadingScreen />
</TableCell>
</TableRow>
) : rows.length === 0 ? (
<TableRow>
<TableCell
colSpan={9}
className="text-center py-8 text-muted-foreground"
>
No records found.
</TableCell>
</TableRow>
) : (
rows.map((r) => {
const billed = Number(r.total_billed ?? 0);
const paid = Number(r.total_paid ?? 0);
const adjusted = Number(r.total_adjusted ?? 0);
const totalDue = Number(r.total_due ?? 0);
const serviceLines = r.service_lines || [];
const procedureCodes =
serviceLines.length > 0
? serviceLines
.map((sl: any) => sl.procedureCode)
.filter(Boolean)
.join(", ")
: r.linked_payment_id
? "No Codes Given"
: "-";
const toothNumbers =
serviceLines.length > 0
? serviceLines
.map((sl: any) =>
sl.toothNumber ? String(sl.toothNumber) : "-"
)
.join(", ")
: "-";
return (
<TableRow
key={`${r.type}-${r.id}`}
className="cursor-pointer hover:bg-gray-50"
onClick={() => gotoRow(r)}
>
<TableCell className="font-medium">
{r.type}
</TableCell>
<TableCell>
{r.date
? new Date(r.date).toLocaleDateString()
: "-"}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{procedureCodes}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{toothNumbers}
</TableCell>
<TableCell className="text-right">
{billed.toFixed(2)}
</TableCell>
<TableCell className="text-right">
{paid.toFixed(2)}
</TableCell>
<TableCell className="text-right">
{adjusted.toFixed(2)}
</TableCell>
<TableCell
className={`text-right ${totalDue > 0 ? "text-red-600" : "text-green-600"}`}
>
{totalDue.toFixed(2)}
</TableCell>
<TableCell>{r.status ?? "-"}</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>
</div>
</div>
</div>
<div className="border-t px-6 py-3 bg-white">
<div className="flex flex-col md:flex-row items-center md:items-center justify-between gap-3">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<label className="text-sm text-muted-foreground">Rows:</label>
<select
value={limit}
onChange={(e) => {
setLimit(Number(e.target.value));
setOffset(0);
}}
className="border rounded px-2 py-1 text-sm"
>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
<div className="text-sm text-muted-foreground">
Showing <span className="font-medium">{startItem}</span>
<span className="font-medium">{endItem}</span> of{" "}
<span className="font-medium">{totalCount}</span>
</div>
</div>
<div className="flex items-center gap-4">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage > 1) setPage(currentPage - 1);
}}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
{getPageNumbers(currentPage, totalPages).map((page, idx) => (
<PaginationItem key={idx}>
{page === "..." ? (
<span className="px-2 text-gray-500"></span>
) : (
<PaginationLink
href="#"
onClick={(e) => {
e.preventDefault();
setPage(Number(page));
}}
isActive={currentPage === page}
>
{page}
</PaginationLink>
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href="#"
onClick={(e) => {
e.preventDefault();
if (currentPage < totalPages) setPage(currentPage + 1);
}}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: ""
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,420 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAuth } from "@/hooks/use-auth";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useEffect, useMemo } from "react";
import { forwardRef, useImperativeHandle } from "react";
import { formatLocalDate } from "@/utils/dateUtils";
import {
InsertPatient,
insertPatientSchema,
Patient,
PatientStatus,
patientStatusOptions,
UpdatePatient,
updatePatientSchema,
} from "@repo/db/types";
import { z } from "zod";
import { DateInputField } from "@/components/ui/dateInputField";
interface PatientFormProps {
patient?: Patient;
extractedInfo?: {
firstName: string;
lastName: string;
dateOfBirth: string;
insuranceId: string;
};
onSubmit: (data: InsertPatient | UpdatePatient) => void;
}
export type PatientFormRef = {
submit: () => void;
};
export const PatientForm = forwardRef<PatientFormRef, PatientFormProps>(
({ patient, extractedInfo, onSubmit }, ref) => {
const { user } = useAuth();
const isEditing = !!patient;
const schema = useMemo(
() =>
isEditing
? updatePatientSchema
: insertPatientSchema.extend({ userId: z.number().optional() }),
[isEditing],
);
const computedDefaultValues = useMemo(() => {
if (isEditing && patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
return {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
? formatLocalDate(new Date(patient.dateOfBirth))
: "",
};
}
return {
firstName: extractedInfo?.firstName || "",
lastName: extractedInfo?.lastName || "",
dateOfBirth: extractedInfo?.dateOfBirth || "",
gender: "",
phone: "",
email: "",
address: "",
city: "",
zipCode: "",
insuranceProvider: "",
insuranceId: extractedInfo?.insuranceId || "",
groupNumber: "",
policyHolder: "",
allergies: "",
medicalConditions: "",
status: "UNKNOWN",
userId: user?.id,
};
}, [isEditing, patient, extractedInfo, user?.id]);
const form = useForm<InsertPatient | UpdatePatient>({
resolver: zodResolver(schema),
defaultValues: computedDefaultValues,
});
useImperativeHandle(ref, () => ({
submit() {
(
document.getElementById("patient-form") as HTMLFormElement | null
)?.requestSubmit();
},
}));
// Debug form errors
useEffect(() => {
const errors = form.formState.errors;
if (Object.keys(errors).length > 0) {
console.log("❌ Form validation errors:", errors);
}
}, [form.formState.errors]);
useEffect(() => {
if (patient) {
const { id, userId, createdAt, ...sanitizedPatient } = patient;
const resetValues: Partial<Patient> = {
...sanitizedPatient,
dateOfBirth: patient.dateOfBirth
? formatLocalDate(new Date(patient.dateOfBirth))
: "",
};
form.reset(resetValues);
}
}, [patient, computedDefaultValues, form]);
const handleSubmit2 = (data: InsertPatient | UpdatePatient) => {
onSubmit(data);
};
return (
<Form {...form}>
<form
id="patient-form"
key={patient?.id || "new"}
onSubmit={form.handleSubmit((data) => {
handleSubmit2(data);
})}
className="space-y-6"
>
{/* Personal Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">
Personal Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DateInputField
control={form.control}
name="dateOfBirth"
label="Date of Birth *"
disableFuture
/>
<FormField
control={form.control}
name="gender"
render={({ field }) => (
<FormItem>
<FormLabel>Gender *</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value as string}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select gender" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Contact Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">
Contact Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number *</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="address"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormLabel>Address</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>City</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="zipCode"
render={({ field }) => (
<FormItem>
<FormLabel>ZIP Code</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Insurance Information */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-3">
Insurance Information
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="status"
render={({ field }) => {
const options = Object.values(
patientStatusOptions,
) as PatientStatus[]; // ['ACTIVE','INACTIVE','UNKNOWN']
const toLabel = (v: PatientStatus) =>
v[0] + v.slice(1).toLowerCase(); // ACTIVE -> Active
return (
<FormItem>
<FormLabel>Status *</FormLabel>
<Select
value={(field.value as PatientStatus) ?? "UNKNOWN"}
onValueChange={(v) =>
field.onChange(v as PatientStatus)
}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
</FormControl>
<SelectContent>
{options.map((v) => (
<SelectItem key={v} value={v}>
{toLabel(v)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="insuranceProvider"
render={({ field }) => (
<FormItem>
<FormLabel>Insurance Provider</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={(field.value as string) || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="placeholder">
Select provider
</SelectItem>
<SelectItem value="Mass Health">Mass Health</SelectItem>
<SelectItem value="Delta MA">Delta MA</SelectItem>
<SelectItem value="Metlife">MetLife</SelectItem>
<SelectItem value="Cigna">Cigna</SelectItem>
<SelectItem value="Aetna">Aetna</SelectItem>
<SelectItem value="Other">Other</SelectItem>
<SelectItem value="none">None</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="insuranceId"
render={({ field }) => (
<FormItem>
<FormLabel>Insurance ID</FormLabel>
<FormControl>
<Input {...field} value={String(field.value) || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="groupNumber"
render={({ field }) => (
<FormItem>
<FormLabel>Group Number</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="policyHolder"
render={({ field }) => (
<FormItem>
<FormLabel>Policy Holder (if not self)</FormLabel>
<FormControl>
<Input {...field} value={field.value || ""} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
{/* Hidden submit button for form validation */}
<button type="submit" className="hidden" aria-hidden="true"></button>
</form>
</Form>
);
},
);

View File

@@ -0,0 +1,300 @@
import { useState } from "react";
import { CalendarIcon, Search, X } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogClose,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
export type SearchCriteria = {
searchTerm: string;
searchBy: "name" | "insuranceId" | "phone" | "gender" | "dob" | "all";
};
interface PatientSearchProps {
onSearch: (criteria: SearchCriteria) => void;
onClearSearch: () => void;
isSearchActive: boolean;
}
export function PatientSearch({
onSearch,
onClearSearch,
isSearchActive,
}: PatientSearchProps) {
const [dobOpen, setDobOpen] = useState(false);
const [advanceDobOpen, setAdvanceDobOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [searchBy, setSearchBy] = useState<SearchCriteria["searchBy"]>("name");
const [showAdvanced, setShowAdvanced] = useState(false);
const [advancedCriteria, setAdvancedCriteria] = useState<SearchCriteria>({
searchTerm: "",
searchBy: "name",
});
const handleSearch = () => {
onSearch({
searchTerm,
searchBy,
});
};
const handleClear = () => {
setSearchTerm("");
setSearchBy("all");
onClearSearch();
};
const handleAdvancedSearch = () => {
onSearch(advancedCriteria);
setShowAdvanced(false);
};
const updateAdvancedCriteria = (
field: keyof SearchCriteria,
value: string
) => {
setAdvancedCriteria({
...advancedCriteria,
[field]: value,
});
};
return (
<div className="w-full pt-8 pb-4 px-4">
<div className="flex gap-2 mb-4">
<div className="relative flex-1">
{searchBy === "dob" ? (
<Popover open={dobOpen} onOpenChange={setDobOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
onKeyDown={(e) => {
if (e.key === "Enter") handleSearch();
}}
className={cn(
"w-full pl-3 pr-20 text-left font-normal",
!searchTerm && "text-muted-foreground"
)}
>
{searchTerm ? (
format(new Date(searchTerm), "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4">
<Calendar
mode="single"
selected={searchTerm ? new Date(searchTerm) : undefined}
onSelect={(date: Date | undefined) => {
if (date) {
const formattedDate = format(date, "yyyy-MM-dd");
setSearchTerm(String(formattedDate));
setDobOpen(false);
}
}}
disabled={(date) => date > new Date()}
/>
</PopoverContent>
</Popover>
) : (
<Input
placeholder="Search patients..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pr-10"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleSearch();
}
}}
/>
)}
{searchTerm && (
<button
className="absolute right-10 top-3 text-gray-400 hover:text-gray-600"
onClick={() => {
setSearchTerm("");
if (isSearchActive) onClearSearch();
}}
>
<X size={16} />
</button>
)}
<button
className="absolute right-3 top-3 text-gray-400 hover:text-gray-600"
onClick={handleSearch}
>
<Search size={16} />
</button>
</div>
<Select
value={searchBy}
onValueChange={(value) => {
setSearchBy(value as SearchCriteria["searchBy"]);
setSearchTerm("");
}}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Search by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceId">InsuranceId</SelectItem>
<SelectItem value="gender">Gender</SelectItem>
<SelectItem value="dob">DOB</SelectItem>
</SelectContent>
</Select>
<Dialog open={showAdvanced} onOpenChange={setShowAdvanced}>
<DialogTrigger asChild>
<Button variant="outline">Advanced</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[550px]">
<DialogHeader>
<DialogTitle>Advanced Search</DialogTitle>
<DialogDescription>
Search for patients using multiple criteria
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<label className="text-right text-sm font-medium">
Search by
</label>
<Select
value={advancedCriteria.searchBy}
onValueChange={(value) => {
setAdvancedCriteria((prev) => ({
...prev,
searchBy: value as SearchCriteria["searchBy"],
searchTerm: "",
}));
}}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Name" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Fields</SelectItem>
<SelectItem value="name">Name</SelectItem>
<SelectItem value="phone">Phone</SelectItem>
<SelectItem value="insuranceId">InsuranceId</SelectItem>
<SelectItem value="gender">Gender</SelectItem>
<SelectItem value="dob">DOB</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<label className="text-right text-sm font-medium">
Search term
</label>
{advancedCriteria.searchBy === "dob" ? (
<Popover
open={advanceDobOpen}
onOpenChange={setAdvanceDobOpen}
>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
onKeyDown={(e) => {
if (e.key === "Enter") handleSearch();
}}
className={cn(
"col-span-3 text-left font-normal",
!advancedCriteria.searchTerm &&
"text-muted-foreground"
)}
>
{advancedCriteria.searchTerm ? (
format(new Date(advancedCriteria.searchTerm), "PPP")
) : (
<span>Pick a date</span>
)}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-4">
<Calendar
mode="single"
selected={
advancedCriteria.searchTerm
? new Date(advancedCriteria.searchTerm)
: undefined
}
onSelect={(date) => {
if (date) {
const formattedDate = format(date, "yyyy-MM-dd");
updateAdvancedCriteria(
"searchTerm",
String(formattedDate)
);
setAdvanceDobOpen(false);
}
}}
disabled={(date) => date > new Date()}
/>
</PopoverContent>
</Popover>
) : (
<Input
className="col-span-3"
value={advancedCriteria.searchTerm}
onChange={(e) =>
updateAdvancedCriteria("searchTerm", e.target.value)
}
placeholder="Enter search term..."
/>
)}
</div>
</div>
<div className="flex justify-end gap-2">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleAdvancedSearch}>Search</Button>
</div>
</DialogContent>
</Dialog>
{isSearchActive && (
<Button variant="outline" onClick={handleClear}>
Clear
</Button>
)}
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff