feat: batch eligibility by patient name + "Check All & Appointment Today" option

- 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 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-19 23:32:05 -04:00
parent e081f32648
commit 60689e58f6
4 changed files with 136 additions and 8 deletions

View File

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

View File

@@ -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<ChatResponse> {
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(

View File

@@ -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() {
<Stethoscope className="h-3 w-3 mr-1" />
Check All ({batchEligibilityData.length} patients)
</Button>
<Button
size="sm"
className="w-full h-8 text-xs bg-green-600 hover:bg-green-700 text-white"
onClick={handleBatchEligibilityAndAppointment}
>
<Calendar className="h-3 w-3 mr-1" />
Check All & Appointment Today
</Button>
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Cancel
</Button>

View File

@@ -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<void> => {
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",