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:
@@ -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>) => {
|
||||
|
||||
Reference in New Issue
Block a user