feat: AI chat scheduling, claim automation, and session improvements

- Internal AI chat: schedule_appointment intent books earliest available
  slot in Column A using office hours; claim_only intent looks up latest
  past appointment for service date, asks user when two appointments are
  within 7 days, auto-triggers correct Selenium worker with mapped prices
- Gemini model updated to gemini-flash-latest; conversation history (15
  messages) passed for pronoun/reference resolution; history trimmed to
  start with user turn so Gemini doesn't reject the context
- Insurance alias file (insuranceAliases.json) replaces hardcoded siteKey
  matching; "tufs" now resolves to TUFTS_SCO
- DOB format normalized (MM/DD/YYYY → YYYY-MM-DD) before parseLocalDate;
  autoCheck now fires for all insurance types, not just MH/CMSP
- Claim form auto-submit: all handlers (MH, CCA, DDMA, UnitedDH, Tufts)
  accept formToUse and receive fee-schedule-priced form; prefillDone set
  after chatbot code prefill so autoSubmit gate opens correctly
- Chatbot: chat history persisted in sessionStorage, cleared on logout
  and auto-logout; Clear button writes fresh state synchronously; message
  history window increased to 15
- DentaQuest/TuftsSCO Selenium: "Remember me" checkbox clicked before
  sign-in to persist OTP trust cookie across sessions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-05 16:19:56 -04:00
parent ba2882957a
commit 1bbca38344
11 changed files with 693 additions and 94 deletions

View File

