feat: AI chat Eligibility & Appointment button with 15-min slots and Column B fallback

- Add "Eligibility & Appointment" button to chatbot eligibility-id-ready card
- For known patients: creates today's appointment immediately, then opens eligibility page
- For unknown patients: navigates to eligibility page; after Selenium creates the patient,
  auto-creates appointment via tryAppointmentFromChatbot on the insurance-status page
- Update MH Eligibility & Appointment button to create a today's schedule slot instead of
  navigating to the appointments page; shows PDF preview on completion
- createAppointmentToday falls back from Column A to Column B when Column A is full;
  returns column label in response so UI can display it
- Set AI-scheduled appointment duration to 15 minutes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-09 16:14:08 -04:00
parent a8ec1a21c0
commit 541d65da6d
4 changed files with 192 additions and 13 deletions

View File

@@ -299,6 +299,35 @@ export function ChatbotButton() {
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
};
const handleEligibilityAndAppointment = async () => {
if (!eligibilityIdData) return;
addMsg("user", "Check eligibility & add to today's schedule");
if (!eligibilityIdData.patient) {
// Patient not yet in DB — eligibility check will create them; flag for post-check appointment
addMsg("bot", "Running eligibility check — will add patient and create today's appointment after...");
sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId }));
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
return;
}
addMsg("bot", "Creating appointment for today...", true);
try {
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
patientId: eligibilityIdData.patient.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...`);
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
} catch {
replaceLastMsg("Could not create appointment. Please try again.");
}
};
const handleCheckAndClaimRun = () => {
if (!checkAndClaimData) return;
addMsg("user", "Run check & claim");
@@ -653,16 +682,24 @@ export function ChatbotButton() {
{eligibilityIdData.patient?.insuranceProvider && (
<p className="text-xs text-gray-500">{eligibilityIdData.patient.insuranceProvider}</p>
)}
<div className="flex gap-2 pt-1">
<div className="flex flex-col gap-2 pt-1">
<Button
size="sm"
className="flex-1 h-8 text-xs bg-primary hover:bg-primary/90"
className="w-full h-8 text-xs bg-primary hover:bg-primary/90"
onClick={handleEligibilityIdRun}
>
<Stethoscope className="h-3 w-3 mr-1" />
Check Eligibility
</Button>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={reset}>
<Button
size="sm"
className="w-full h-8 text-xs bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={handleEligibilityAndAppointment}
>
<Calendar className="h-3 w-3 mr-1" />
Eligibility &amp; Appointment
</Button>
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={reset}>
Cancel
</Button>
</div>

View File

@@ -363,6 +363,7 @@ export default function InsuranceStatusPage() {
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
void tryAppointmentFromChatbot();
if (jobResult.pdfFileId) {
setPreviewPdfId(Number(jobResult.pdfFileId));
@@ -377,7 +378,7 @@ export default function InsuranceStatusPage() {
}
};
// MH Eligibility & Appointment — check eligibility, save to DB, navigate to appointments
// MH Eligibility & Appointment — check eligibility, save to DB, create appointment for today
const handleMHEligibilityAppointmentButton = async () => {
if (!memberId || !dateOfBirth) {
toast({
@@ -399,34 +400,60 @@ export default function InsuranceStatusPage() {
setTaskStatus({
key: "eligibilityCheck",
status: "error",
message: "Insurance is inactive. Staying on Eligibility page.",
message: "Insurance is inactive. No appointment created.",
}),
);
toast({
title: "Insurance Inactive",
description: "Patient insurance is inactive. Staying on Eligibility page.",
description: "Patient insurance is inactive. No appointment was created.",
variant: "destructive",
});
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
return;
}
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
// Look up the patient (created/updated by the Selenium worker) then create today's appointment
let apptTime: string | null = null;
try {
const lookupRes = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(memberId)}`);
if (lookupRes.ok) {
const patient = await lookupRes.json();
if (patient?.id) {
const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id });
if (apptRes.ok) {
const apptData = await apptRes.json();
apptTime = apptData.startTime ? `${apptData.startTime} (${apptData.column ?? "Column A"})` : null;
}
}
}
} catch {}
dispatch(
setTaskStatus({
key: "eligibilityCheck",
status: "success",
message: "Eligibility checked and saved. Redirecting to Appointments...",
message: apptTime
? `Eligibility checked. Appointment created at ${apptTime}.`
: "Eligibility checked. Could not create appointment — please add manually.",
}),
);
toast({
title: "Eligibility checked.",
description: "Patient eligibility saved. Redirecting to Appointments.",
description: apptTime
? `Patient added to today's schedule at ${apptTime}.`
: "Eligibility saved. Could not create appointment — Column A may be full.",
variant: "default",
});
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
setLocation("/appointments");
if (jobResult.pdfFileId) {
setPreviewPdfId(Number(jobResult.pdfFileId));
setPreviewFallbackFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setPreviewOpen(true);
}
} catch (error: any) {
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" });
@@ -523,6 +550,7 @@ export default function InsuranceStatusPage() {
});
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (claimed) return;
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
@@ -686,6 +714,38 @@ export default function InsuranceStatusPage() {
return false;
};
// After any eligibility check succeeds, check if chatbot wanted an appointment created.
// Patient was just created/updated in DB by the Selenium worker before this is called.
const tryAppointmentFromChatbot = async (): Promise<void> => {
try {
const raw = sessionStorage.getItem("chatbot_appt_after_eligibility");
if (!raw) return;
const { memberId: storedMemberId } = JSON.parse(raw);
sessionStorage.removeItem("chatbot_appt_after_eligibility");
if (!storedMemberId) return;
const lookupRes = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(storedMemberId)}`);
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 });
const apptData = await apptRes.json();
if (apptRes.ok) {
toast({
title: "Appointment created",
description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} added to today's schedule at ${apptData.startTime} (${apptData.column ?? "Column A"}).`.trim(),
});
} else {
toast({
title: "Could not create appointment",
description: apptData.message ?? "Please add manually.",
variant: "destructive",
});
}
} catch {}
};
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
useEffect(() => {
const params = new URLSearchParams(window.location.search);
@@ -925,6 +985,7 @@ export default function InsuranceStatusPage() {
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={async (pdfId, fallbackFilename) => {
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (!claimed) {
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`);
@@ -945,6 +1006,7 @@ export default function InsuranceStatusPage() {
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={async (pdfId, fallbackFilename) => {
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (!claimed) {
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`);
@@ -961,7 +1023,8 @@ export default function InsuranceStatusPage() {
firstName={firstName}
lastName={lastName}
isFormIncomplete={isFormIncomplete}
onPdfReady={(pdfId, fallbackFilename) => {
onPdfReady={async (pdfId, fallbackFilename) => {
void tryAppointmentFromChatbot();
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(
fallbackFilename ?? `eligibility_bcbs_ma_${memberId}.pdf`,
@@ -985,6 +1048,7 @@ export default function InsuranceStatusPage() {
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={async (pdfId, fallbackFilename) => {
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (!claimed) {
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`);
@@ -1005,6 +1069,7 @@ export default function InsuranceStatusPage() {
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={async (pdfId, fallbackFilename) => {
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (!claimed) {
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
@@ -1025,6 +1090,7 @@ export default function InsuranceStatusPage() {
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={async (pdfId, fallbackFilename) => {
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (!claimed) {
setPreviewPdfId(pdfId);
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`);