fix: extract tooth number and surface from #NN notation for all DDMA procedures
Added extractToothSurface() helper that parses "#NN [SURFACES]" from any procedure input (tooth 1-32, surface letters O/M/D/B/L/F/I/V). Applied to all lookup paths so any procedure with #14 or #29 OB carries toothNumber and toothSurface through to the DDMA Selenium worker. Also propagate toothNumber/toothSurface in matchedCodes and chatbot_claim_prefill so the claim-form prefill sets these fields on the service lines before auto-submit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ interface CdtMatch {
|
|||||||
code: string;
|
code: string;
|
||||||
description: string;
|
description: string;
|
||||||
input: string;
|
input: string;
|
||||||
|
toothNumber?: string;
|
||||||
|
toothSurface?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load once at module init
|
// Load once at module init
|
||||||
@@ -215,6 +217,23 @@ const DIRECT_CODE_MAP: Record<string, string> = {
|
|||||||
"implant crown": "D6058",
|
"implant crown": "D6058",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tooth number and surface from any input containing "#NN" (1-32).
|
||||||
|
* e.g. "#29 OB" → { toothNumber: "29", toothSurface: "OB" }
|
||||||
|
* e.g. "extraction #14" → { toothNumber: "14" }
|
||||||
|
*/
|
||||||
|
function extractToothSurface(input: string): { toothNumber?: string; toothSurface?: string } {
|
||||||
|
const m = input.match(/#(\d{1,2})(?:\s+([A-Za-z]{1,5}))?/);
|
||||||
|
if (!m) return {};
|
||||||
|
const toothNum = parseInt(m[1]!, 10);
|
||||||
|
if (toothNum < 1 || toothNum > 32) return {};
|
||||||
|
const surfaces = (m[2] ?? "").toUpperCase();
|
||||||
|
return {
|
||||||
|
toothNumber: String(toothNum),
|
||||||
|
...(surfaces && /^[OMDBLFIV]+$/.test(surfaces) ? { toothSurface: surfaces } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Composite filling tooth classification
|
// Composite filling tooth classification
|
||||||
const FRONT_TEETH = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]);
|
const FRONT_TEETH = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]);
|
||||||
const BACK_TEETH = new Set([1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31, 32]);
|
const BACK_TEETH = new Set([1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31, 32]);
|
||||||
@@ -249,7 +268,7 @@ function parseCompositeCode(input: string): CdtMatch | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const row = ALL_CODES.find((r) => r["Procedure Code"] === code);
|
const row = ALL_CODES.find((r) => r["Procedure Code"] === code);
|
||||||
return { code, description: row?.Description ?? code, input };
|
return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum), toothSurface: surfaces.toUpperCase() };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -326,28 +345,30 @@ export function lookupCdtCodes(
|
|||||||
|
|
||||||
return procedureNames.map((name) => {
|
return procedureNames.map((name) => {
|
||||||
const cleaned = name.trim().toLowerCase();
|
const cleaned = name.trim().toLowerCase();
|
||||||
|
// Extract tooth# from "#NN" notation — applies to any procedure
|
||||||
|
const toothInfo = extractToothSurface(name);
|
||||||
|
|
||||||
// 1. Custom alias exact match (highest priority)
|
// 1. Custom alias exact match (highest priority)
|
||||||
if (customMap[cleaned]) {
|
if (customMap[cleaned]) {
|
||||||
const code = customMap[cleaned]!;
|
const code = customMap[cleaned]!;
|
||||||
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
||||||
return { code, description: row?.Description ?? code, input: name };
|
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Composite filling by tooth# and surfaces (e.g. "#29 OB", "composite #8 MO")
|
// 2. Composite filling by tooth# and surfaces — also determines CDT code from tooth position
|
||||||
const compositeMatch = parseCompositeCode(name);
|
const compositeMatch = parseCompositeCode(name);
|
||||||
if (compositeMatch) return compositeMatch;
|
if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface
|
||||||
|
|
||||||
// 3. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
|
// 3. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
|
||||||
if (DIRECT_CODE_MAP[cleaned]) {
|
if (DIRECT_CODE_MAP[cleaned]) {
|
||||||
const code = DIRECT_CODE_MAP[cleaned]!;
|
const code = DIRECT_CODE_MAP[cleaned]!;
|
||||||
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
||||||
return { code, description: row?.Description ?? code, input: name };
|
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Hardcoded alias + keyword search
|
// 4. Hardcoded alias + keyword search
|
||||||
const match = matchOne(name);
|
const match = matchOne(name);
|
||||||
if (match) return match;
|
if (match) return { ...match, ...toothInfo };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: null,
|
code: null,
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ async function handleCheckAndClaim(
|
|||||||
siteKey,
|
siteKey,
|
||||||
autoCheck: siteKeyToAutoCheck(siteKey),
|
autoCheck: siteKeyToAutoCheck(siteKey),
|
||||||
cdtResults,
|
cdtResults,
|
||||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })),
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -518,7 +518,7 @@ async function handleClaimOnly(
|
|||||||
actionData: {
|
actionData: {
|
||||||
patient,
|
patient,
|
||||||
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
|
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
|
||||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })),
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })),
|
||||||
options: [
|
options: [
|
||||||
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) },
|
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) },
|
||||||
{ label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) },
|
{ label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) },
|
||||||
@@ -561,7 +561,7 @@ async function handleClaimOnly(
|
|||||||
siteKey,
|
siteKey,
|
||||||
serviceDate,
|
serviceDate,
|
||||||
appointmentId,
|
appointmentId,
|
||||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description })),
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ export function ClaimForm({
|
|||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
try {
|
try {
|
||||||
const { codes, serviceDate } = JSON.parse(raw) as {
|
const { codes, serviceDate } = JSON.parse(raw) as {
|
||||||
codes: { code: string; description: string }[];
|
codes: { code: string; description: string; toothNumber?: string; toothSurface?: string }[];
|
||||||
serviceDate?: string;
|
serviceDate?: string;
|
||||||
};
|
};
|
||||||
sessionStorage.removeItem("chatbot_claim_prefill");
|
sessionStorage.removeItem("chatbot_claim_prefill");
|
||||||
@@ -515,7 +515,13 @@ export function ClaimForm({
|
|||||||
const updatedLines = [...prev.serviceLines];
|
const updatedLines = [...prev.serviceLines];
|
||||||
codes.forEach((c, i) => {
|
codes.forEach((c, i) => {
|
||||||
if (i < updatedLines.length) {
|
if (i < updatedLines.length) {
|
||||||
updatedLines[i] = { ...updatedLines[i]!, procedureCode: c.code, procedureDate: date };
|
updatedLines[i] = {
|
||||||
|
...updatedLines[i]!,
|
||||||
|
procedureCode: c.code,
|
||||||
|
procedureDate: date,
|
||||||
|
...(c.toothNumber ? { toothNumber: c.toothNumber } : {}),
|
||||||
|
...(c.toothSurface ? { toothSurface: c.toothSurface } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { ...prev, serviceLines: updatedLines };
|
return { ...prev, serviceLines: updatedLines };
|
||||||
|
|||||||
Reference in New Issue
Block a user