From a5d5e96d9a7ff731ede8f9ecd070ad05309a22fd Mon Sep 17 00:00:00 2001 From: Potenz Date: Fri, 17 Oct 2025 06:14:35 +0530 Subject: [PATCH] fixes - dob state fixed, combo added --- .../src/components/claims/claim-form.tsx | 172 +++++++++++++----- apps/Frontend/src/pages/claims-page.tsx | 3 + apps/Frontend/src/utils/procedureCombos.ts | 30 +-- .../src/utils/procedureCombosMapping.ts | 59 ++++-- 4 files changed, 200 insertions(+), 64 deletions(-) diff --git a/apps/Frontend/src/components/claims/claim-form.tsx b/apps/Frontend/src/components/claims/claim-form.tsx index 639f3b4..6b8dd57 100644 --- a/apps/Frontend/src/components/claims/claim-form.tsx +++ b/apps/Frontend/src/components/claims/claim-form.tsx @@ -47,6 +47,8 @@ import { getDescriptionForCode, } from "@/utils/procedureCombosMapping"; import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; +import { DateInputField } from "../ui/dateInputField"; +import { DateInput } from "../ui/dateInput"; interface ClaimFileMeta { filename: string; @@ -251,22 +253,86 @@ export function ClaimForm({ }, [serviceDate]); // Determine patient date of birth format - required as date extracted from pdfs has different format. - const formatDOB = (dob: string | Date | undefined) => { + // Replace previous implementation with this type-safe normalizer. + // Always returns canonical YYYY-MM-DD or "" if it cannot parse. + function normalizeToIsoDateString(dob: string | Date | undefined): string { 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}`; + // Date object -> canonicalize + if (dob instanceof Date) { + if (isNaN(dob.getTime())) return ""; + return formatLocalDate(dob); } - return normalized; - }; + const raw = String(dob).trim(); + if (!raw) return ""; + + // 1) If already date-only ISO (yyyy-mm-dd) + if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) { + try { + parseLocalDate(raw); // validate + return raw; + } catch { + return ""; + } + } + + // 2) Try parseLocalDate for ISO-like inputs (will throw if not suitable) + try { + const parsed = parseLocalDate(raw); + return formatLocalDate(parsed); + } catch { + // continue to other fallbacks + } + + // 3) MM/DD/YYYY or M/D/YYYY -> convert to ISO + const m1 = raw.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); + if (m1) { + const mm = m1[1] ?? ""; + const dd = m1[2] ?? ""; + const yyyy = m1[3] ?? ""; + if (mm && dd && yyyy) { + const iso = `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`; + try { + parseLocalDate(iso); + return iso; + } catch { + return ""; + } + } + } + + // 4) OCR-ish short form: MMDDYY (exactly 6 digits) -> guess century + const m2 = raw.match(/^(\d{6})$/); + if (m2) { + const s = m2[1]; + if (s && s.length === 6) { + const mm = s.slice(0, 2); + const dd = s.slice(2, 4); + const yy = s.slice(4, 6); + const year = Number(yy) < 50 ? 2000 + Number(yy) : 1900 + Number(yy); + const iso = `${year}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}`; + try { + parseLocalDate(iso); + return iso; + } catch { + return ""; + } + } + } + + // 5) Last resort: naive Date parse -> normalize to local calendar fields + try { + const maybe = new Date(raw); + if (!isNaN(maybe.getTime())) { + return formatLocalDate(maybe); + } + } catch { + /* ignore */ + } + + return ""; + } // MAIN FORM INITIAL STATE const [form, setForm] = useState({ @@ -276,7 +342,7 @@ export function ClaimForm({ staffId: Number(staff?.id), patientName: `${patient?.firstName} ${patient?.lastName}`.trim(), memberId: patient?.insuranceId ?? "", - dateOfBirth: formatDOB(patient?.dateOfBirth), + dateOfBirth: normalizeToIsoDateString(patient?.dateOfBirth), remarks: "", serviceDate: serviceDate, insuranceProvider: "", @@ -304,7 +370,7 @@ export function ClaimForm({ ...prev, patientId: Number(patient.id), patientName: fullName, - dateOfBirth: formatDOB(patient.dateOfBirth), + dateOfBirth: normalizeToIsoDateString(patient.dateOfBirth), memberId: patient.insuranceId || "", })); } @@ -406,6 +472,20 @@ export function ClaimForm({ return; } + // require at least one procedure code before proceeding + const filteredServiceLines = (f.serviceLines || []).filter( + (line) => (line.procedureCode ?? "").trim() !== "" + ); + if (filteredServiceLines.length === 0) { + toast({ + title: "No procedure codes", + description: + "Please add at least one procedure code before submitting the claim.", + variant: "destructive", + }); + return; + } + // 1. Create or update appointment let appointmentIdToUse = appointmentId; @@ -443,9 +523,6 @@ export function ClaimForm({ // 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) @@ -498,6 +575,20 @@ export function ClaimForm({ return; } + // require at least one procedure code before proceeding + const filteredServiceLines = (form.serviceLines || []).filter( + (line) => (line.procedureCode ?? "").trim() !== "" + ); + if (filteredServiceLines.length === 0) { + toast({ + title: "No procedure codes", + description: + "Please add at least one procedure code before submitting the claim.", + variant: "destructive", + }); + return; + } + // 1. Create or update appointment let appointmentIdToUse = appointmentId; @@ -535,9 +626,6 @@ export function ClaimForm({ // 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) @@ -608,24 +696,24 @@ export function ClaimForm({ - setForm({ ...form, memberId: e.target.value }) - } + onChange={(e) => { + setForm({ ...form, memberId: e.target.value }); + updatePatientField("insuranceId", e.target.value); + }} />
- { - updatePatientField("dateOfBirth", e.target.value); - setForm((prev) => ({ - ...prev, - dateOfBirth: e.target.value, - })); + { + const formatted = date ? formatLocalDate(date) : ""; + setForm((prev) => ({ ...prev, dateOfBirth: formatted })); + updatePatientField("dateOfBirth", formatted); }} - disabled={isLoading} + disableFuture />
@@ -782,7 +870,7 @@ export function ClaimForm({ "childRecallDirect4BW", "childRecallDirect2PA2BW", "childRecallDirect2PA4BW", - "childRecallDirectPANO2PA2BW", + "childRecallDirectPANO", ].map((comboId) => { const b = PROCEDURE_COMBOS[comboId]; if (!b) return null; @@ -797,7 +885,7 @@ export function ClaimForm({ childRecallDirect4BW: "Direct 4BW", childRecallDirect2PA2BW: "Direct 2PA 2BW", childRecallDirect2PA4BW: "Direct 2PA 4BW", - childRecallDirectPANO2PA2BW: "Direct PANO 2PA 2BW", + childRecallDirectPANO: "Direct Pano", }; return ( @@ -829,9 +917,10 @@ export function ClaimForm({
{[ "adultRecallDirect", - "adultRecallDirect2bw", - "adultRecallDirect4bw", - "adultRecallDirect4bw2pa", + "adultRecallDirect2BW", + "adultRecallDirect4BW", + "adultRecallDirect2PA2BW", + "adultRecallDirect2PA4BW", "adultRecallDirectPano", ].map((comboId) => { const b = PROCEDURE_COMBOS[comboId]; @@ -843,9 +932,10 @@ export function ClaimForm({ const tooltipText = codesWithTooth.join(", "); const labelMap: Record = { adultRecallDirect: "Direct", - adultRecallDirect2bw: "Direct 2BW", - adultRecallDirect4bw: "Direct 4BW", - adultRecallDirect4bw2pa: "Direct 4BW2PA", + adultRecallDirect2BW: "Direct 2BW", + adultRecallDirect4BW: "Direct 4BW", + adultRecallDirect2PA2BW: "Direct 2PA 2BW", + adultRecallDirect2PA4BW: "Direct 2PA 4BW", adultRecallDirectPano: "Direct Pano", }; return ( diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 0ec8c76..94fb68c 100644 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -29,6 +29,7 @@ import { UpdatePatient, } from "@repo/db/types"; import ClaimDocumentsUploadMultiple from "@/components/claims/claim-document-upload-modal"; +import { QK_PATIENTS_BASE } from "@/components/patients/patient-table"; export default function ClaimsPage() { const [isClaimFormOpen, setIsClaimFormOpen] = useState(false); @@ -64,6 +65,8 @@ export default function ClaimsPage() { description: "Patient updated successfully!", variant: "default", }); + queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE }); + queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); }, onError: (error) => { toast({ diff --git a/apps/Frontend/src/utils/procedureCombos.ts b/apps/Frontend/src/utils/procedureCombos.ts index 27489d8..cfd2b8c 100644 --- a/apps/Frontend/src/utils/procedureCombos.ts +++ b/apps/Frontend/src/utils/procedureCombos.ts @@ -39,9 +39,9 @@ export const PROCEDURE_COMBOS: Record< codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0274"], toothNumbers: [null, null, null, "9", "24", null], // only these two need values }, - childRecallDirectPANO2PA2BW: { - id: "childRecallDirectPANO2PA2BW", - label: "Child Recall Direct PANO 2PA 2BW", + childRecallDirectPANO: { + id: "childRecallDirectPANO", + label: "Child Recall Direct PANO", codes: ["D0120", "D1120", "D1208", "D0330"], }, adultRecall: { @@ -55,25 +55,31 @@ export const PROCEDURE_COMBOS: Record< label: "Adult Recall Direct(no x-ray)", codes: ["D0120", "D1110"], }, - adultRecallDirect2bw: { - id: "adultRecallDirect2bw", + adultRecallDirect2BW: { + id: "adultRecallDirect2BW", label: "Adult Recall Direct - 2bw (no x-ray)", codes: ["D0120", "D1110", "D0272"], }, - adultRecallDirect4bw: { - id: "adultRecallDirect4bw", + adultRecallDirect4BW: { + id: "adultRecallDirect4BW", label: "Adult Recall Direct - 4bw (no x-ray)", codes: ["D0120", "D1110", "D0274"], }, - adultRecallDirect4bw2pa: { - id: "adultRecallDirect4bw", - label: "Adult Recall Direct - 4bw (no x-ray)", + adultRecallDirect2PA2BW: { + id: "adultRecallDirect2PA2BW", + label: "Adult Recall Direct - 2PA 2BW", + codes: ["D0120", "D0220", "D0230", "D0272", "D1110"], + toothNumbers: [null, "9", "24", null, null], // only these two need values + }, + adultRecallDirect2PA4BW: { + id: "adultRecallDirect2PA4BW", + label: "Adult Recall Direct - 2PA 4BW", codes: ["D0120", "D0220", "D0230", "D0274", "D1110"], toothNumbers: [null, "9", "24", null, null], // only these two need values }, adultRecallDirectPano: { id: "adultRecallDirectPano", - label: "Adult Recall Direct - Pano", + label: "Adult Recall Direct - PANO", codes: ["D0120", "D1110", "D0330"], }, newChildPatient: { @@ -83,7 +89,7 @@ export const PROCEDURE_COMBOS: Record< }, newAdultPatientPano: { id: "newAdultPatientPano", - label: "New Adult Patient (Pano)", + label: "New Adult Patient - PANO", codes: ["D0150", "D0330", "D1110"], }, newAdultPatientFMX: { diff --git a/apps/Frontend/src/utils/procedureCombosMapping.ts b/apps/Frontend/src/utils/procedureCombosMapping.ts index 2b21564..95ab307 100644 --- a/apps/Frontend/src/utils/procedureCombosMapping.ts +++ b/apps/Frontend/src/utils/procedureCombosMapping.ts @@ -107,17 +107,56 @@ const ageOnDate = (dob: DateInput, on: DateInput): number => { }; /** - * Price chooser that respects your age rules and IC/NC semantics. - * - If <=21 → PriceLTEQ21 (if present and not IC/NC/blank) else Price. - * - If >21 → PriceGT21 (if present and not IC/NC/blank) else Price. - * - If chosen field is IC/NC/blank → 0 (leave empty). + * we can implement per-code age buckets without changing the JSON. + * + * Behavior: + * - Default: same as before: age <= 21 -> PriceLTEQ21, else PriceGT21 + * - Fallback to Price if tiered field is blank/IC/NC + * - Special-cases D1110 and D1120 according to MH rules */ -export function pickPriceForRowByAge(row: CodeRow, age: number): Decimal { +export function pickPriceForRowByAge( + row: CodeRow, + age: number, + normalizedCode?: string +): Decimal { + // Special-case rules (add more codes here if needed) + if (normalizedCode) { + // D1110: only valid for age >=14 (14..21 => PriceLTEQ21, >21 => PriceGT21) + if (normalizedCode === "D1110") { + if (age < 14) { + // D1110 not applicable to children <14 (those belong to D1120) + return new Decimal(0); + } + if (age >= 14 && age <= 21) { + // use PriceLTEQ21 only if present + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + return new Decimal(0); + } + // age > 21 + if (!isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); + return new Decimal(0); + } + + // D1120: child 0-13 => PriceLTEQ21, otherwise no price (NC) + if (normalizedCode === "D1120") { + if (age < 14) { + if (!isBlankPrice(row.PriceLTEQ21)) + return toDecimalOrZero(row.PriceLTEQ21); + return new Decimal(0); + } + // age >= 14 => NC / no price + return new Decimal(0); + } + } + + // Generic/default behavior (unchanged) if (age <= 21) { if (!isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21); } else { if (!isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); } + // Fallback to Price if tiered not available/blank if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); return new Decimal(0); @@ -131,8 +170,9 @@ function getPriceForCodeWithAgeFromMap( code: string, age: number ): Decimal { - const row = map.get(normalizeCode(code)); - return row ? pickPriceForRowByAge(row, age) : new Decimal(0); + const norm = normalizeCode(code); + const row = map.get(norm); + return row ? pickPriceForRowByAge(row, age, norm) : new Decimal(0); } // helper keeping lines empty, @@ -251,10 +291,7 @@ export function applyComboToForm( procedureCode: code, procedureDate: lineDate, oralCavityArea: original?.oralCavityArea ?? "", - toothNumber: - preset.toothNumbers?.[j] ?? - original?.toothNumber ?? - "", + toothNumber: preset.toothNumbers?.[j] ?? original?.toothNumber ?? "", toothSurface: original?.toothSurface ?? "", totalBilled: price, totalAdjusted: new Decimal(0),