fix: make chatbot patient-found DOB/Member ID editable and fix eligibility→appointment ordering

- Member ID and DOB in the AI chat "patient found" confirmation card are now
  editable inputs, and edits are persisted back to the patient record before
  proceeding so corrections don't revert to stale values.
- Fix auto-lookup-by-member-ID effect on the insurance status page clobbering
  a hand-edited DOB with the stale DB value during chatbot prefill.
- CMSP (under-14) eligibility flow was missing the appointment-creation hook
  that the adult MassHealth flow already had, so under-14 patients never got
  scheduled after a chatbot-initiated "eligibility & appointment" request.
- For already-known patients, the chatbot was creating the appointment before
  running the eligibility check instead of after; now eligibility is always
  checked first and the appointment created only afterward, and only when the
  resulting patient status is ACTIVE.
This commit is contained in:
2026-07-20 20:05:13 -04:00
parent b30bed8903
commit ca568de817
2 changed files with 88 additions and 50 deletions

View File

@@ -197,6 +197,9 @@ export function ChatbotButton() {
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
const [freeTextInput, setFreeTextInput] = useState("");
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
// Snapshot of insuranceId/dateOfBirth as originally fetched, so we can tell whether the
// user hand-edited them in the patient-found card and only PUT to the DB when they did.
const originalPatientResultRef = useRef<{ insuranceId: string | null; dateOfBirth: 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);
@@ -435,30 +438,41 @@ export function ChatbotButton() {
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
};
const handleEligibilityFromPatient = () => {
// If the user hand-edited Member ID / DOB on the patient-found card, save the correction
// back to the patient record so it doesn't revert to the stale value next time.
const persistPatientEditsIfChanged = async (patient: PatientResult) => {
const original = originalPatientResultRef.current;
if (!original) return;
const changed =
patient.insuranceId !== original.insuranceId || patient.dateOfBirth !== original.dateOfBirth;
if (!changed) return;
try {
await apiRequest("PUT", `/api/patients/${patient.id}`, {
insuranceId: patient.insuranceId,
dateOfBirth: patient.dateOfBirth,
});
originalPatientResultRef.current = { insuranceId: patient.insuranceId, dateOfBirth: patient.dateOfBirth };
} catch {
// Non-fatal — the edited values are still used for this eligibility check below.
}
};
const handleEligibilityFromPatient = async () => {
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
addMsg("user", "Check eligibility now");
addMsg("bot", "Opening the eligibility check page...");
await persistPatientEditsIfChanged(patientResult);
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
};
const handleEligibilityAndAppointmentFromPatient = async () => {
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
addMsg("user", "Check eligibility & add to schedule (today)");
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...");
}
addMsg("bot", "Running eligibility check — will add to today's schedule after...");
await persistPatientEditsIfChanged(patientResult);
// Create the appointment only after eligibility is confirmed, not before — the eligibility
// page's tryAppointmentFromChatbot picks this up once the check running below completes.
sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: patientResult.insuranceId, date: null }));
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
};
@@ -532,30 +546,11 @@ export function ChatbotButton() {
: "today";
addMsg("user", `Check eligibility & add to schedule (${dateLabel})`);
await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id);
if (!eligibilityIdData.patient) {
addMsg("bot", `Running eligibility check — will add patient and create appointment for ${dateLabel} after...`);
sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null }));
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
return;
}
addMsg("bot", `Creating appointment for ${dateLabel}...`, true);
try {
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
patientId: eligibilityIdData.patient.id,
date: targetDate ?? undefined,
});
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"}) for ${data.dateLabel} — opening eligibility check page...`);
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
} catch {
replaceLastMsg("Could not create appointment. Please try again.");
}
addMsg("bot", `Running eligibility check — will add${eligibilityIdData.patient ? "" : " patient and"} to schedule for ${dateLabel} after...`);
// Create the appointment only after eligibility is confirmed, not before — the eligibility
// page's tryAppointmentFromChatbot picks this up once the check running below completes.
sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null }));
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
};
const handleCheckAndClaimRun = () => {
@@ -614,6 +609,10 @@ export function ChatbotButton() {
data.actionData?.patient
) {
setPatientResult(data.actionData.patient);
originalPatientResultRef.current = {
insuranceId: data.actionData.patient.insuranceId,
dateOfBirth: data.actionData.patient.dateOfBirth,
};
setStep("patient-found");
return;
}
@@ -933,12 +932,29 @@ export function ChatbotButton() {
{patientResult.insuranceProvider && (
<p className="text-xs text-blue-600">{patientResult.insuranceProvider}</p>
)}
{patientResult.insuranceId && (
<p className="text-xs text-gray-500">ID: {patientResult.insuranceId}</p>
)}
{patientResult.dateOfBirth && (
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
)}
<div className="space-y-1">
<Label className="text-[10px] text-gray-500">Member ID</Label>
<Input
value={patientResult.insuranceId ?? ""}
onChange={(e) =>
setPatientResult((prev) => (prev ? { ...prev, insuranceId: e.target.value } : prev))
}
className="h-7 text-xs bg-white"
/>
</div>
<div className="space-y-1">
<Label className="text-[10px] text-gray-500">Date of Birth (MM/DD/YYYY)</Label>
<Input
value={patientResult.dateOfBirth ? isoToMDY(patientResult.dateOfBirth) : ""}
onChange={(e) =>
setPatientResult((prev) =>
prev ? { ...prev, dateOfBirth: mdyToISO(e.target.value) } : prev
)
}
placeholder="MM/DD/YYYY"
className="h-7 text-xs bg-white"
/>
</div>
<div className="flex flex-col gap-2 pt-1">
{patientResult.insuranceId && patientResult.dateOfBirth && (
<>

View File

@@ -173,6 +173,9 @@ export default function InsuranceStatusPage() {
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
const pendingScrollTo = useRef<string | null>(null);
const [prefillTick, setPrefillTick] = useState(0);
// Set when the chatbot prefill supplies a DOB, so the auto-lookup-by-member-ID effect below
// doesn't clobber a deliberately hand-edited DOB with the (possibly stale/wrong) one on file.
const suppressAutoLookupDobRef = useRef(false);
// Prefill from chatbot
useEffect(() => {
@@ -192,6 +195,7 @@ export default function InsuranceStatusPage() {
? (() => { const [m, d, y] = dob.split("/"); return `${y}-${m!.padStart(2,"0")}-${d!.padStart(2,"0")}`; })()
: dob;
setDateOfBirth(parseLocalDate(normalized));
suppressAutoLookupDobRef.current = true;
} catch (dobErr) {
console.error("[insurance-status] failed to parse prefilled DOB:", dob, dobErr);
toast({
@@ -254,11 +258,15 @@ export default function InsuranceStatusPage() {
if (patient) {
setFirstName(patient.firstName ?? "");
setLastName(patient.lastName ?? "");
const dob =
typeof patient.dateOfBirth === "string"
? parseLocalDate(patient.dateOfBirth)
: patient.dateOfBirth ?? null;
setDateOfBirth(dob);
if (suppressAutoLookupDobRef.current) {
suppressAutoLookupDobRef.current = false;
} else {
const dob =
typeof patient.dateOfBirth === "string"
? parseLocalDate(patient.dateOfBirth)
: patient.dateOfBirth ?? null;
setDateOfBirth(dob);
}
}
} catch {
// silently ignore lookup errors
@@ -709,6 +717,9 @@ export default function InsuranceStatusPage() {
variant: "default",
});
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
void tryAppointmentFromChatbot();
if (claimed) return;
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
processNextInQueue();
@@ -831,6 +842,17 @@ export default function InsuranceStatusPage() {
const patient = await lookupRes.json();
if (!patient?.id) return;
// The eligibility check that just ran saved the result to patient.status — only book
// the appointment when insurance came back ACTIVE.
if (patient.status !== "ACTIVE") {
toast({
title: "No appointment created",
description: `${patient.firstName ?? ""} ${patient.lastName ?? ""} insurance is ${patient.status === "INACTIVE" ? "inactive" : "not active"} — please add manually if needed.`.trim(),
variant: "destructive",
});
return;
}
const apptRes = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: patient.id, date: useDate ?? undefined });
const apptData = await apptRes.json();
if (apptRes.ok) {