import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { X, Calendar as CalendarIcon, HelpCircle } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { PatientUncheckedCreateInputObjectSchema, AppointmentUncheckedCreateInputObjectSchema, ClaimUncheckedCreateInputObjectSchema, ClaimStatusSchema, StaffUncheckedCreateInputObjectSchema, } from "@repo/db/usedSchemas"; import { z } from "zod"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { MultipleFileUploadZone } from "../file-upload/multiple-file-upload-zone"; import { useAuth } from "@/hooks/use-auth"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import procedureCodes from "../../assets/data/procedureCodes.json"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; const PatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ appointments: true, }); type Patient = z.infer; const updatePatientSchema = ( PatientUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) .omit({ id: true, createdAt: true, userId: true, }) .partial(); type UpdatePatient = z.infer; //creating types out of schema auto generated. const insertAppointmentSchema = ( AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject ).omit({ id: true, createdAt: true, }); type InsertAppointment = z.infer; const updateAppointmentSchema = ( AppointmentUncheckedCreateInputObjectSchema as unknown as z.ZodObject ) .omit({ id: true, createdAt: true, userId: true, }) .partial(); type UpdateAppointment = z.infer; type Claim = z.infer; interface ServiceLine { procedureCode: string; procedureDate: string; // YYYY-MM-DD oralCavityArea?: string; toothNumber?: string; toothSurface?: string; billedAmount: number; } interface ClaimFormData { patientId: number; appointmentId: number; userId: number; staffId: number; patientName: string; memberId: string; dateOfBirth: string; remarks: string; serviceDate: string; // YYYY-MM-DD insuranceProvider: string; insuranceSiteKey?: string; status: string; // default "pending" serviceLines: ServiceLine[]; claimId?: number; } interface ClaimFormProps { patientId: number; onSubmit: (data: ClaimFormData) => Promise; onHandleAppointmentSubmit: ( appointmentData: InsertAppointment | UpdateAppointment ) => void; onHandleUpdatePatient: (patient: UpdatePatient & { id: number }) => void; onHandleForMHSelenium: (data: ClaimFormData) => void; onClose: () => void; } export type ClaimStatus = z.infer; type Staff = z.infer; export function ClaimForm({ patientId, onHandleAppointmentSubmit, onHandleUpdatePatient, onHandleForMHSelenium, onSubmit, onClose, }: ClaimFormProps) { const { toast } = useToast(); const { user } = useAuth(); const [patient, setPatient] = useState(null); // Query patient based on given patient id const { data: fetchedPatient, isLoading, error, } = useQuery({ queryKey: ["/api/patients/", patientId], queryFn: async () => { const res = await apiRequest("GET", `/api/patients/${patientId}`); if (!res.ok) throw new Error("Failed to fetch patient"); return res.json(); }, enabled: !!patientId, }); // Sync fetched patient when available useEffect(() => { if (fetchedPatient) { setPatient(fetchedPatient); } }, [fetchedPatient]); //Fetching staff memebers const [staff, setStaff] = useState(null); const { data: staffMembersRaw = [] as Staff[], isLoading: isLoadingStaff } = useQuery({ queryKey: ["/api/staffs/"], queryFn: async () => { const res = await apiRequest("GET", "/api/staffs/"); return res.json(); }, }); useEffect(() => { if (staffMembersRaw.length > 0 && !staff) { const kaiGao = staffMembersRaw.find( (member) => member.name === "Kai Gao" ); const defaultStaff = kaiGao || staffMembersRaw[0]; if (defaultStaff) setStaff(defaultStaff); } }, [staffMembersRaw, staff]); // Service date state const [serviceDateValue, setServiceDateValue] = useState(new Date()); const [serviceDate, setServiceDate] = useState( formatLocalDate(new Date()) ); // Update service date when calendar date changes const onServiceDateChange = (date: Date | undefined) => { if (date) { const formattedDate = formatLocalDate(date); setServiceDateValue(date); setServiceDate(formattedDate); setForm((prev) => ({ ...prev, serviceDate: formattedDate })); } }; // when service date is chenged, it will change the each service lines procedure date in sync as well. useEffect(() => { setForm((prevForm) => { const updatedLines = prevForm.serviceLines.map((line) => ({ ...line, procedureDate: serviceDate, // set all to current serviceDate string })); return { ...prevForm, serviceLines: updatedLines, serviceDate, // keep form.serviceDate in sync as well }; }); }, [serviceDate]); // Determine patient date of birth format - required as date extracted from pdfs has different format. const formatDOB = (dob: string | undefined) => { if (!dob) return ""; if (/^\d{2}\/\d{2}\/\d{4}$/.test(dob)) return dob; // already MM/DD/YYYY if (/^\d{4}-\d{2}-\d{2}/.test(dob)) { const datePart = dob?.split("T")[0]; // safe optional chaining if (!datePart) return ""; const [year, month, day] = datePart.split("-"); return `${month}/${day}/${year}`; } return dob; }; // MAIN FORM INITIAL STATE const [form, setForm] = useState({ patientId: patientId || 0, appointmentId: 0, userId: Number(user?.id), staffId: Number(staff?.id), patientName: `${patient?.firstName} ${patient?.lastName}`.trim(), memberId: patient?.insuranceId, dateOfBirth: formatDOB(patient?.dateOfBirth), remarks: "", serviceDate: serviceDate, insuranceProvider: "", insuranceSiteKey: "", status: "PENDING", serviceLines: Array.from({ length: 10 }, () => ({ procedureCode: "", procedureDate: serviceDate, oralCavityArea: "", toothNumber: "", toothSurface: "", billedAmount: 0, })), uploadedFiles: [], }); // Sync patient data to form when patient updates useEffect(() => { if (patient) { const fullName = `${patient.firstName || ""} ${patient.lastName || ""}`.trim(); setForm((prev) => ({ ...prev, patientName: fullName, dateOfBirth: formatDOB(patient.dateOfBirth), memberId: patient.insuranceId || "", patientId: patient.id, })); } }, [patient]); // Handle patient field changes (to make inputs controlled and editable) const updatePatientField = (field: keyof Patient, value: any) => { setPatient((prev) => (prev ? { ...prev, [field]: value } : null)); }; const updateServiceLine = ( index: number, field: keyof ServiceLine, value: any ) => { const updatedLines = [...form.serviceLines]; if (updatedLines[index]) { if (field === "billedAmount") { const num = typeof value === "string" ? parseFloat(value) : value; const rounded = Math.round((isNaN(num) ? 0 : num) * 100) / 100; updatedLines[index][field] = rounded; } else { updatedLines[index][field] = value; } } setForm({ ...form, serviceLines: updatedLines }); }; const updateProcedureDate = (index: number, date: Date | undefined) => { if (!date) return; const formattedDate = formatLocalDate(date); const updatedLines = [...form.serviceLines]; if (updatedLines[index]) { updatedLines[index].procedureDate = formattedDate; } setForm({ ...form, serviceLines: updatedLines }); }; // FILE UPLOAD ZONE const [isUploading, setIsUploading] = useState(false); const handleFileUpload = (files: File[]) => { setIsUploading(true); const allowedTypes = [ "application/pdf", "image/jpeg", "image/jpg", "image/png", "image/webp", ]; const validFiles = files.filter((file) => allowedTypes.includes(file.type)); if (validFiles.length > 10) { toast({ title: "Too Many Files", description: "You can only upload up to 10 files (PDFs or images).", variant: "destructive", }); setIsUploading(false); return; } if (validFiles.length === 0) { toast({ title: "Invalid File Type", description: "Only PDF and image files are allowed.", variant: "destructive", }); setIsUploading(false); return; } setForm((prev) => ({ ...prev, uploadedFiles: validFiles, })); toast({ title: "Files Selected", description: `${validFiles.length} file(s) ready for processing.`, }); setIsUploading(false); }; // 1st Button workflow - Mass Health Button Handler const handleMHSubmit = async () => { // 0. Validate required fields const missingFields: string[] = []; if (!form.memberId?.trim()) missingFields.push("Member ID"); if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth"); if (!patient?.firstName?.trim()) missingFields.push("First Name"); if (missingFields.length > 0) { toast({ title: "Missing Required Fields", description: `Please fill out the following field(s): ${missingFields.join(", ")}`, variant: "destructive", }); return; } // 1. Create or update appointment const appointmentData = { patientId: patientId, date: serviceDate, staffId: staff?.id, }; const appointmentId = await onHandleAppointmentSubmit(appointmentData); // 2. Update patient if (patient && typeof patient.id === "number") { const { id, createdAt, userId, ...sanitizedFields } = patient; const updatedPatientFields = { id, ...sanitizedFields, insuranceProvider: "MassHealth", }; onHandleUpdatePatient(updatedPatientFields); } else { toast({ title: "Error", description: "Cannot update patient: Missing or invalid patient data", variant: "destructive", }); } // 3. Create Claim(if not) // Filter out empty service lines (empty procedureCode) const filteredServiceLines = form.serviceLines.filter( (line) => line.procedureCode.trim() !== "" ); const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form; const createdClaim = await onSubmit({ ...formToCreateClaim, serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "MassHealth", appointmentId: appointmentId!, }); // 4. sending form data to selenium service onHandleForMHSelenium({ ...form, serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "Mass Health", appointmentId: appointmentId!, insuranceSiteKey: "MH", claimId: createdClaim.id, }); // 5. Close form onClose(); }; return (
Insurance Claim Form
{/* Patient Information */}
setForm({ ...form, memberId: e.target.value }) } />
{ updatePatientField("dateOfBirth", e.target.value); setForm((prev) => ({ ...prev, dateOfBirth: e.target.value, })); }} disabled={isLoading} />
{ updatePatientField("firstName", e.target.value); setForm((prev) => ({ ...prev, patientName: `${e.target.value} ${patient?.lastName || ""}`.trim(), })); }} disabled={isLoading} />
{ updatePatientField("lastName", e.target.value); setForm((prev) => ({ ...prev, patientName: `${patient?.firstName || ""} ${e.target.value}`.trim(), })); }} disabled={isLoading} />
{/* Clinical Notes Entry */}
setForm({ ...form, remarks: e.target.value })} />
{/* Service Lines */}

