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:
@@ -6,6 +6,7 @@ export type InternalChatIntent =
|
|||||||
| "check_eligibility" // by patient name → look up in DB
|
| "check_eligibility" // by patient name → look up in DB
|
||||||
| "eligibility_by_id" // by explicit memberId + dob (no name)
|
| "eligibility_by_id" // by explicit memberId + dob (no name)
|
||||||
| "batch_eligibility" // multiple patients by memberId + dob
|
| "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_claim" // claim same procedures for multiple patients by name
|
||||||
| "batch_check_and_claim" // eligibility + claim for multiple patients by memberId+dob
|
| "batch_check_and_claim" // eligibility + claim for multiple patients by memberId+dob
|
||||||
| "check_and_claim" // eligibility + claim procedures
|
| "check_and_claim" // eligibility + claim procedures
|
||||||
@@ -81,6 +82,11 @@ Intents:
|
|||||||
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000"
|
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.
|
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.
|
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
|
- 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. "claim perio exam and adult prophy for Jackaline and Keioson"
|
||||||
e.g. "perio exam, adult cleaning for Maria and John"
|
e.g. "perio exam, adult cleaning for Maria and John"
|
||||||
|
|||||||
@@ -323,6 +323,10 @@ export async function runInternalChatWorkflow(
|
|||||||
return await handleBatchEligibility(classification, storage);
|
return await handleBatchEligibility(classification, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (intent === "batch_eligibility_by_name") {
|
||||||
|
return await handleBatchEligibilityByName(classification, storage);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Check eligibility + claim procedures ──────────────────────────────────
|
// ── Check eligibility + claim procedures ──────────────────────────────────
|
||||||
|
|
||||||
if (intent === "check_and_claim") {
|
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 ────────────────────────────────────────────────────
|
// ─── batch_check_and_claim ────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function handleBatchCheckAndClaim(
|
async function handleBatchCheckAndClaim(
|
||||||
|
|||||||
@@ -394,6 +394,18 @@ export function ChatbotButton() {
|
|||||||
prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck);
|
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) => {
|
const handleEligibilityAndAppointment = async (targetDate?: string) => {
|
||||||
if (!eligibilityIdData) return;
|
if (!eligibilityIdData) return;
|
||||||
const dateLabel = targetDate
|
const dateLabel = targetDate
|
||||||
@@ -896,6 +908,14 @@ export function ChatbotButton() {
|
|||||||
<Stethoscope className="h-3 w-3 mr-1" />
|
<Stethoscope className="h-3 w-3 mr-1" />
|
||||||
Check All ({batchEligibilityData.length} patients)
|
Check All ({batchEligibilityData.length} patients)
|
||||||
</Button>
|
</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}>
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -745,22 +745,31 @@ export default function InsuranceStatusPage() {
|
|||||||
// Patient was just created/updated in DB by the Selenium worker before this is called.
|
// Patient was just created/updated in DB by the Selenium worker before this is called.
|
||||||
const tryAppointmentFromChatbot = async (): Promise<void> => {
|
const tryAppointmentFromChatbot = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
|
// Single-patient flow
|
||||||
const raw = sessionStorage.getItem("chatbot_appt_after_eligibility");
|
const raw = sessionStorage.getItem("chatbot_appt_after_eligibility");
|
||||||
if (!raw) return;
|
// Batch flow: create appointment for the current patient after each eligibility check
|
||||||
const { memberId: storedMemberId, date: storedDate } = JSON.parse(raw);
|
const isBatch = sessionStorage.getItem("chatbot_batch_appt_after_eligibility") === "true";
|
||||||
sessionStorage.removeItem("chatbot_appt_after_eligibility");
|
|
||||||
if (!storedMemberId) return;
|
|
||||||
|
|
||||||
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;
|
if (!lookupRes.ok) return;
|
||||||
const patient = await lookupRes.json();
|
const patient = await lookupRes.json();
|
||||||
if (!patient?.id) return;
|
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();
|
const apptData = await apptRes.json();
|
||||||
if (apptRes.ok) {
|
if (apptRes.ok) {
|
||||||
const scheduledOn = storedDate
|
const scheduledOn = useDate
|
||||||
? new Date(storedDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
? new Date(useDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
||||||
: "today";
|
: "today";
|
||||||
toast({
|
toast({
|
||||||
title: "Appointment created",
|
title: "Appointment created",
|
||||||
|
|||||||
Reference in New Issue
Block a user