@@ -79,6 +79,8 @@ interface ClaimFormProps {
patientId: number;
appointmentId?: number;
autoSubmit?: boolean;
/** When set, autoSubmit triggers this insurance's Selenium worker instead of MH */
autoSubmitSiteKey?: string;
/** When true: form saves to AppointmentProcedure (Select Procedures flow), shows only Save button */
proceduresOnly?: boolean;
onSubmit: (data: ClaimFormData) => Promise<Claim>;
@@ -101,6 +103,7 @@ export function ClaimForm({
patientId,
appointmentId,
autoSubmit,
autoSubmitSiteKey,
proceduresOnly = false,
onHandleAppointmentSubmit,
onHandleUpdatePatient,
@@ -487,6 +490,41 @@ export function ClaimForm({
};
}, [appointmentId, serviceDate, existingClaimId]);
// Prefill service lines (and optional service date) from chatbot claim_only flow
useEffect(() => {
const raw = sessionStorage.getItem("chatbot_claim_prefill");
if (!raw) return;
try {
const { codes, serviceDate } = JSON.parse(raw) as {
codes: { code: string; description: string }[];
serviceDate?: string;
};
sessionStorage.removeItem("chatbot_claim_prefill");
if (!codes?.length) return;
if (serviceDate) {
try {
const d = parseLocalDate(serviceDate);
setServiceDateValue(d);
setServiceDate(formatLocalDate(d));
} catch {}
}
setForm((prev) => {
const date = serviceDate ? serviceDate : prev.serviceDate;
const updatedLines = [...prev.serviceLines];
codes.forEach((c, i) => {
if (i < updatedLines.length) {
updatedLines[i] = { ...updatedLines[i]!, procedureCode: c.code, procedureDate: date };
}
});
return { ...prev, serviceLines: updatedLines };
});
if (!appointmentId) setPrefillDone(true);
} catch {}
}, []);
// Restore NPI provider from saved procedures when npiProviders list loads after 2b
useEffect(() => {
if (!savedProcNpiId || !npiProviders.length) return;
@@ -949,10 +987,11 @@ export function ClaimForm({
};
// 3rd Button workflow — CCA Claim: saves to DB then submits via Selenium
const handleCCAClaim = async () => {
const handleCCAClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
const missingFields: string[] = [];
if (!form.memberId?.trim()) missingFields.push("Member ID");
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!f.memberId?.trim()) missingFields.push("Member ID");
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!patient?.firstName?.trim()) missingFields.push("First Name");
if (missingFields.length > 0) {
toast({
@@ -963,7 +1002,7 @@ export function ClaimForm({
return;
}
const filteredServiceLines = (form.serviceLines || []).filter(
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
@@ -990,10 +1029,10 @@ export function ClaimForm({
}
}
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((f) => ({
filename: f.name,
mimeType: f.type,
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
const claimFilesMeta: ClaimFileMeta[] = (uploadedFiles || []).map((file) => ({
filename: file.name,
mimeType: file.type,
}));
const selectedNpiProviderId = npiProvider?.npiNumber
@@ -1014,7 +1053,7 @@ export function ClaimForm({
// Send to CCA Selenium — send raw YYYY-MM-DD so Python _format_dob converts correctly
onHandleForCCASeleniumClaim({
...form,
...f,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
@@ -1028,10 +1067,11 @@ export function ClaimForm({
};
// Delta MA Claim: saves to DB then submits via Selenium
const handleDDMAClaim = async () => {
const handleDDMAClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
const missingFields: string[] = [];
if (!form.memberId?.trim()) missingFields.push("Member ID");
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!f.memberId?.trim()) missingFields.push("Member ID");
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!patient?.firstName?.trim()) missingFields.push("First Name");
if (missingFields.length > 0) {
toast({
@@ -1042,7 +1082,7 @@ export function ClaimForm({
return;
}
const filteredServiceLines = (form.serviceLines || []).filter(
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
@@ -1068,7 +1108,7 @@ export function ClaimForm({
}
}
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
// Upload files to server so we get local filePaths for Selenium
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
@@ -1091,7 +1131,7 @@ export function ClaimForm({
});
onHandleForDDMASeleniumClaim({
...form,
...f,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
@@ -1106,10 +1146,11 @@ export function ClaimForm({
};
// United/DentalHub Claim: saves to DB then submits via Selenium
const handleUnitedDHClaim = async () => {
const handleUnitedDHClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
const missingFields: string[] = [];
if (!form.memberId?.trim()) missingFields.push("Member ID");
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!f.memberId?.trim()) missingFields.push("Member ID");
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!patient?.firstName?.trim()) missingFields.push("First Name");
if (missingFields.length > 0) {
toast({
@@ -1120,7 +1161,7 @@ export function ClaimForm({
return;
}
const filteredServiceLines = (form.serviceLines || []).filter(
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
@@ -1146,7 +1187,7 @@ export function ClaimForm({
}
}
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
? await uploadAttachmentsToLocalFolder(uploadedFiles)
@@ -1168,7 +1209,7 @@ export function ClaimForm({
});
onHandleForUnitedDHSeleniumClaim({
...form,
...f,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
@@ -1183,10 +1224,11 @@ export function ClaimForm({
};
// Tufts SCO Claim: saves to DB then submits via Selenium
const handleTuftsSCOClaim = async () => {
const handleTuftsSCOClaim = async (formToUse?: ClaimFormData & { uploadedFiles?: File[] }) => {
const f = formToUse ?? form;
const missingFields: string[] = [];
if (!form.memberId?.trim()) missingFields.push("Member ID");
if (!form.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!f.memberId?.trim()) missingFields.push("Member ID");
if (!f.dateOfBirth?.trim()) missingFields.push("Date of Birth");
if (!patient?.firstName?.trim()) missingFields.push("First Name");
if (missingFields.length > 0) {
toast({
@@ -1197,7 +1239,7 @@ export function ClaimForm({
return;
}
const filteredServiceLines = (form.serviceLines || []).filter(
const filteredServiceLines = (f.serviceLines || []).filter(
(line) => (line.procedureCode ?? "").trim() !== "",
);
if (filteredServiceLines.length === 0) {
@@ -1223,7 +1265,7 @@ export function ClaimForm({
}
}
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = form;
const { uploadedFiles, insuranceSiteKey, npiProvider, ...formToCreateClaim } = f;
const claimFilesMeta: ClaimFileMeta[] = uploadedFiles?.length
? await uploadAttachmentsToLocalFolder(uploadedFiles)
@@ -1255,7 +1297,7 @@ export function ClaimForm({
}
onHandleForTuftsSCOSeleniumClaim({
...form,
...f,
serviceLines: filteredServiceLines,
staffId: appointmentStaffId ?? Number(staff?.id),
patientId,
@@ -1656,8 +1698,30 @@ export function ClaimForm({
if (autoSubmittedRef.current) return;
autoSubmittedRef.current = true;
handleMHSubmit();
}, [autoSubmit, prefillDone, isFormReady]);
// Apply fee-schedule prices before triggering so billed amounts are populated
const siteKeyForPricing = autoSubmitSiteKey
? autoSubmitSiteKey.replace(/_/g, "").toLowerCase()
: deriveInsuranceSiteKey(form.insuranceProvider || "");
const pricedForm = mapPricesForForm({
form: { ...form, insuranceSiteKey: siteKeyForPricing },
patientDOB: patient?.dateOfBirth ?? "",
insuranceSiteKey: siteKeyForPricing,
});
const key = (autoSubmitSiteKey ?? "").toLowerCase();
if (key === "tufts_sco" || key === "tuftsco" || key === "tufts sco") {
handleTuftsSCOClaim(pricedForm);
} else if (key === "cca") {
handleCCAClaim(pricedForm);
} else if (key === "ddma") {
handleDDMAClaim(pricedForm);
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
handleUnitedDHClaim(pricedForm);
} else {
handleMHSubmit(pricedForm);
}
}, [autoSubmit, autoSubmitSiteKey, prefillDone, isFormReady]);
// overlay click handler (close when clicking backdrop)
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {