fixes - dob state fixed, combo added

This commit is contained in:
2025-10-17 06:14:35 +05:30
parent ff7c5713d9
commit a5d5e96d9a
4 changed files with 200 additions and 64 deletions

View File

@@ -47,6 +47,8 @@ import {
getDescriptionForCode, getDescriptionForCode,
} from "@/utils/procedureCombosMapping"; } from "@/utils/procedureCombosMapping";
import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos"; import { COMBO_CATEGORIES, PROCEDURE_COMBOS } from "@/utils/procedureCombos";
import { DateInputField } from "../ui/dateInputField";
import { DateInput } from "../ui/dateInput";
interface ClaimFileMeta { interface ClaimFileMeta {
filename: string; filename: string;
@@ -251,22 +253,86 @@ export function ClaimForm({
}, [serviceDate]); }, [serviceDate]);
// Determine patient date of birth format - required as date extracted from pdfs has different format. // 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 ""; if (!dob) return "";
const normalized = formatLocalDate(parseLocalDate(dob)); // Date object -> canonicalize
if (dob instanceof Date) {
// If it's already MM/DD/YYYY, leave it alone if (isNaN(dob.getTime())) return "";
if (/^\d{2}\/\d{2}\/\d{4}$/.test(normalized)) return normalized; return formatLocalDate(dob);
// 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; 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 // MAIN FORM INITIAL STATE
const [form, setForm] = useState<ClaimFormData & { uploadedFiles: File[] }>({ const [form, setForm] = useState<ClaimFormData & { uploadedFiles: File[] }>({
@@ -276,7 +342,7 @@ export function ClaimForm({
staffId: Number(staff?.id), staffId: Number(staff?.id),
patientName: `${patient?.firstName} ${patient?.lastName}`.trim(), patientName: `${patient?.firstName} ${patient?.lastName}`.trim(),
memberId: patient?.insuranceId ?? "", memberId: patient?.insuranceId ?? "",
dateOfBirth: formatDOB(patient?.dateOfBirth), dateOfBirth: normalizeToIsoDateString(patient?.dateOfBirth),
remarks: "", remarks: "",
serviceDate: serviceDate, serviceDate: serviceDate,
insuranceProvider: "", insuranceProvider: "",
@@ -304,7 +370,7 @@ export function ClaimForm({
...prev, ...prev,
patientId: Number(patient.id), patientId: Number(patient.id),
patientName: fullName, patientName: fullName,
dateOfBirth: formatDOB(patient.dateOfBirth), dateOfBirth: normalizeToIsoDateString(patient.dateOfBirth),
memberId: patient.insuranceId || "", memberId: patient.insuranceId || "",
})); }));
} }
@@ -406,6 +472,20 @@ export function ClaimForm({
return; 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 // 1. Create or update appointment
let appointmentIdToUse = appointmentId; let appointmentIdToUse = appointmentId;
@@ -443,9 +523,6 @@ export function ClaimForm({
// 3. Create Claim(if not) // 3. Create Claim(if not)
// Filter out empty service lines (empty procedureCode) // Filter out empty service lines (empty procedureCode)
const filteredServiceLines = f.serviceLines.filter(
(line) => line.procedureCode.trim() !== ""
);
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = f; const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = f;
// build claimFiles metadata from uploadedFiles (only filename + mimeType) // build claimFiles metadata from uploadedFiles (only filename + mimeType)
@@ -498,6 +575,20 @@ export function ClaimForm({
return; 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 // 1. Create or update appointment
let appointmentIdToUse = appointmentId; let appointmentIdToUse = appointmentId;
@@ -535,9 +626,6 @@ export function ClaimForm({
// 3. Create Claim(if not) // 3. Create Claim(if not)
// Filter out empty service lines (empty procedureCode) // Filter out empty service lines (empty procedureCode)
const filteredServiceLines = form.serviceLines.filter(
(line) => line.procedureCode.trim() !== ""
);
const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form; const { uploadedFiles, insuranceSiteKey, ...formToCreateClaim } = form;
// build claimFiles metadata from uploadedFiles (only filename + mimeType) // build claimFiles metadata from uploadedFiles (only filename + mimeType)
@@ -608,24 +696,24 @@ export function ClaimForm({
<Input <Input
id="memberId" id="memberId"
value={form.memberId} value={form.memberId}
onChange={(e) => onChange={(e) => {
setForm({ ...form, memberId: e.target.value }) setForm({ ...form, memberId: e.target.value });
} updatePatientField("insuranceId", e.target.value);
}}
/> />
</div> </div>
<div> <div>
<Label htmlFor="dateOfBirth">Date Of Birth</Label> <Label htmlFor="dateOfBirth">Date Of Birth</Label>
<Input <DateInput
id="dateOfBirth" value={
value={form.dateOfBirth} form.dateOfBirth ? parseLocalDate(form.dateOfBirth) : null
onChange={(e) => { }
updatePatientField("dateOfBirth", e.target.value); onChange={(date: Date | null) => {
setForm((prev) => ({ const formatted = date ? formatLocalDate(date) : "";
...prev, setForm((prev) => ({ ...prev, dateOfBirth: formatted }));
dateOfBirth: e.target.value, updatePatientField("dateOfBirth", formatted);
}));
}} }}
disabled={isLoading} disableFuture
/> />
</div> </div>
<div> <div>
@@ -782,7 +870,7 @@ export function ClaimForm({
"childRecallDirect4BW", "childRecallDirect4BW",
"childRecallDirect2PA2BW", "childRecallDirect2PA2BW",
"childRecallDirect2PA4BW", "childRecallDirect2PA4BW",
"childRecallDirectPANO2PA2BW", "childRecallDirectPANO",
].map((comboId) => { ].map((comboId) => {
const b = PROCEDURE_COMBOS[comboId]; const b = PROCEDURE_COMBOS[comboId];
if (!b) return null; if (!b) return null;
@@ -797,7 +885,7 @@ export function ClaimForm({
childRecallDirect4BW: "Direct 4BW", childRecallDirect4BW: "Direct 4BW",
childRecallDirect2PA2BW: "Direct 2PA 2BW", childRecallDirect2PA2BW: "Direct 2PA 2BW",
childRecallDirect2PA4BW: "Direct 2PA 4BW", childRecallDirect2PA4BW: "Direct 2PA 4BW",
childRecallDirectPANO2PA2BW: "Direct PANO 2PA 2BW", childRecallDirectPANO: "Direct Pano",
}; };
return ( return (
<Tooltip key={b.id}> <Tooltip key={b.id}>
@@ -829,9 +917,10 @@ export function ClaimForm({
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{[ {[
"adultRecallDirect", "adultRecallDirect",
"adultRecallDirect2bw", "adultRecallDirect2BW",
"adultRecallDirect4bw", "adultRecallDirect4BW",
"adultRecallDirect4bw2pa", "adultRecallDirect2PA2BW",
"adultRecallDirect2PA4BW",
"adultRecallDirectPano", "adultRecallDirectPano",
].map((comboId) => { ].map((comboId) => {
const b = PROCEDURE_COMBOS[comboId]; const b = PROCEDURE_COMBOS[comboId];
@@ -843,9 +932,10 @@ export function ClaimForm({
const tooltipText = codesWithTooth.join(", "); const tooltipText = codesWithTooth.join(", ");
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
adultRecallDirect: "Direct", adultRecallDirect: "Direct",
adultRecallDirect2bw: "Direct 2BW", adultRecallDirect2BW: "Direct 2BW",
adultRecallDirect4bw: "Direct 4BW", adultRecallDirect4BW: "Direct 4BW",
adultRecallDirect4bw2pa: "Direct 4BW2PA", adultRecallDirect2PA2BW: "Direct 2PA 2BW",
adultRecallDirect2PA4BW: "Direct 2PA 4BW",
adultRecallDirectPano: "Direct Pano", adultRecallDirectPano: "Direct Pano",
}; };
return ( return (

View File

@@ -29,6 +29,7 @@ import {
UpdatePatient, UpdatePatient,
} from "@repo/db/types"; } from "@repo/db/types";
import ClaimDocumentsUploadMultiple from "@/components/claims/claim-document-upload-modal"; import ClaimDocumentsUploadMultiple from "@/components/claims/claim-document-upload-modal";
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
export default function ClaimsPage() { export default function ClaimsPage() {
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false); const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
@@ -64,6 +65,8 @@ export default function ClaimsPage() {
description: "Patient updated successfully!", description: "Patient updated successfully!",
variant: "default", variant: "default",
}); });
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
}, },
onError: (error) => { onError: (error) => {
toast({ toast({

View File

@@ -39,9 +39,9 @@ export const PROCEDURE_COMBOS: Record<
codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0274"], codes: ["D0120", "D1120", "D1208", "D0220", "D0230", "D0274"],
toothNumbers: [null, null, null, "9", "24", null], // only these two need values toothNumbers: [null, null, null, "9", "24", null], // only these two need values
}, },
childRecallDirectPANO2PA2BW: { childRecallDirectPANO: {
id: "childRecallDirectPANO2PA2BW", id: "childRecallDirectPANO",
label: "Child Recall Direct PANO 2PA 2BW", label: "Child Recall Direct PANO",
codes: ["D0120", "D1120", "D1208", "D0330"], codes: ["D0120", "D1120", "D1208", "D0330"],
}, },
adultRecall: { adultRecall: {
@@ -55,25 +55,31 @@ export const PROCEDURE_COMBOS: Record<
label: "Adult Recall Direct(no x-ray)", label: "Adult Recall Direct(no x-ray)",
codes: ["D0120", "D1110"], codes: ["D0120", "D1110"],
}, },
adultRecallDirect2bw: { adultRecallDirect2BW: {
id: "adultRecallDirect2bw", id: "adultRecallDirect2BW",
label: "Adult Recall Direct - 2bw (no x-ray)", label: "Adult Recall Direct - 2bw (no x-ray)",
codes: ["D0120", "D1110", "D0272"], codes: ["D0120", "D1110", "D0272"],
}, },
adultRecallDirect4bw: { adultRecallDirect4BW: {
id: "adultRecallDirect4bw", id: "adultRecallDirect4BW",
label: "Adult Recall Direct - 4bw (no x-ray)", label: "Adult Recall Direct - 4bw (no x-ray)",
codes: ["D0120", "D1110", "D0274"], codes: ["D0120", "D1110", "D0274"],
}, },
adultRecallDirect4bw2pa: { adultRecallDirect2PA2BW: {
id: "adultRecallDirect4bw", id: "adultRecallDirect2PA2BW",
label: "Adult Recall Direct - 4bw (no x-ray)", 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"], codes: ["D0120", "D0220", "D0230", "D0274", "D1110"],
toothNumbers: [null, "9", "24", null, null], // only these two need values toothNumbers: [null, "9", "24", null, null], // only these two need values
}, },
adultRecallDirectPano: { adultRecallDirectPano: {
id: "adultRecallDirectPano", id: "adultRecallDirectPano",
label: "Adult Recall Direct - Pano", label: "Adult Recall Direct - PANO",
codes: ["D0120", "D1110", "D0330"], codes: ["D0120", "D1110", "D0330"],
}, },
newChildPatient: { newChildPatient: {
@@ -83,7 +89,7 @@ export const PROCEDURE_COMBOS: Record<
}, },
newAdultPatientPano: { newAdultPatientPano: {
id: "newAdultPatientPano", id: "newAdultPatientPano",
label: "New Adult Patient (Pano)", label: "New Adult Patient - PANO",
codes: ["D0150", "D0330", "D1110"], codes: ["D0150", "D0330", "D1110"],
}, },
newAdultPatientFMX: { newAdultPatientFMX: {

View File

@@ -107,17 +107,56 @@ const ageOnDate = (dob: DateInput, on: DateInput): number => {
}; };
/** /**
* Price chooser that respects your age rules and IC/NC semantics. * we can implement per-code age buckets without changing the JSON.
* - If <=21 → PriceLTEQ21 (if present and not IC/NC/blank) else Price. *
* - If >21 → PriceGT21 (if present and not IC/NC/blank) else Price. * Behavior:
* - If chosen field is IC/NC/blank → 0 (leave empty). * - 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 (age <= 21) {
if (!isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21); if (!isBlankPrice(row.PriceLTEQ21)) return toDecimalOrZero(row.PriceLTEQ21);
} else { } else {
if (!isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21); if (!isBlankPrice(row.PriceGT21)) return toDecimalOrZero(row.PriceGT21);
} }
// Fallback to Price if tiered not available/blank // Fallback to Price if tiered not available/blank
if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price); if (!isBlankPrice(row.Price)) return toDecimalOrZero(row.Price);
return new Decimal(0); return new Decimal(0);
@@ -131,8 +170,9 @@ function getPriceForCodeWithAgeFromMap(
code: string, code: string,
age: number age: number
): Decimal { ): Decimal {
const row = map.get(normalizeCode(code)); const norm = normalizeCode(code);
return row ? pickPriceForRowByAge(row, age) : new Decimal(0); const row = map.get(norm);
return row ? pickPriceForRowByAge(row, age, norm) : new Decimal(0);
} }
// helper keeping lines empty, // helper keeping lines empty,
@@ -251,10 +291,7 @@ export function applyComboToForm<T extends ClaimFormLike>(
procedureCode: code, procedureCode: code,
procedureDate: lineDate, procedureDate: lineDate,
oralCavityArea: original?.oralCavityArea ?? "", oralCavityArea: original?.oralCavityArea ?? "",
toothNumber: toothNumber: preset.toothNumbers?.[j] ?? original?.toothNumber ?? "",
preset.toothNumbers?.[j] ??
original?.toothNumber ??
"",
toothSurface: original?.toothSurface ?? "", toothSurface: original?.toothSurface ?? "",
totalBilled: price, totalBilled: price,
totalAdjusted: new Decimal(0), totalAdjusted: new Decimal(0),