Service Lines

{/* Service Date */}
{/* Treating doctor */}
Procedure Code Abbreviation Procedure Date Oral Cavity Area Tooth Number Tooth Surface Billed Amount
{/* Dynamic Rows */} {form.serviceLines.map((line, i) => (
updateServiceLine(i, "procedureCode", e.target.value) } />
{line.procedureCode && line.procedureCode.trim() !== "" ? (() => { const normalizedCode = line.procedureCode .toUpperCase() .trim(); const procedureInfo = procedureCodes.find( (p) => p["Procedure Code"].toUpperCase().trim() === normalizedCode ); return procedureInfo ? procedureInfo.Description || "No description available" : "Enter a valid procedure code"; })() : "Enter a procedure code"}
{/* Date Picker */} updateProcedureDate(i, date)} /> updateServiceLine(i, "oralCavityArea", e.target.value) } /> updateServiceLine(i, "toothNumber", e.target.value) } /> updateServiceLine(i, "toothSurface", e.target.value) } /> { updateServiceLine(i, "billedAmount", e.target.value); }} onBlur={(e) => { const val = parseFloat(e.target.value); const rounded = Math.round(val * 100) / 100; updateServiceLine( i, "billedAmount", isNaN(rounded) ? 0 : rounded ); }} />
))}
{/* File Upload Section */}

You can upload up to 10 files. Allowed types: PDF, JPG, PNG, WEBP.

{form.uploadedFiles.length > 0 && (
    {form.uploadedFiles.map((file, index) => (
  • {file.name}
  • ))}
)}
{/* Insurance Carriers */}

Insurance Carriers

); }