feat: batch eligibility, batch claim, and batch check+claim from AI chat

- Add batch_eligibility, batch_claim, and batch_check_and_claim intents
  to AI classifier so multiple patients can be processed one by one
- Add queue processing on insurance-status and claims pages to auto-start
  the next patient after each check/claim completes
- Make patient schema firstName, lastName, phone optional so patients can
  be created with just member ID + DOB from eligibility checks
- Cancel buttons now preserve chat history instead of clearing it
- Patient-found card shows Check Eligibility, Eligibility & Appointment
  Today, and Cancel buttons
- Claim service date asks user to pick between latest appointment and
  today when they differ
- Login page subtitle styled with animated gradient

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-18 23:41:56 -04:00
parent a2e5c157ad
commit a52ff2d723
9 changed files with 710 additions and 66 deletions

View File

@@ -5,6 +5,9 @@ import { getLlm, type AiProvider } from "./llm-factory";
export type InternalChatIntent = 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_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 | "check_and_claim" // eligibility + claim procedures
| "find_patient" // look up patient record only | "find_patient" // look up patient record only
| "schedule_appointment" // add patient to today's (or specified) schedule | "schedule_appointment" // add patient to today's (or specified) schedule
@@ -21,6 +24,10 @@ export interface ChatClassification {
patientName?: string; // for check_eligibility / find_patient / schedule_appointment patientName?: string; // for check_eligibility / find_patient / schedule_appointment
memberId?: string; // for eligibility_by_id / check_and_claim memberId?: string; // for eligibility_by_id / check_and_claim
dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY) dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY)
// --- batch eligibility (multiple patients) ---
patients?: { memberId: string; dob: string }[]; // for batch_eligibility
// --- batch claim (same procedures for multiple patients by name) ---
patientNames?: string[]; // for batch_claim
// --- insurance hint (only if explicitly stated in the message) --- // --- insurance hint (only if explicitly stated in the message) ---
insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA" insuranceHint?: string; // raw text, e.g. "masshealth", "BCBS", "CCA"
// --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") --- // --- rendering/treating provider (only if explicitly stated, e.g. "with provider Kai Gao") ---
@@ -48,6 +55,8 @@ Respond ONLY with valid JSON (no markdown fences):
"patientName": "<full name if mentioned by name>", "patientName": "<full name if mentioned by name>",
"memberId": "<member/insurance ID if given explicitly or found in history>", "memberId": "<member/insurance ID if given explicitly or found in history>",
"dob": "<date of birth in MM/DD/YYYY if given explicitly or found in history>", "dob": "<date of birth in MM/DD/YYYY if given explicitly or found in history>",
"patients": [{"memberId": "<id>", "dob": "<MM/DD/YYYY>"}, ...],
"patientNames": ["<name1>", "<name2>", ...],
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>", "insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>", "renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
"procedureNames": ["<raw procedure name>", ...], "procedureNames": ["<raw procedure name>", ...],
@@ -61,12 +70,27 @@ Omit any field that is not present in the message or history.
Intents: Intents:
- check_eligibility : user wants to check insurance for a patient identified by NAME only - check_eligibility : user wants to check insurance for a patient identified by NAME only
e.g. "check Maria Jesus", "verify insurance for John Smith" e.g. "check Maria Jesus", "verify insurance for John Smith"
- eligibility_by_id : user provides a member ID and date of birth (no patient name) - eligibility_by_id : user provides a SINGLE member ID and date of birth (no patient name)
e.g. "check masshealth for 100xxxx, 10/10/1988" e.g. "check masshealth for 100xxxx, 10/10/1988"
ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date ALSO use this when user wants to check eligibility AND schedule/add an appointment on a date
e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026" e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026"
e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026" e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026"
In these cases set appointmentDate to the mentioned date (YYYY-MM-DD) In these cases set appointmentDate to the mentioned date (YYYY-MM-DD)
- batch_eligibility : user provides MULTIPLE member IDs with dates of birth in one message
e.g. "check mh for 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.
Put each pair into the "patients" array. Also set insuranceHint if stated.
- 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"
Use this ONLY when procedures AND two or more patient names are given.
Put each patient name into the "patientNames" array. Put procedure names in "procedureNames".
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000 and claim D0120 D1110 for them"
Use this when TWO OR MORE memberId+dob pairs are given WITH procedures.
Put each pair into "patients" array. Put procedure names in "procedureNames".
- check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims - check_and_claim : user wants to check eligibility AND submit PROCEDURES/BILLING as claims
e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning" e.g. "check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning"
e.g. "check Maria Jesus and claim D0120 D1110" e.g. "check Maria Jesus and claim D0120 D1110"

View File

