From 60689e58f69ad94b8e1cb15825245eb3d5cbcc13 Mon Sep 17 00:00:00 2001 From: ff Date: Fri, 19 Jun 2026 23:32:05 -0400 Subject: [PATCH] feat: batch eligibility by patient name + "Check All & Appointment Today" option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add batch_eligibility_by_name intent so "check Mary and Sinthia" resolves multiple names - Add "Check All & Appointment Today" button to batch eligibility UI — creates appointment for each patient after their eligibility check completes Co-Authored-By: Claude Sonnet 4.6 --- apps/Backend/src/ai/internal-chat-graph.ts | 6 ++ apps/Backend/src/ai/internal-chat-workflow.ts | 93 +++++++++++++++++++ .../src/components/layout/chatbot.tsx | 20 ++++ .../src/pages/insurance-status-page.tsx | 25 +++-- 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/apps/Backend/src/ai/internal-chat-graph.ts b/apps/Backend/src/ai/internal-chat-graph.ts index 6eff3b24..165a600f 100644 --- a/apps/Backend/src/ai/internal-chat-graph.ts +++ b/apps/Backend/src/ai/internal-chat-graph.ts @@ -6,6 +6,7 @@ export type InternalChatIntent = | "check_eligibility" // by patient name → look up in DB | "eligibility_by_id" // by explicit memberId + dob (no name) | "batch_eligibility" // multiple patients by memberId + dob + | "batch_eligibility_by_name" // multiple patients by name (no memberId) | "batch_claim" // claim same procedures for multiple patients by name | "batch_check_and_claim" // eligibility + claim for multiple patients by memberId+dob | "check_and_claim" // eligibility + claim procedures @@ -81,6 +82,11 @@ Intents: e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000" Use this ONLY when TWO OR MORE distinct memberId+dob pairs are given. Put each pair into the "patients" array. Also set insuranceHint if stated. +- batch_eligibility_by_name : user wants to check eligibility for MULTIPLE patients identified by NAME + e.g. "check Mary and Sinthia", "check eligibility for John, Jane, and Bob" + e.g. "verify insurance for Mary and Sinthia" + Use this when TWO OR MORE patient names are given WITHOUT member IDs. + Put each patient name into the "patientNames" array. - batch_claim : user wants to claim the SAME procedures for MULTIPLE patients identified by NAME e.g. "claim perio exam and adult prophy for Jackaline and Keioson" e.g. "perio exam, adult cleaning for Maria and John" diff --git a/apps/Backend/src/ai/internal-chat-workflow.ts b/apps/Backend/src/ai/internal-chat-workflow.ts index 1d172739..c65de418 100644 --- a/apps/Backend/src/ai/internal-chat-workflow.ts +++ b/apps/Backend/src/ai/internal-chat-workflow.ts @@ -323,6 +323,10 @@ export async function runInternalChatWorkflow( return await handleBatchEligibility(classification, storage); } + if (intent === "batch_eligibility_by_name") { + return await handleBatchEligibilityByName(classification, storage); + } + // ── Check eligibility + claim procedures ────────────────────────────────── if (intent === "check_and_claim") { @@ -525,6 +529,95 @@ async function handleBatchEligibility( }; } +// ─── batch_eligibility_by_name ─────────────────────────────────────────────── + +async function handleBatchEligibilityByName( + c: ChatClassification, + storage: StorageLike +): Promise { + const names = c.patientNames ?? []; + if (names.length < 2) { + return { reply: "Please include at least two patient names to batch-check eligibility." }; + } + + const resolved: { + memberId: string; + dob: string; + siteKey: string; + autoCheck: string; + patient: ResolvedPatient | null; + }[] = []; + const notFound: string[] = []; + const noInsurance: string[] = []; + + for (const name of names) { + const trimmed = name.trim(); + if (!trimmed) continue; + const raw = await findPatientByName(trimmed, storage); + if (!raw) { + notFound.push(trimmed); + continue; + } + const patient = patientToResult(raw); + const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(); + + if (!patient.insuranceId) { + noInsurance.push(fullName); + continue; + } + + const resolvedDob = patient.dateOfBirth ?? null; + if (!resolvedDob) { + noInsurance.push(fullName); + continue; + } + + const siteKey = resolveSiteKey( + patient.insuranceProvider ?? null, + c.insuranceHint ?? null + ) ?? "MH"; + + resolved.push({ + memberId: patient.insuranceId, + dob: resolvedDob, + siteKey, + autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob), + patient, + }); + } + + if (resolved.length === 0) { + if (notFound.length > 0) { + return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` }; + } + if (noInsurance.length > 0) { + return { reply: `Found ${noInsurance.join(", ")} but they have no Member ID or DOB on file. Please add their insurance info first.` }; + } + return { reply: "Could not resolve any patients for eligibility check." }; + } + + const labels = resolved.map((r) => { + const name = r.patient + ? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim() + : `ID ${r.memberId}`; + return name; + }); + + let reply = `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`; + if (notFound.length > 0) { + reply += ` Could not find: ${notFound.join(", ")}.`; + } + if (noInsurance.length > 0) { + reply += ` Missing insurance info: ${noInsurance.join(", ")}.`; + } + + return { + reply, + action: "batch_eligibility_ready", + actionData: { queue: resolved }, + }; +} + // ─── batch_check_and_claim ──────────────────────────────────────────────────── async function handleBatchCheckAndClaim( diff --git a/apps/Frontend/src/components/layout/chatbot.tsx b/apps/Frontend/src/components/layout/chatbot.tsx index af590e6a..0217d470 100644 --- a/apps/Frontend/src/components/layout/chatbot.tsx +++ b/apps/Frontend/src/components/layout/chatbot.tsx @@ -394,6 +394,18 @@ export function ChatbotButton() { prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck); }; + const handleBatchEligibilityAndAppointment = () => { + if (!batchEligibilityData || batchEligibilityData.length === 0) return; + addMsg("user", `Check all & appointment today (${batchEligibilityData.length} patients)`); + addMsg("bot", `Checking ${batchEligibilityData.length} patients — appointments will be created after each check...`); + sessionStorage.setItem("chatbot_batch_appt_after_eligibility", "true"); + const [first, ...rest] = batchEligibilityData; + if (rest.length > 0) { + sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest)); + } + prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck); + }; + const handleEligibilityAndAppointment = async (targetDate?: string) => { if (!eligibilityIdData) return; const dateLabel = targetDate @@ -896,6 +908,14 @@ export function ChatbotButton() { Check All ({batchEligibilityData.length} patients) + diff --git a/apps/Frontend/src/pages/insurance-status-page.tsx b/apps/Frontend/src/pages/insurance-status-page.tsx index cbcad7db..3f23137b 100755 --- a/apps/Frontend/src/pages/insurance-status-page.tsx +++ b/apps/Frontend/src/pages/insurance-status-page.tsx @@ -745,22 +745,31 @@ export default function InsuranceStatusPage() { // Patient was just created/updated in DB by the Selenium worker before this is called. const tryAppointmentFromChatbot = async (): Promise => { try { + // Single-patient flow const raw = sessionStorage.getItem("chatbot_appt_after_eligibility"); - if (!raw) return; - const { memberId: storedMemberId, date: storedDate } = JSON.parse(raw); - sessionStorage.removeItem("chatbot_appt_after_eligibility"); - if (!storedMemberId) return; + // Batch flow: create appointment for the current patient after each eligibility check + const isBatch = sessionStorage.getItem("chatbot_batch_appt_after_eligibility") === "true"; - const lookupRes = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(storedMemberId)}`); + const useMemberId = raw ? JSON.parse(raw).memberId : isBatch ? memberId : null; + const useDate = raw ? JSON.parse(raw).date : null; + if (raw) sessionStorage.removeItem("chatbot_appt_after_eligibility"); + // Clean up batch flag only when no more patients in queue + if (isBatch && !sessionStorage.getItem("chatbot_eligibility_queue")) { + sessionStorage.removeItem("chatbot_batch_appt_after_eligibility"); + } + + if (!useMemberId) return; + + const lookupRes = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(useMemberId)}`); if (!lookupRes.ok) return; const patient = await lookupRes.json(); if (!patient?.id) return; - const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id, date: storedDate ?? undefined }); + const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id, date: useDate ?? undefined }); const apptData = await apptRes.json(); if (apptRes.ok) { - const scheduledOn = storedDate - ? new Date(storedDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) + const scheduledOn = useDate + ? new Date(useDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "today"; toast({ title: "Appointment created",