feat: AI-powered eligibility detection from Copy Agent screenshots
Adds an "AI Detect & Check Eligibility" action to the Copy Agent page that sends captured screenshots to Claude to extract Member ID/DOB, then opens the chatbot's eligibility confirm step pre-filled with the result. - POST /api/ai/detect-eligibility-info: Claude vision extraction of Member ID/DOB from uploaded screenshots. - POST /api/patients/create-temp: auto-creates a placeholder "PID-<id>" patient (matched by insuranceId+dob) when no patient is selected, so screenshots can be saved immediately; the real eligibility check later rewrites the patient's name and Cloud Storage folder automatically via the existing createOrUpdatePatientByInsuranceId matching logic. - Chatbot: Member ID/DOB on the confirm card are now editable (MM/DD/YYYY), with edits synced back to the patient record before the check runs. - Fixed a useEffect ordering bug on the insurance-status page that could silently clear a chatbot-prefilled Member ID/DOB before the auto-trigger effect ran, preventing the eligibility check from ever starting. - Added a local-disk-only screenshot backup (apps/Backend/uploads/ screenshot-backups/) for debugging, not exposed anywhere in the UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,8 @@ function waitForSeleniumJob(
|
||||
});
|
||||
}
|
||||
|
||||
const PENDING_COPY_AGENT_SCREENSHOTS_KEY = "copy_agent_pending_screenshots";
|
||||
|
||||
export default function InsuranceStatusPage() {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
@@ -181,18 +183,32 @@ export default function InsuranceStatusPage() {
|
||||
const { memberId: id, dob, autoCheck: ac } = JSON.parse(raw);
|
||||
if (id) setMemberId(id);
|
||||
if (dob) {
|
||||
// dob may arrive as MM/DD/YYYY or YYYY-MM-DD — normalize to YYYY-MM-DD
|
||||
const normalized = /^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dob)
|
||||
? (() => { const [m, d, y] = dob.split("/"); return `${y}-${m!.padStart(2,"0")}-${d!.padStart(2,"0")}`; })()
|
||||
: dob;
|
||||
setDateOfBirth(parseLocalDate(normalized));
|
||||
// dob may arrive as MM/DD/YYYY or YYYY-MM-DD — normalize to YYYY-MM-DD.
|
||||
// A malformed date (e.g. hand-edited in the chatbot) must not abort the rest of
|
||||
// this function — otherwise pendingAutoCheck/prefillTick below never get set and
|
||||
// the auto-trigger effect silently never fires (no error, Selenium never launches).
|
||||
try {
|
||||
const normalized = /^\d{1,2}\/\d{1,2}\/\d{4}$/.test(dob)
|
||||
? (() => { const [m, d, y] = dob.split("/"); return `${y}-${m!.padStart(2,"0")}-${d!.padStart(2,"0")}`; })()
|
||||
: dob;
|
||||
setDateOfBirth(parseLocalDate(normalized));
|
||||
} catch (dobErr) {
|
||||
console.error("[insurance-status] failed to parse prefilled DOB:", dob, dobErr);
|
||||
toast({
|
||||
title: "Invalid Date of Birth",
|
||||
description: `Couldn't parse "${dob}" — please re-enter the Date of Birth below and try again.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (ac) pendingAutoCheck.current = ac;
|
||||
sessionStorage.removeItem("chatbot_eligibility");
|
||||
// Increment tick so the auto-trigger effect re-fires even when
|
||||
// memberId/dob are unchanged (same patient submitted a second time).
|
||||
setPrefillTick((n) => n + 1);
|
||||
} catch {}
|
||||
} catch (err) {
|
||||
console.error("[insurance-status] failed to apply chatbot eligibility prefill:", err);
|
||||
}
|
||||
};
|
||||
apply();
|
||||
window.addEventListener("chatbot:eligibility-prefill", apply);
|
||||
@@ -201,6 +217,7 @@ export default function InsuranceStatusPage() {
|
||||
}, []);
|
||||
|
||||
// Populate fields from selected patient
|
||||
const didMountSelectedPatientEffect = useRef(false);
|
||||
useEffect(() => {
|
||||
if (selectedPatient) {
|
||||
setMemberId(selectedPatient.insuranceId ?? "");
|
||||
@@ -212,12 +229,16 @@ export default function InsuranceStatusPage() {
|
||||
? parseLocalDate(selectedPatient.dateOfBirth)
|
||||
: selectedPatient.dateOfBirth;
|
||||
setDateOfBirth(dob ?? null);
|
||||
} else {
|
||||
} else if (didMountSelectedPatientEffect.current) {
|
||||
// Only clear on an actual deselection (user action) — not on the initial mount, where
|
||||
// selectedPatient starts null by default and would otherwise wipe out values the
|
||||
// chatbot-prefill effect (above) just set from sessionStorage on this same page load.
|
||||
setMemberId("");
|
||||
setFirstName("");
|
||||
setLastName("");
|
||||
setDateOfBirth(null);
|
||||
}
|
||||
didMountSelectedPatientEffect.current = true;
|
||||
}, [selectedPatient]);
|
||||
|
||||
// Auto-lookup patient by member ID when typed manually
|
||||
@@ -286,6 +307,49 @@ export default function InsuranceStatusPage() {
|
||||
return new Date() < cutoff;
|
||||
};
|
||||
|
||||
// If the Copy Agent page couldn't read a Member ID/DOB from a screenshot, it stashes the
|
||||
// screenshot(s) here and lets the user complete the check manually. Once that check resolves
|
||||
// to a real patient, attach the originally-captured screenshots to that patient's Attachments.
|
||||
const flushPendingCopyAgentScreenshots = async (jobResult: any) => {
|
||||
let raw: string | null = null;
|
||||
try {
|
||||
raw = sessionStorage.getItem(PENDING_COPY_AGENT_SCREENSHOTS_KEY);
|
||||
} catch {}
|
||||
if (!raw || !jobResult?.patientId) return;
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem(PENDING_COPY_AGENT_SCREENSHOTS_KEY);
|
||||
const items: { dataUrl: string; fileName: string }[] = JSON.parse(raw);
|
||||
if (!items.length) return;
|
||||
|
||||
let patientName = `Patient ${jobResult.patientId}`;
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/patients/${jobResult.patientId}`);
|
||||
const patient: Patient = await res.json();
|
||||
const full = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||
if (full) patientName = full;
|
||||
} catch {}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("patientId", String(jobResult.patientId));
|
||||
formData.append("patientName", patientName);
|
||||
formData.append("screenshotBackup", "true");
|
||||
for (const item of items) {
|
||||
const blobRes = await fetch(item.dataUrl);
|
||||
const blob = await blobRes.blob();
|
||||
formData.append("files", new File([blob], `${item.fileName}.png`, { type: "image/png" }));
|
||||
}
|
||||
|
||||
await apiRequest("POST", "/api/claims/upload-to-cloud", formData);
|
||||
toast({
|
||||
title: "Screenshots attached",
|
||||
description: `Screenshots from Copy Agent were attached to ${patientName}'s attachments.`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to attach pending Copy Agent screenshots:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Shared: run MH eligibility selenium job, return jobResult or throw
|
||||
const runMHEligibilitySelenium = async (): Promise<any> => {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
@@ -325,9 +389,11 @@ export default function InsuranceStatusPage() {
|
||||
}),
|
||||
);
|
||||
|
||||
return waitForSeleniumJob(jobId, (msg) =>
|
||||
const jobResult = await waitForSeleniumJob(jobId, (msg) =>
|
||||
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: msg }))
|
||||
);
|
||||
await flushPendingCopyAgentScreenshots(jobResult);
|
||||
return jobResult;
|
||||
};
|
||||
|
||||
const handleAddPatient = async () => {
|
||||
|
||||
Reference in New Issue
Block a user