@@ -45,6 +45,9 @@ export interface ChatResponse {
| "show_patient" | "show_patient"
| "check_eligibility_prefill" | "check_eligibility_prefill"
| "eligibility_id_ready" | "eligibility_id_ready"
| "batch_eligibility_ready"
| "batch_claim_ready"
| "batch_check_and_claim_ready"
| "check_and_claim_ready" | "check_and_claim_ready"
| "need_insurance_clarification" | "need_insurance_clarification"
| "appointment_created" | "appointment_created"
@@ -314,12 +317,30 @@ export async function runInternalChatWorkflow(
return await handleEligibilityById(classification, storage); return await handleEligibilityById(classification, storage);
} }
// ── Batch eligibility (multiple patients) ─────────────────────────────────
if (intent === "batch_eligibility") {
return await handleBatchEligibility(classification, storage);
}
// ── Check eligibility + claim procedures ────────────────────────────────── // ── Check eligibility + claim procedures ──────────────────────────────────
if (intent === "check_and_claim") { if (intent === "check_and_claim") {
return await handleCheckAndClaim(classification, storage, customAliases); return await handleCheckAndClaim(classification, storage, customAliases);
} }
// ── Batch check & claim (eligibility + claim for multiple patients) ────────
if (intent === "batch_check_and_claim") {
return await handleBatchCheckAndClaim(classification, storage, customAliases);
}
// ── Batch claim (same procedures for multiple patients) ───────────────────
if (intent === "batch_claim") {
return await handleBatchClaim(classification, storage, customAliases);
}
// ── Claim only (no eligibility check) ───────────────────────────────────── // ── Claim only (no eligibility check) ─────────────────────────────────────
if (intent === "claim_only") { if (intent === "claim_only") {
@@ -436,6 +457,266 @@ async function handleEligibilityById(
}; };
} }
// ─── batch_eligibility ───────────────────────────────────────────────────────
async function handleBatchEligibility(
c: ChatClassification,
storage: StorageLike
): Promise<ChatResponse> {
const pairs = c.patients ?? [];
if (pairs.length < 2) {
// Fallback to single if somehow only 1 pair
if (pairs.length === 1) {
return await handleEligibilityById(
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "eligibility_by_id" },
storage
);
}
return { reply: "Please provide at least two member ID + DOB pairs to batch-check." };
}
const resolved: {
memberId: string;
dob: string;
siteKey: string;
autoCheck: string;
patient: ResolvedPatient | null;
}[] = [];
for (const { memberId, dob } of pairs) {
const id = memberId?.trim();
const d = dob?.trim();
if (!id || !d) continue;
const existing = await findPatientByMemberId(id, d, storage);
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
if (!resolvedDob) continue;
const siteKey = resolveSiteKey(
patient?.insuranceProvider ?? null,
c.insuranceHint ?? null
) ?? "MH";
resolved.push({
memberId: id,
dob: resolvedDob,
siteKey,
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
patient,
});
}
if (resolved.length === 0) {
return { reply: "Could not parse any valid member ID + DOB pairs." };
}
const labels = resolved.map((r) => {
const name = r.patient
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
: `ID ${r.memberId}`;
return name;
});
return {
reply: `Ready to check eligibility for ${resolved.length} patients: ${labels.join(", ")}.`,
action: "batch_eligibility_ready",
actionData: { queue: resolved },
};
}
// ─── batch_check_and_claim ────────────────────────────────────────────────────
async function handleBatchCheckAndClaim(
c: ChatClassification,
storage: StorageLike,
customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
const pairs = c.patients ?? [];
if (pairs.length < 2) {
if (pairs.length === 1) {
return await handleCheckAndClaim(
{ ...c, memberId: pairs[0]!.memberId, dob: pairs[0]!.dob, intent: "check_and_claim" },
storage,
customAliases
);
}
return { reply: "Please provide at least two member ID + DOB pairs." };
}
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
if (procedureNames.length === 0) {
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
}
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
const matched = cdtResults.filter((r) => r.code !== null);
const unmatched = cdtResults.filter((r) => r.code === null);
if (unmatched.length > 0) {
const phrases = unmatched.map((r) => r.input);
return {
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
action: "need_cdt_clarification",
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
};
}
const resolved: {
memberId: string;
dob: string;
siteKey: string;
autoCheck: string;
patient: ResolvedPatient | null;
}[] = [];
for (const { memberId, dob } of pairs) {
const id = memberId?.trim();
const d = dob?.trim();
if (!id || !d) continue;
const existing = await findPatientByMemberId(id, d, storage);
const patient: ResolvedPatient | null = existing ? patientToResult(existing) : null;
const resolvedDob = d ?? patient?.dateOfBirth ?? null;
if (!resolvedDob) continue;
const siteKey = resolveSiteKey(
patient?.insuranceProvider ?? null,
c.insuranceHint ?? null
) ?? "MH";
resolved.push({
memberId: id,
dob: resolvedDob,
siteKey,
autoCheck: siteKeyToAutoCheck(siteKey, resolvedDob),
patient,
});
}
if (resolved.length === 0) {
return { reply: "Could not parse any valid member ID + DOB pairs." };
}
const labels = resolved.map((r) => {
const name = r.patient
? `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
: `ID ${r.memberId}`;
return name;
});
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
return {
reply: `Ready to check eligibility and claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`,
action: "batch_check_and_claim_ready",
actionData: {
queue: resolved,
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
renderingProvider: c.renderingProvider ?? null,
},
};
}
// ─── batch_claim ─────────────────────────────────────────────────────────────
async function handleBatchClaim(
c: ChatClassification,
storage: StorageLike,
customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
const names = c.patientNames ?? [];
if (names.length < 2) {
if (names.length === 1) {
return await handleClaimOnly(
{ ...c, patientName: names[0], intent: "claim_only" },
storage,
customAliases
);
}
return { reply: "Please include at least two patient names to batch-claim." };
}
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
if (procedureNames.length === 0) {
return { reply: "Please specify which procedures to claim (e.g. perio exam, adult prophy)." };
}
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
const matched = cdtResults.filter((r) => r.code !== null);
const unmatched = cdtResults.filter((r) => r.code === null);
if (unmatched.length > 0) {
const phrases = unmatched.map((r) => r.input);
return {
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
action: "need_cdt_clarification",
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
};
}
const resolved: {
patient: ResolvedPatient;
siteKey: string;
serviceDate: string | null;
appointmentId: number | null;
}[] = [];
const notFound: 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();
const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
if (d1120Warning) return d1120Warning;
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
let serviceDate: string | null = c.appointmentDate ?? null;
let appointmentId: number | null = null;
if (!serviceDate) {
const now = new Date();
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
}
resolved.push({ patient, siteKey, serviceDate, appointmentId });
}
if (notFound.length > 0 && resolved.length === 0) {
return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
}
const labels = resolved.map((r) =>
`${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim()
);
const codeList = matched.map((r) => `${r.code} (${r.description})`).join(", ");
let reply = `Ready to claim ${codeList} for ${resolved.length} patients: ${labels.join(", ")}.`;
if (notFound.length > 0) {
reply += ` Could not find: ${notFound.join(", ")}.`;
}
return {
reply,
action: "batch_claim_ready",
actionData: {
queue: resolved.map((r) => ({
patient: r.patient,
siteKey: r.siteKey,
serviceDate: r.serviceDate,
appointmentId: r.appointmentId,
})),
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
renderingProvider: c.renderingProvider ?? null,
},
};
}
// ─── check_and_claim ───────────────────────────────────────────────────────── // ─── check_and_claim ─────────────────────────────────────────────────────────
async function handleCheckAndClaim( async function handleCheckAndClaim(
@@ -624,53 +905,44 @@ async function handleClaimOnly(
let appointmentId: number | null = null; let appointmentId: number | null = null;
if (!serviceDate) { if (!serviceDate) {
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const appts = await storage.getAppointmentsByPatientId(patient.id); const appts = await storage.getAppointmentsByPatientId(patient.id);
const sorted = appts.sort((a: any, b: any) => const sorted = appts.sort((a: any, b: any) =>
new Date(b.date).getTime() - new Date(a.date).getTime() new Date(b.date).getTime() - new Date(a.date).getTime()
); );
if (sorted.length >= 2) { if (sorted.length > 0) {
const d1 = new Date(sorted[0].date).getTime(); const rawDate = new Date(sorted[0].date);
const d2 = new Date(sorted[1].date).getTime(); const latestStr = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
const diffDays = Math.abs(d1 - d2) / (1000 * 60 * 60 * 24);
if (diffDays < 7) { if (latestStr === todayStr) {
// Use UTC methods to avoid local-timezone day-shift on midnight UTC dates serviceDate = todayStr;
appointmentId = sorted[0].id ?? null;
} else {
const fmtUTC = (a: any) => { const fmtUTC = (a: any) => {
const d = new Date(a.date); const d = new Date(a.date);
return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`; return `${String(d.getUTCMonth() + 1).padStart(2, "0")}/${String(d.getUTCDate()).padStart(2, "0")}/${d.getUTCFullYear()}`;
}; };
const isoUTC = (a: any) => { const todayLabel = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}/${now.getFullYear()}`;
const d = new Date(a.date);
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
};
return { return {
reply: `Found two appointments close together for ${fullName}: ${fmtUTC(sorted[0])} and ${fmtUTC(sorted[1])}. Which date should I use for the claim?`, reply: `Which service date for ${fullName}?`,
action: "need_appointment_selection", action: "need_appointment_selection",
actionData: { actionData: {
patient, patient,
siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH", siteKey: resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH",
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })), matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
options: [ options: [
{ label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: isoUTC(sorted[0]) }, { label: fmtUTC(sorted[0]), appointmentId: sorted[0].id, serviceDate: latestStr },
{ label: fmtUTC(sorted[1]), appointmentId: sorted[1].id, serviceDate: isoUTC(sorted[1]) }, { label: `${todayLabel} (Today)`, appointmentId: null, serviceDate: todayStr },
], ],
}, },
}; };
} }
} else {
serviceDate = todayStr;
} }
if (sorted.length > 0) {
const rawDate = new Date(sorted[0].date);
serviceDate = `${rawDate.getUTCFullYear()}-${String(rawDate.getUTCMonth() + 1).padStart(2, "0")}-${String(rawDate.getUTCDate()).padStart(2, "0")}`;
appointmentId = sorted[0].id ?? null;
}
}
if (!serviceDate) {
// No appointment on file and no date in message — default to today
const now = new Date();
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
} }
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH"; const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";

View File

@@ -137,7 +137,6 @@ export async function createOrUpdatePatientByInsuranceId(options: {
updates.firstName = incomingFirst; updates.firstName = incomingFirst;
if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast) if (incomingLast && String(patient.lastName ?? "").trim() !== incomingLast)
updates.lastName = incomingLast; updates.lastName = incomingLast;
// Store DOB if not already set
if (dobDate && !patient.dateOfBirth) updates.dateOfBirth = dobDate; if (dobDate && !patient.dateOfBirth) updates.dateOfBirth = dobDate;
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates); console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
@@ -163,15 +162,8 @@ export async function createOrUpdatePatientByInsuranceId(options: {
try { try {
patientData = insertPatientSchema.parse(createPayload); patientData = insertPatientSchema.parse(createPayload);
} catch (e1) { } catch (e1) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 1):`, e1); console.warn(`[createOrUpdatePatient] schema parse failed:`, e1);
const safePayload = { ...createPayload }; patientData = createPayload as InsertPatient;
delete safePayload.dateOfBirth;
try {
patientData = insertPatientSchema.parse(safePayload);
} catch (e2) {
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 2):`, e2);
patientData = safePayload as InsertPatient;
}
} }
try { try {

View File

@@ -26,11 +26,14 @@ type Step =
| "ai-loading" | "ai-loading"
| "patient-found" | "patient-found"
| "eligibility-id-ready" | "eligibility-id-ready"
| "batch-eligibility-ready"
| "check-and-claim-ready" | "check-and-claim-ready"
| "need-insurance-clarification" | "need-insurance-clarification"
| "need-appointment-selection" | "need-appointment-selection"
| "need-cdt-clarification" | "need-cdt-clarification"
| "claim-ready" | "claim-ready"
| "batch-claim-ready"
| "batch-check-and-claim-ready"
| "preauth-ready"; | "preauth-ready";
interface Message { interface Message {
@@ -160,6 +163,7 @@ export function ChatbotButton() {
const [freeTextInput, setFreeTextInput] = useState(""); const [freeTextInput, setFreeTextInput] = useState("");
const [patientResult, setPatientResult] = useState<PatientResult | null>(null); const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null); const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null);
const [batchEligibilityData, setBatchEligibilityData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[] | null>(null);
const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(null); const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(null);
const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null); const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null);
const [apptSelectionData, setApptSelectionData] = useState<{ const [apptSelectionData, setApptSelectionData] = useState<{
@@ -181,6 +185,16 @@ export function ChatbotButton() {
appointmentId: number | null; appointmentId: number | null;
renderingProvider: string | null; renderingProvider: string | null;
} | null>(null); } | null>(null);
const [batchCheckAndClaimData, setBatchCheckAndClaimData] = useState<{
queue: { memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[];
matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
renderingProvider: string | null;
} | null>(null);
const [batchClaimData, setBatchClaimData] = useState<{
queue: { patient: PatientResult; siteKey: string; serviceDate: string | null; appointmentId: number | null }[];
matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
renderingProvider: string | null;
} | null>(null);
const [preauthReadyData, setPreauthReadyData] = useState<{ const [preauthReadyData, setPreauthReadyData] = useState<{
patient: PatientResult | null; patient: PatientResult | null;
matchedCodes: { code: string; description: string; toothNumber?: string }[]; matchedCodes: { code: string; description: string; toothNumber?: string }[];
@@ -257,11 +271,14 @@ export function ChatbotButton() {
setFreeTextInput(""); setFreeTextInput("");
setPatientResult(null); setPatientResult(null);
setEligibilityIdData(null); setEligibilityIdData(null);
setBatchEligibilityData(null);
setCheckAndClaimData(null); setCheckAndClaimData(null);
setClarificationData(null); setClarificationData(null);
setApptSelectionData(null); setApptSelectionData(null);
setCdtClarificationData(null); setCdtClarificationData(null);
setClaimReadyData(null); setClaimReadyData(null);
setBatchClaimData(null);
setBatchCheckAndClaimData(null);
setPreauthReadyData(null); setPreauthReadyData(null);
setPendingFiles([]); setPendingFiles([]);
}; };
@@ -326,18 +343,30 @@ export function ChatbotButton() {
}; };
const handleEligibilityFromPatient = () => { const handleEligibilityFromPatient = () => {
if (!patientResult) return; if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
addMsg("user", "Check eligibility now"); addMsg("user", "Check eligibility now");
addMsg("bot", "Opening the eligibility check page..."); addMsg("bot", "Opening the eligibility check page...");
if (patientResult.insuranceId && patientResult.dateOfBirth) { prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ };
memberId: patientResult.insuranceId,
dob: patientResult.dateOfBirth, const handleEligibilityAndAppointmentFromPatient = async () => {
autoCheck: getAutoCheck(patientResult.dateOfBirth), if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
})); addMsg("user", "Check eligibility & add to schedule (today)");
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill")); addMsg("bot", "Creating appointment for today...", true);
try {
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
patientId: patientResult.id,
});
const data = await res.json();
if (!res.ok) {
replaceLastMsg(data.message ?? "Could not create appointment.");
return;
}
replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) — opening eligibility check page...`);
} catch {
replaceLastMsg("Could not create appointment — opening eligibility check page...");
} }
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600); prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
}; };
const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => { const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => {
@@ -354,6 +383,17 @@ export function ChatbotButton() {
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
}; };
const handleBatchEligibilityRun = () => {
if (!batchEligibilityData || batchEligibilityData.length === 0) return;
addMsg("user", `Check all ${batchEligibilityData.length} patients`);
addMsg("bot", `Checking ${batchEligibilityData.length} patients one by one...`);
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
@@ -445,6 +485,12 @@ export function ChatbotButton() {
return; return;
} }
if (data.action === "batch_eligibility_ready" && data.actionData?.queue) {
setBatchEligibilityData(data.actionData.queue);
setStep("batch-eligibility-ready");
return;
}
if (data.action === "eligibility_id_ready" && data.actionData) { if (data.action === "eligibility_id_ready" && data.actionData) {
setEligibilityIdData({ setEligibilityIdData({
memberId: data.actionData.memberId, memberId: data.actionData.memberId,
@@ -510,6 +556,26 @@ export function ChatbotButton() {
return; return;
} }
if (data.action === "batch_check_and_claim_ready" && data.actionData) {
setBatchCheckAndClaimData({
queue: data.actionData.queue ?? [],
matchedCodes: data.actionData.matchedCodes ?? [],
renderingProvider: data.actionData.renderingProvider ?? null,
});
setStep("batch-check-and-claim-ready");
return;
}
if (data.action === "batch_claim_ready" && data.actionData) {
setBatchClaimData({
queue: data.actionData.queue ?? [],
matchedCodes: data.actionData.matchedCodes ?? [],
renderingProvider: data.actionData.renderingProvider ?? null,
});
setStep("batch-claim-ready");
return;
}
if (data.action === "claim_only_ready" && data.actionData) { if (data.action === "claim_only_ready" && data.actionData) {
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData; const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
setClaimReadyData({ setClaimReadyData({
@@ -556,7 +622,10 @@ export function ChatbotButton() {
step === "ai-loading" || step === "ai-loading" ||
step === "patient-found" || step === "patient-found" ||
step === "eligibility-id-ready" || step === "eligibility-id-ready" ||
step === "batch-eligibility-ready" ||
step === "check-and-claim-ready" || step === "check-and-claim-ready" ||
step === "batch-claim-ready" ||
step === "batch-check-and-claim-ready" ||
step === "need-insurance-clarification" || step === "need-insurance-clarification" ||
step === "need-appointment-selection"; step === "need-appointment-selection";
@@ -684,7 +753,7 @@ export function ChatbotButton() {
> >
Continue <ChevronRight className="h-3 w-3 ml-1" /> Continue <ChevronRight className="h-3 w-3 ml-1" />
</Button> </Button>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}> <Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -726,19 +795,29 @@ export function ChatbotButton() {
{patientResult.dateOfBirth && ( {patientResult.dateOfBirth && (
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p> <p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
)} )}
<div className="flex gap-2 pt-1"> <div className="flex flex-col gap-2 pt-1">
{patientResult.insuranceId && ( {patientResult.insuranceId && patientResult.dateOfBirth && (
<Button <>
size="sm" <Button
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90" size="sm"
onClick={handleEligibilityFromPatient} className="w-full h-8 text-xs bg-primary hover:bg-primary/90"
> onClick={handleEligibilityFromPatient}
<Stethoscope className="h-3 w-3 mr-1" /> >
Check Eligibility <Stethoscope className="h-3 w-3 mr-1" />
</Button> Check Eligibility
</Button>
<Button
size="sm"
className="w-full h-8 text-xs bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={handleEligibilityAndAppointmentFromPatient}
>
<Calendar className="h-3 w-3 mr-1" />
Eligibility &amp; Appointment Today
</Button>
</>
)} )}
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}> <Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Done Cancel
</Button> </Button>
</div> </div>
</div> </div>
@@ -785,7 +864,39 @@ export function ChatbotButton() {
{new Date(eligibilityIdData.appointmentDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} {new Date(eligibilityIdData.appointmentDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
</Button> </Button>
)} )}
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={reset}> <Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Cancel
</Button>
</div>
</div>
)}
{/* Batch eligibility ready */}
{step === "batch-eligibility-ready" && batchEligibilityData && (
<div className="bg-indigo-50 border border-indigo-200 rounded-xl p-3 space-y-2">
<p className="text-xs font-semibold text-indigo-800">
{batchEligibilityData.length} patients to check:
</p>
{batchEligibilityData.map((item, i) => {
const name = item.patient
? `${item.patient.firstName ?? ""} ${item.patient.lastName ?? ""}`.trim()
: `ID: ${item.memberId}`;
return (
<p key={i} className="text-xs text-indigo-600 pl-2">
{i + 1}. {name} DOB: {item.dob}
</p>
);
})}
<div className="flex flex-col gap-2 pt-1">
<Button
size="sm"
className="w-full h-8 text-xs bg-indigo-600 hover:bg-indigo-700 text-white"
onClick={handleBatchEligibilityRun}
>
<Stethoscope className="h-3 w-3 mr-1" />
Check All ({batchEligibilityData.length} patients)
</Button>
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -820,7 +931,7 @@ export function ChatbotButton() {
<Stethoscope className="h-3 w-3 mr-1" /> <Stethoscope className="h-3 w-3 mr-1" />
Check &amp; Claim Check &amp; Claim
</Button> </Button>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}> <Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -884,7 +995,7 @@ export function ChatbotButton() {
</button> </button>
))} ))}
</div> </div>
<Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={reset}> <Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={resetStep}>
Cancel Cancel
</Button> </Button>
</div> </div>
@@ -931,6 +1042,135 @@ export function ChatbotButton() {
</div> </div>
)} )}
{/* Batch check & claim ready */}
{step === "batch-check-and-claim-ready" && batchCheckAndClaimData && (
<div className="bg-purple-50 border border-purple-200 rounded-xl p-3 space-y-2">
<p className="text-xs font-semibold text-purple-800">
Check &amp; Claim for {batchCheckAndClaimData.queue.length} patients:
</p>
{batchCheckAndClaimData.queue.map((item, i) => {
const name = item.patient
? `${item.patient.firstName ?? ""} ${item.patient.lastName ?? ""}`.trim()
: `ID: ${item.memberId}`;
return (
<p key={i} className="text-xs text-purple-600 pl-2">
{i + 1}. {name} DOB: {item.dob}
</p>
);
})}
{batchCheckAndClaimData.matchedCodes.length > 0 && (
<div className="space-y-0.5 pt-0.5">
<p className="text-xs font-medium text-purple-700">Claim after ACTIVE:</p>
{batchCheckAndClaimData.matchedCodes.map((c) => (
<p key={c.code} className="text-xs text-gray-700 pl-2">
<span className="font-medium">{c.code}</span> {c.description}
</p>
))}
</div>
)}
<div className="flex flex-col gap-2 pt-1">
<Button
size="sm"
className="w-full h-8 text-xs bg-purple-600 hover:bg-purple-700 text-white"
onClick={() => {
const { queue, matchedCodes, renderingProvider } = batchCheckAndClaimData;
addMsg("user", `Check & claim all ${queue.length} patients`);
addMsg("bot", `Checking eligibility and claiming for ${queue.length} patients one by one...`);
const [first, ...rest] = queue;
if (rest.length > 0) {
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({
remaining: rest,
matchedCodes,
renderingProvider,
}));
}
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
codes: matchedCodes,
siteKey: first!.siteKey,
patientId: first!.patient?.id ?? null,
memberId: first!.memberId,
dob: first!.dob,
serviceDate: null,
renderingProvider,
}));
prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck);
}}
>
<Stethoscope className="h-3 w-3 mr-1" />
Check &amp; Claim All ({batchCheckAndClaimData.queue.length} patients)
</Button>
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Cancel
</Button>
</div>
</div>
)}
{/* Batch claim ready */}
{step === "batch-claim-ready" && batchClaimData && (
<div className="bg-orange-50 border border-orange-200 rounded-xl p-3 space-y-2">
<p className="text-xs font-semibold text-orange-800">
Claim for {batchClaimData.queue.length} patients:
</p>
{batchClaimData.queue.map((item, i) => (
<p key={i} className="text-xs text-orange-600 pl-2">
{i + 1}. {item.patient.firstName} {item.patient.lastName}
</p>
))}
{batchClaimData.matchedCodes.length > 0 && (
<div className="space-y-0.5 pt-0.5">
<p className="text-xs font-medium text-orange-700">Procedures:</p>
{batchClaimData.matchedCodes.map((c) => (
<p key={c.code} className="text-xs text-gray-700 pl-2">
<span className="font-medium">{c.code}</span> {c.description}
</p>
))}
</div>
)}
<div className="flex flex-col gap-2 pt-1">
<Button
size="sm"
className="w-full h-8 text-xs bg-orange-600 hover:bg-orange-700 text-white"
onClick={() => {
const { queue, matchedCodes, renderingProvider } = batchClaimData;
addMsg("user", `Claim all ${queue.length} patients`);
addMsg("bot", `Claiming ${queue.length} patients one by one...`);
const [first, ...rest] = queue;
if (rest.length > 0) {
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({
remaining: rest,
matchedCodes,
renderingProvider,
}));
}
if (first!.patient.id && matchedCodes.length > 0) {
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
codes: matchedCodes,
siteKey: first!.siteKey,
serviceDate: first!.serviceDate,
autoSubmit: true,
renderingProvider: renderingProvider ?? null,
dob: first!.patient.dateOfBirth ?? null,
}));
}
setChatbotPendingFiles(pendingFiles);
markJobStarted();
const url = first!.appointmentId
? `/claims?appointmentId=${first!.appointmentId}`
: `/claims?newPatient=${first!.patient.id}`;
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
}}
>
<FileText className="h-3 w-3 mr-1" />
Claim All ({batchClaimData.queue.length} patients)
</Button>
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
Cancel
</Button>
</div>
</div>
)}
{/* Claim ready — confirm before submitting */} {/* Claim ready — confirm before submitting */}
{step === "claim-ready" && claimReadyData && (() => { {step === "claim-ready" && claimReadyData && (() => {
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-"); const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");

View File

@@ -110,3 +110,13 @@
min-width: 6rem; /* Prevent shrinking */ min-width: 6rem; /* Prevent shrinking */
appearance: none; /* Removes native styling */ appearance: none; /* Removes native styling */
} }
@keyframes gradient-flash {
0% { background-position: 100% 50%; }
50% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
.animate-gradient-flash {
animation: gradient-flash 6s ease-in-out infinite;
}

View File

@@ -90,8 +90,9 @@ export default function AuthPage() {
<h1 className="text-3xl font-bold text-primary mb-3 tracking-tight"> <h1 className="text-3xl font-bold text-primary mb-3 tracking-tight">
My Dental Office Management My Dental Office Management
</h1> </h1>
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-gray-400"> <p className="text-sm font-semibold tracking-[0.15em] bg-clip-text text-transparent animate-gradient-flash"
Driven by multiple AI agents style={{ backgroundImage: "linear-gradient(90deg, #7c3aed, #d946ef, #06b6d4, #7c3aed)", backgroundSize: "200% 100%" }}>
Driven By Multiple AI Agents
</p> </p>
</div> </div>

View File

@@ -911,6 +911,79 @@ export default function ClaimsPage() {
} }
} }
} catch {} } catch {}
// If a chatbot batch check+claim queue is pending, navigate to eligibility for the next patient
try {
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
if (raw) {
const parsed = JSON.parse(raw);
const remaining = parsed?.remaining as any[] | undefined;
const matchedCodes = parsed?.matchedCodes ?? [];
const renderingProvider = parsed?.renderingProvider ?? null;
if (remaining && remaining.length > 0) {
const [next, ...rest] = remaining;
if (rest.length > 0) {
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
} else {
sessionStorage.removeItem("chatbot_check_claim_queue");
}
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
codes: matchedCodes,
siteKey: next.siteKey,
patientId: next.patient?.id ?? null,
memberId: next.memberId,
dob: next.dob,
serviceDate: null,
renderingProvider,
}));
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
memberId: next.memberId,
dob: next.dob,
autoCheck: next.autoCheck,
}));
setTimeout(() => {
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
setWouterLocation("/insurance-status");
}, 500);
return;
}
sessionStorage.removeItem("chatbot_check_claim_queue");
}
} catch {}
// If a chatbot batch claim queue is pending, open the next patient
try {
const raw = sessionStorage.getItem("chatbot_claim_queue");
if (raw) {
const parsed = JSON.parse(raw);
const remaining = parsed?.remaining as any[] | undefined;
const matchedCodes = parsed?.matchedCodes ?? [];
const renderingProvider = parsed?.renderingProvider ?? null;
if (remaining && remaining.length > 0) {
const [next, ...rest] = remaining;
if (rest.length > 0) {
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
} else {
sessionStorage.removeItem("chatbot_claim_queue");
}
if (next.patient?.id && matchedCodes.length > 0) {
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
codes: matchedCodes,
siteKey: next.siteKey,
serviceDate: next.serviceDate,
autoSubmit: true,
renderingProvider,
dob: next.patient.dateOfBirth ?? null,
}));
}
setTimeout(() => {
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
}, 500);
return;
}
sessionStorage.removeItem("chatbot_claim_queue");
}
} catch {}
}; };
// Pre Auth section // Pre Auth section

View File

@@ -385,6 +385,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`); setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
} catch (error: any) { } catch (error: any) {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" })); dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" }); toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
@@ -574,6 +575,7 @@ export default function InsuranceStatusPage() {
if (claimed) return; if (claimed) return;
setSelectedPatient(null); setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
processNextInQueue();
// Open all PDFs side by side in the modal // Open all PDFs side by side in the modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) { if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId) {
@@ -643,6 +645,7 @@ export default function InsuranceStatusPage() {
setSelectedPatient(null); setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
processNextInQueue();
// Open 4-panel modal // Open 4-panel modal
if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) { if (jobResult.pdfFileId || jobResult.memberDetailsPdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
@@ -773,6 +776,29 @@ export default function InsuranceStatusPage() {
} catch {} } catch {}
}; };
const processNextInQueue = () => {
try {
const raw = sessionStorage.getItem("chatbot_eligibility_queue");
if (!raw) return;
const queue = JSON.parse(raw) as { memberId: string; dob: string; autoCheck: string }[];
if (!queue.length) {
sessionStorage.removeItem("chatbot_eligibility_queue");
return;
}
const [next, ...rest] = queue;
if (rest.length > 0) {
sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest));
} else {
sessionStorage.removeItem("chatbot_eligibility_queue");
}
toast({ title: `${rest.length + 1} patient${rest.length > 0 ? "s" : ""} remaining`, description: `Starting next: ${next!.memberId}` });
setTimeout(() => {
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId: next!.memberId, dob: next!.dob, autoCheck: next!.autoCheck }));
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
}, 1500);
} catch {}
};
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll // Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
@@ -1018,6 +1044,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`); setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
}} }}
/> />
</div> </div>
@@ -1039,6 +1066,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`); setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
}} }}
/> />
</div> </div>
@@ -1057,6 +1085,7 @@ export default function InsuranceStatusPage() {
fallbackFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`, fallbackFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`,
); );
setPreviewOpen(true); setPreviewOpen(true);
processNextInQueue();
}} }}
/> />
</div> </div>
@@ -1081,6 +1110,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`); setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
}} }}
/> />
</div> </div>
@@ -1102,6 +1132,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`); setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
}} }}
/> />
</div> </div>
@@ -1123,6 +1154,7 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`); setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`);
setPreviewOpen(true); setPreviewOpen(true);
} }
processNextInQueue();
}} }}
/> />
</div> </div>

View File

@@ -51,14 +51,14 @@ export const insertPatientSchema = (
createdAt: true, createdAt: true,
}) })
.extend({ .extend({
firstName: z.string().min(1, "First name is required"), firstName: z.string().optional().default(""),
lastName: z.string().min(1, "Last name is required"), lastName: z.string().optional().default(""),
dateOfBirth: z.preprocess( dateOfBirth: z.preprocess(
(val) => (val === null || val === undefined || val === "" ? undefined : val), (val) => (val === null || val === undefined || val === "" ? undefined : val),
z.coerce.date({ required_error: "Date of birth is required" }) z.coerce.date({ required_error: "Date of birth is required" })
), ),
gender: z.string().optional().nullable(), gender: z.string().optional().nullable(),
phone: z.string().min(1, "Phone number is required"), phone: z.string().optional().nullable(),
insuranceId: insuranceIdSchema, insuranceId: insuranceIdSchema,
}); });