import { useState, useEffect, useRef, useCallback } 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, Trash2 } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { Calendar } from "@/components/ui/calendar"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import { MultipleFileUploadZone, MultipleFileUploadZoneHandle, } from "../file-upload/multiple-file-upload-zone"; import { useAuth } from "@/hooks/use-auth"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils"; import { Claim, InputServiceLine, InsertAppointment, Patient, Staff, UpdateAppointment, UpdatePatient, } from "@repo/db/types"; import { Decimal } from "decimal.js"; import { mapPricesForForm, applyComboToForm, getDescriptionForCode, } from "@/utils/procedureCombosMapping"; import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; interface ClaimFileMeta { filename: string; mimeType: string; } 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: InputServiceLine[]; claimId?: number; claimFiles?: ClaimFileMeta[]; } 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 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()) ); const [serviceDateOpen, setServiceDateOpen] = useState(false); const [openProcedureDateIndex, setOpenProcedureDateIndex] = useState< number | null >(null); // 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 | Date | undefined) => { if (!dob) return ""; const normalized = formatLocalDate(parseLocalDate(dob)); // If it's already MM/DD/YYYY, leave it alone if (/^\d{2}\/\d{2}\/\d{4}$/.test(normalized)) return normalized; // If it's yyyy-MM-dd, swap order to MM/DD/YYYY if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) { const [year, month, day] = normalized.split("-"); return `${month}/${day}/${year}`; } return normalized; }; // 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: "", totalBilled: new Decimal(0), totalAdjusted: new Decimal(0), totalPaid: new Decimal(0), })), uploadedFiles: [], }); // Sync patient data to form when patient updates useEffect(() => { if (patient) { const fullName = `${patient.firstName || ""} ${patient.lastName || ""}`.trim(); setForm((prev) => ({ ...prev, patientId: Number(patient.id), patientName: fullName, dateOfBirth: formatDOB(patient.dateOfBirth), memberId: patient.insuranceId || "", })); } }, [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 InputServiceLine, value: any ) => { const updatedLines = [...form.serviceLines]; if (updatedLines[index]) { if (field === "totalBilled") { const num = typeof value === "string" ? parseFloat(value) : value; const rounded = Math.round((isNaN(num) ? 0 : num) * 100) / 100; updatedLines[index][field] = new Decimal(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 }); }; // for serviceLine rows, to auto scroll when it got updated by combo buttons and all. const rowRefs = useRef<(HTMLDivElement | null)[]>([]); const scrollToLine = (index: number) => { const el = rowRefs.current[index]; if (el) { el.scrollIntoView({ behavior: "smooth", block: "start" }); } }; // Map Price function const onMapPrice = () => { setForm((prev) => mapPricesForForm({ form: prev, patientDOB: patient?.dateOfBirth ?? "", }) ); }; // FILE UPLOAD ZONE const uploadZoneRef = useRef(null); const [isUploading, setIsUploading] = useState(false); // NO validation here — the upload zone handles validation, toasts, max files, sizes, etc. const handleFilesChange = useCallback((files: File[]) => { setForm((prev) => ({ ...prev, uploadedFiles: files })); }, []); // 1st Button workflow - Mass Health Button Handler const handleMHSubmit = async ( formToUse?: ClaimFormData & { uploadedFiles?: File[] } ) => { // Use the passed form, or fallback to current state const f = formToUse ?? form; // 0. Validate required fields const missingFields: string[] = []; if (!f.memberId?.trim()) missingFields.push("Member ID"); if (!f.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: f.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 = f.serviceLines.filter( (line) => line.procedureCode.trim() !== "" ); const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = f; // build claimFiles metadata from uploadedFiles (only filename + mimeType) const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({ filename: f.name, mimeType: f.type, })); const createdClaim = await onSubmit({ ...formToCreateClaim, serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "MassHealth", appointmentId: appointmentId!, claimFiles: claimFilesMeta, }); // 4. sending form data to selenium service onHandleForMHSelenium({ ...f, serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "Mass Health", appointmentId: appointmentId!, insuranceSiteKey: "MH", claimId: createdClaim.id, }); // 5. Close form onClose(); }; // 2nd Button workflow - Only Creates Data, patient, appointmetn, claim, payment, not actually submits claim to MH site. const handleAddService = 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; // build claimFiles metadata from uploadedFiles (only filename + mimeType) const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({ filename: f.name, mimeType: f.type, })); const createdClaim = await onSubmit({ ...formToCreateClaim, serviceLines: filteredServiceLines, staffId: Number(staff?.id), patientId: patientId, insuranceProvider: "MassHealth", appointmentId: appointmentId!, claimFiles: claimFilesMeta, }); // 4. Close form onClose(); }; const applyComboAndThenMH = async ( comboId: keyof typeof PROCEDURE_COMBOS ) => { const nextForm = applyComboToForm( form, comboId, patient?.dateOfBirth ?? "", { replaceAll: true, lineDate: form.serviceDate } ); setForm(nextForm); setTimeout(() => scrollToLine(0), 0); await handleMHSubmit(nextForm); }; 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 */}
{ onServiceDateChange(date); }} onClose={() => setServiceDateOpen(false)} /> {/* Treating doctor */} {/* Map Price Button */}
Direct Claim Submittion Buttons
{/* Header */}
Procedure Code
Info Procedure Date Oral Cavity Area Tooth Number Tooth Surface Billed Amount
{/* Dynamic Rows */} {form.serviceLines.map((line, i) => { const raw = line.procedureCode || ""; const code = raw.trim(); const desc = code ? getDescriptionForCode(code) || "No description available" : "Enter a procedure code"; return (
{ rowRefs.current[i] = el; if (!el) rowRefs.current.splice(i, 1); }} className="scroll-mt-28 grid grid-cols-[1.5fr,0.5fr,1fr,1fr,1fr,1fr,1fr] gap-1 mb-2 items-center" >
updateServiceLine( i, "procedureCode", e.target.value.toUpperCase() ) } />
{desc}
{/* Date Picker */} setOpenProcedureDateIndex(open ? i : null) } > updateProcedureDate(i, date)} onClose={() => setOpenProcedureDateIndex(null)} /> updateServiceLine(i, "oralCavityArea", e.target.value) } /> updateServiceLine(i, "toothNumber", e.target.value) } /> updateServiceLine(i, "toothSurface", e.target.value) } /> { updateServiceLine(i, "totalBilled", e.target.value); }} onBlur={(e) => { const val = parseFloat(e.target.value); const rounded = Math.round(val * 100) / 100; updateServiceLine( i, "totalBilled", isNaN(rounded) ? 0 : rounded ); }} />
); })}
{Object.entries(COMBO_CATEGORIES).map(([section, ids]) => (
{section}
{ids.map((id) => { const b = PROCEDURE_COMBOS[id]; if (!b) { return; } return ( ); })}
))}
{/* 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

); }