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",