From 91adac89e53248f08f081587fabfbf8ce1f2b769 Mon Sep 17 00:00:00 2001 From: Gitead Date: Mon, 22 Jun 2026 10:31:03 -0400 Subject: [PATCH] fix: AI claim queue restart from first patient; recognize D7210 # 32 in notes - appointments-page: save full queue from current patient on confirm so that cancelling the claim form resumes at the same patient instead of skipping it - claims-page: advance ai_claim_queue only after successful submission (CCA, MH selenium PDF download, or direct non-draft submit) via new advanceAiClaimQueue() - cdt-lookup: fix extractToothSurface regex to allow space between # and digit (e.g. "# 32") so tooth number is correctly captured - cdt-lookup: silently skip bare tooth-number tokens ("# 32", "#32") that the LLM may emit as standalone items, preventing false "unrecognized procedure" errors - internal-chat-graph: add explicit rule that a CDT code followed by # NN (e.g. "D7210 # 32") must stay as one entry and not spread to other procedures Co-Authored-By: Claude Sonnet 4.6 --- apps/Backend/src/ai/cdt-lookup.ts | 11 +++++++--- apps/Backend/src/ai/internal-chat-graph.ts | 4 ++++ apps/Frontend/src/pages/appointments-page.tsx | 8 +++---- apps/Frontend/src/pages/claims-page.tsx | 21 +++++++++++++++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/apps/Backend/src/ai/cdt-lookup.ts b/apps/Backend/src/ai/cdt-lookup.ts index ceaabfba..c2e9e2e4 100644 --- a/apps/Backend/src/ai/cdt-lookup.ts +++ b/apps/Backend/src/ai/cdt-lookup.ts @@ -227,8 +227,8 @@ const DIRECT_CODE_MAP: Record = { * e.g. "extraction #14" → { toothNumber: "14" } */ function extractToothSurface(input: string): { toothNumber?: string; toothSurface?: string } { - // Numeric tooth #1-32 (e.g. "#29 OB") - const m = input.match(/#(\d{1,2})(?:\s+([A-Za-z]{1,5}))?/); + // Numeric tooth #1-32 (e.g. "#29 OB", "# 32") + const m = input.match(/#\s*(\d{1,2})(?:\s+([A-Za-z]{1,5}))?/); if (m) { const toothNum = parseInt(m[1]!, 10); if (toothNum >= 1 && toothNum <= 32) { @@ -418,8 +418,13 @@ export function lookupCdtCodes( if (phrase && cdtCode) customMap[phrase.toLowerCase().trim()] = cdtCode.toUpperCase().trim(); } - return procedureNames.map((name) => { + return procedureNames.flatMap((name) => { const cleaned = name.trim().toLowerCase(); + + // If the LLM emitted a bare tooth-number token like "# 32" or "#32" with no procedure, + // silently skip it — the tooth info was already captured on the preceding CDT code entry. + if (/^#\s*\d{1,2}$/.test(cleaned)) return []; + // Extract tooth# from "#NN" notation — applies to any procedure const toothInfo = extractToothSurface(name); // Strip tooth notation for alias matching: diff --git a/apps/Backend/src/ai/internal-chat-graph.ts b/apps/Backend/src/ai/internal-chat-graph.ts index afeb3624..911e5a40 100644 --- a/apps/Backend/src/ai/internal-chat-graph.ts +++ b/apps/Backend/src/ai/internal-chat-graph.ts @@ -141,6 +141,10 @@ Rules: e.g. "#14 rct, buildup, crown" → ["#14 rct", "#14 buildup", "#14 crown"] - For RCT/root canal with a tooth number, preserve the tooth# in the entry: e.g. "rct #29", "#14 root canal", "rct #6", "#20 rct" — keep the #number with the procedure so the correct code can be selected +- For a CDT code (D####) followed by a tooth number (# NN or #NN), keep them together as ONE entry and do NOT spread the tooth# to other procedures: + e.g. "Limited exam, PANO, D7210 # 32" → ["Limited exam", "PANO", "D7210 # 32"] + e.g. "D7210 #32, D0140, pano" → ["D7210 #32", "D0140", "pano"] + The tooth# after a CDT code belongs only to that procedure. - For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry: e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code - For multiple PA X-rays with tooth numbers, expand each PA into its own entry: diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 0ab6a983..6be3411c 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -1577,11 +1577,11 @@ export default function AppointmentsPage() { const handleAiClaimConfirm = () => { if (!aiClaimCurrentData) return; const { matchedCodes, siteKey, serviceDate, appointmentId } = aiClaimCurrentData; - const nextIndex = aiClaimCurrentIndex + 1; - const remaining = aiClaimQueue.slice(nextIndex); - if (remaining.length > 0) { + // Include current patient in queue; claims-page advances it only on successful submit. + const fromCurrent = aiClaimQueue.slice(aiClaimCurrentIndex); + if (fromCurrent.length > 0) { sessionStorage.setItem("ai_claim_queue", JSON.stringify({ - appointments: remaining, + appointments: fromCurrent, date: formattedSelectedDate, pendingResume: true, })); diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 00e9a558..11591721 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -142,6 +142,7 @@ export default function ClaimsPage() { // CCA result: pdfFileId is already saved by the processor — open preview directly if (jobResult.pdfFileId) { + advanceAiClaimQueue(); setPreviewPdfId(jobResult.pdfFileId); setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`); setPreviewOpen(true); @@ -332,6 +333,23 @@ export default function ClaimsPage() { } }; + // Advance the ai_claim_queue by removing the first item (current patient). + // Called only on successful submission so that cancel leaves the queue intact. + const advanceAiClaimQueue = () => { + try { + const raw = sessionStorage.getItem("ai_claim_queue"); + if (!raw) return; + const parsed = JSON.parse(raw); + if (!parsed?.pendingResume || !Array.isArray(parsed.appointments)) return; + const [, ...rest] = parsed.appointments; + if (rest.length > 0) { + sessionStorage.setItem("ai_claim_queue", JSON.stringify({ ...parsed, appointments: rest })); + } else { + sessionStorage.removeItem("ai_claim_queue"); + } + } catch {} + }; + // 3. create or update claim (update when claimId is present) const handleClaimSubmit = async (claimData: any): Promise => { const { isDraft, claimId, uploadedFiles: _uf, ...cleanData } = claimData; @@ -352,6 +370,7 @@ export default function ClaimsPage() { const data = await res.json(); if (!res.ok) throw new Error(data?.message || "Failed to save claim"); queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE }); + if (!isDraft) advanceAiClaimQueue(); return data; }; @@ -865,6 +884,8 @@ export default function ClaimsPage() { duration: isPreAuth ? 10000 : 5000, }); + if (!isPreAuth) advanceAiClaimQueue(); + // Pop up the final PDF so the user doesn't need to navigate to Documents if (result.pdfFileId) { setPreviewPdfId(result.pdfFileId);