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:
@@ -197,6 +197,9 @@ export function ChatbotButton() {
|
|||||||
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
|
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
|
||||||
const [freeTextInput, setFreeTextInput] = useState("");
|
const [freeTextInput, setFreeTextInput] = useState("");
|
||||||
const [patientResult, setPatientResult] = useState<PatientResult | null>(null);
|
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 [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 [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);
|
||||||
@@ -435,30 +438,41 @@ export function ChatbotButton() {
|
|||||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
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;
|
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...");
|
||||||
|
await persistPatientEditsIfChanged(patientResult);
|
||||||
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEligibilityAndAppointmentFromPatient = async () => {
|
const handleEligibilityAndAppointmentFromPatient = async () => {
|
||||||
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return;
|
||||||
addMsg("user", "Check eligibility & add to schedule (today)");
|
addMsg("user", "Check eligibility & add to schedule (today)");
|
||||||
addMsg("bot", "Creating appointment for today...", true);
|
addMsg("bot", "Running eligibility check — will add to today's schedule after...");
|
||||||
try {
|
await persistPatientEditsIfChanged(patientResult);
|
||||||
const res = await apiRequest("POST", "/api/ai/create-appointment-today", {
|
// Create the appointment only after eligibility is confirmed, not before — the eligibility
|
||||||
patientId: patientResult.id,
|
// 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 }));
|
||||||
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...");
|
|
||||||
}
|
|
||||||
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -532,30 +546,11 @@ export function ChatbotButton() {
|
|||||||
: "today";
|
: "today";
|
||||||
addMsg("user", `Check eligibility & add to schedule (${dateLabel})`);
|
addMsg("user", `Check eligibility & add to schedule (${dateLabel})`);
|
||||||
await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id);
|
await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id);
|
||||||
|
addMsg("bot", `Running eligibility check — will add${eligibilityIdData.patient ? "" : " patient and"} to schedule for ${dateLabel} after...`);
|
||||||
if (!eligibilityIdData.patient) {
|
// Create the appointment only after eligibility is confirmed, not before — the eligibility
|
||||||
addMsg("bot", `Running eligibility check — will add patient and create appointment for ${dateLabel} after...`);
|
// 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 }));
|
sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null }));
|
||||||
prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck);
|
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.");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCheckAndClaimRun = () => {
|
const handleCheckAndClaimRun = () => {
|
||||||
@@ -614,6 +609,10 @@ export function ChatbotButton() {
|
|||||||
data.actionData?.patient
|
data.actionData?.patient
|
||||||
) {
|
) {
|
||||||
setPatientResult(data.actionData.patient);
|
setPatientResult(data.actionData.patient);
|
||||||
|
originalPatientResultRef.current = {
|
||||||
|
insuranceId: data.actionData.patient.insuranceId,
|
||||||
|
dateOfBirth: data.actionData.patient.dateOfBirth,
|
||||||
|
};
|
||||||
setStep("patient-found");
|
setStep("patient-found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -933,12 +932,29 @@ export function ChatbotButton() {
|
|||||||
{patientResult.insuranceProvider && (
|
{patientResult.insuranceProvider && (
|
||||||
<p className="text-xs text-blue-600">{patientResult.insuranceProvider}</p>
|
<p className="text-xs text-blue-600">{patientResult.insuranceProvider}</p>
|
||||||
)}
|
)}
|
||||||
{patientResult.insuranceId && (
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-gray-500">ID: {patientResult.insuranceId}</p>
|
<Label className="text-[10px] text-gray-500">Member ID</Label>
|
||||||
)}
|
<Input
|
||||||
{patientResult.dateOfBirth && (
|
value={patientResult.insuranceId ?? ""}
|
||||||
<p className="text-xs text-gray-500">DOB: {patientResult.dateOfBirth}</p>
|
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">
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
{patientResult.insuranceId && patientResult.dateOfBirth && (
|
{patientResult.insuranceId && patientResult.dateOfBirth && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ export default function InsuranceStatusPage() {
|
|||||||
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
||||||
const pendingScrollTo = useRef<string | null>(null);
|
const pendingScrollTo = useRef<string | null>(null);
|
||||||
const [prefillTick, setPrefillTick] = useState(0);
|
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
|
// Prefill from chatbot
|
||||||
useEffect(() => {
|
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")}`; })()
|
? (() => { const [m, d, y] = dob.split("/"); return `${y}-${m!.padStart(2,"0")}-${d!.padStart(2,"0")}`; })()
|
||||||
: dob;
|
: dob;
|
||||||
setDateOfBirth(parseLocalDate(normalized));
|
setDateOfBirth(parseLocalDate(normalized));
|
||||||
|
suppressAutoLookupDobRef.current = true;
|
||||||
} catch (dobErr) {
|
} catch (dobErr) {
|
||||||
console.error("[insurance-status] failed to parse prefilled DOB:", dob, dobErr);
|
console.error("[insurance-status] failed to parse prefilled DOB:", dob, dobErr);
|
||||||
toast({
|
toast({
|
||||||
@@ -254,12 +258,16 @@ export default function InsuranceStatusPage() {
|
|||||||
if (patient) {
|
if (patient) {
|
||||||
setFirstName(patient.firstName ?? "");
|
setFirstName(patient.firstName ?? "");
|
||||||
setLastName(patient.lastName ?? "");
|
setLastName(patient.lastName ?? "");
|
||||||
|
if (suppressAutoLookupDobRef.current) {
|
||||||
|
suppressAutoLookupDobRef.current = false;
|
||||||
|
} else {
|
||||||
const dob =
|
const dob =
|
||||||
typeof patient.dateOfBirth === "string"
|
typeof patient.dateOfBirth === "string"
|
||||||
? parseLocalDate(patient.dateOfBirth)
|
? parseLocalDate(patient.dateOfBirth)
|
||||||
: patient.dateOfBirth ?? null;
|
: patient.dateOfBirth ?? null;
|
||||||
setDateOfBirth(dob);
|
setDateOfBirth(dob);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// silently ignore lookup errors
|
// silently ignore lookup errors
|
||||||
}
|
}
|
||||||
@@ -709,6 +717,9 @@ export default function InsuranceStatusPage() {
|
|||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||||
|
void tryAppointmentFromChatbot();
|
||||||
|
if (claimed) return;
|
||||||
setSelectedPatient(null);
|
setSelectedPatient(null);
|
||||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
processNextInQueue();
|
processNextInQueue();
|
||||||
@@ -831,6 +842,17 @@ export default function InsuranceStatusPage() {
|
|||||||
const patient = await lookupRes.json();
|
const patient = await lookupRes.json();
|
||||||
if (!patient?.id) return;
|
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 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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user