feat: AI chatbot preauth intent, UnitedDH pre-auth improvements
- Add preauth intent to AI chatbot (classifier, workflow, frontend UI card) - Auto-prefill preauth form with CDT codes, service date, and mapped prices - Auto-trigger preauth Selenium handler by insurance siteKey (MH/Tufts/CCA/UnitedDH) - Default tentative service date to today+3 for preauth (user didn't pick a date) - Fix #number always means tooth number, distributes to all procedures in comma list - Fix bare "post"/"pos" → D2954 (was matching D2955 via keyword scorer) - UnitedDH pre-auth: fill procedure date with Ctrl+A to overwrite prefilled value - UnitedDH pre-auth: select Location "Summit Dental Care" in step1 (same as billing entity) - UnitedDH pre-auth: remove page refresh in step9 to preserve pre-auth number - UnitedDH pre-auth: wait for table rows before extracting pre-auth number - UnitedDH pre-auth/claim: explicit wait for Submit button after file upload (no sleep) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,10 @@ interface ClaimFormProps {
|
||||
autoSubmit?: boolean;
|
||||
/** When set, autoSubmit triggers this insurance's Selenium worker instead of MH */
|
||||
autoSubmitSiteKey?: string;
|
||||
/** When set, opens this tab by default (e.g. "preauth") */
|
||||
initialTab?: string;
|
||||
/** When set, auto-triggers the matching preauth Selenium handler (siteKey value) */
|
||||
autoSubmitPreauth?: string;
|
||||
/** When true: form saves to AppointmentProcedure (Select Procedures flow), shows only Save button */
|
||||
proceduresOnly?: boolean;
|
||||
onSubmit: (data: ClaimFormData) => Promise<Claim>;
|
||||
@@ -107,6 +111,8 @@ export function ClaimForm({
|
||||
appointmentId,
|
||||
autoSubmit,
|
||||
autoSubmitSiteKey,
|
||||
initialTab,
|
||||
autoSubmitPreauth,
|
||||
proceduresOnly = false,
|
||||
onHandleAppointmentSubmit,
|
||||
onHandleUpdatePatient,
|
||||
@@ -128,6 +134,7 @@ export function ClaimForm({
|
||||
|
||||
const [prefillDone, setPrefillDone] = useState(false);
|
||||
const autoSubmittedRef = useRef(false);
|
||||
const preauthAutoSubmittedRef = useRef(false);
|
||||
// Read chatbot-requested rendering provider synchronously at mount (before any effects run)
|
||||
// so the npiProviders effect always sees it, even when the provider list is already cached.
|
||||
const [chatbotRenderingProvider] = useState<string | null>(() => {
|
||||
@@ -568,6 +575,50 @@ export function ClaimForm({
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
// Prefill service lines and service date from chatbot preauth flow, then map prices
|
||||
useEffect(() => {
|
||||
const raw = sessionStorage.getItem("chatbot_preauth_prefill");
|
||||
if (!raw) return;
|
||||
try {
|
||||
const { codes, serviceDate, siteKey } = JSON.parse(raw) as {
|
||||
codes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
|
||||
serviceDate?: string;
|
||||
siteKey?: string;
|
||||
};
|
||||
sessionStorage.removeItem("chatbot_preauth_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 insuranceSiteKey = siteKey ? siteKey.replace(/_/g, "").toLowerCase() : prev.insuranceSiteKey;
|
||||
const updatedLines = [...prev.serviceLines];
|
||||
codes.forEach((c, i) => {
|
||||
if (i < updatedLines.length) {
|
||||
updatedLines[i] = {
|
||||
...updatedLines[i]!,
|
||||
procedureCode: c.code,
|
||||
procedureDate: date,
|
||||
...(c.toothNumber ? { toothNumber: c.toothNumber } : {}),
|
||||
...(c.toothSurface ? { toothSurface: c.toothSurface } : {}),
|
||||
...(c.quad ? { quad: c.quad } : {}),
|
||||
};
|
||||
}
|
||||
});
|
||||
const filled = { ...prev, insuranceSiteKey, serviceLines: updatedLines };
|
||||
return mapPricesForForm({ form: filled, patientDOB: "", insuranceSiteKey });
|
||||
});
|
||||
setPrefillDone(true);
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
// Restore NPI provider from saved procedures when npiProviders list loads after 2b
|
||||
useEffect(() => {
|
||||
if (!savedProcNpiId || !npiProviders.length) return;
|
||||
@@ -1891,6 +1942,26 @@ export function ClaimForm({
|
||||
}
|
||||
}, [autoSubmit, autoSubmitSiteKey, prefillDone, isFormReady]);
|
||||
|
||||
// Auto-trigger preauth Selenium handler when autoSubmitPreauth is set
|
||||
useEffect(() => {
|
||||
if (!autoSubmitPreauth) return;
|
||||
if (!prefillDone) return;
|
||||
if (!isFormReady) return;
|
||||
if (preauthAutoSubmittedRef.current) return;
|
||||
preauthAutoSubmittedRef.current = true;
|
||||
|
||||
const key = autoSubmitPreauth.toLowerCase();
|
||||
if (key === "tufts_sco" || key === "tuftsco" || key === "tufts sco") {
|
||||
handleTuftsSCOPreAuth();
|
||||
} else if (key === "cca") {
|
||||
handleCCAPreAuth();
|
||||
} else if (key === "united_sco" || key === "unitedco" || key === "dentalhub") {
|
||||
handleUnitedDHPreAuth();
|
||||
} else {
|
||||
handleMHPreAuth();
|
||||
}
|
||||
}, [autoSubmitPreauth, prefillDone, isFormReady]);
|
||||
|
||||
// overlay click handler (close when clicking backdrop)
|
||||
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// only close if clicked the backdrop itself (not inner modal)
|
||||
@@ -1903,6 +1974,7 @@ export function ClaimForm({
|
||||
return () => {
|
||||
// reset when ClaimForm unmounts (modal closes)
|
||||
autoSubmittedRef.current = false;
|
||||
preauthAutoSubmittedRef.current = false;
|
||||
setPrefillDone(false);
|
||||
};
|
||||
}, []);
|
||||
@@ -1913,7 +1985,7 @@ export function ClaimForm({
|
||||
onMouseDown={onOverlayMouseDown}
|
||||
>
|
||||
<Card className="w-[90vw] h-[90vh] max-w-none overflow-auto bg-white relative">
|
||||
<Tabs defaultValue="claim">
|
||||
<Tabs defaultValue={initialTab ?? "claim"}>
|
||||
<CardHeader className="sticky top-0 z-20 pb-2 border-b bg-white/95 backdrop-blur-sm">
|
||||
<div className="flex flex-row items-center justify-between mb-2">
|
||||
<CardTitle className="text-xl font-bold">
|
||||
|
||||
@@ -30,7 +30,8 @@ type Step =
|
||||
| "need-insurance-clarification"
|
||||
| "need-appointment-selection"
|
||||
| "need-cdt-clarification"
|
||||
| "claim-ready";
|
||||
| "claim-ready"
|
||||
| "preauth-ready";
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
@@ -158,6 +159,13 @@ export function ChatbotButton() {
|
||||
appointmentId: number | null;
|
||||
renderingProvider: string | null;
|
||||
} | null>(null);
|
||||
const [preauthReadyData, setPreauthReadyData] = useState<{
|
||||
patient: PatientResult | null;
|
||||
matchedCodes: { code: string; description: string; toothNumber?: string }[];
|
||||
siteKey: string;
|
||||
serviceDate: string;
|
||||
renderingProvider: string | null;
|
||||
} | null>(null);
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||
const [, setLocation] = useLocation();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
@@ -211,6 +219,7 @@ export function ChatbotButton() {
|
||||
setApptSelectionData(null);
|
||||
setCdtClarificationData(null);
|
||||
setClaimReadyData(null);
|
||||
setPreauthReadyData(null);
|
||||
setPendingFiles([]);
|
||||
};
|
||||
|
||||
@@ -471,6 +480,19 @@ export function ChatbotButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "preauth_ready" && data.actionData) {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, renderingProvider } = data.actionData;
|
||||
setPreauthReadyData({
|
||||
patient: patient ?? null,
|
||||
matchedCodes: matchedCodes ?? [],
|
||||
siteKey,
|
||||
serviceDate,
|
||||
renderingProvider: renderingProvider ?? null,
|
||||
});
|
||||
setStep("preauth-ready");
|
||||
return;
|
||||
}
|
||||
|
||||
setStep("menu");
|
||||
} catch {
|
||||
replaceLastMsg("Sorry, something went wrong. Please try again.");
|
||||
@@ -917,6 +939,56 @@ export function ChatbotButton() {
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* PreAuth confirmation card */}
|
||||
{step === "preauth-ready" && preauthReadyData && (() => {
|
||||
const [sy, sm, sd] = (preauthReadyData.serviceDate ?? "").split("-");
|
||||
const dateLabel = sy ? `${sm}/${sd}/${sy}` : preauthReadyData.serviceDate;
|
||||
return (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-blue-800">Confirm Pre-Authorization</p>
|
||||
{preauthReadyData.patient && (
|
||||
<p className="text-xs text-blue-700 font-medium">
|
||||
{preauthReadyData.patient.firstName} {preauthReadyData.patient.lastName}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-gray-500">Tentative date: {dateLabel}</p>
|
||||
{preauthReadyData.matchedCodes.length > 0 && (
|
||||
<div className="space-y-0.5 pt-0.5">
|
||||
{preauthReadyData.matchedCodes.map((c) => (
|
||||
<p key={c.code} className="text-xs text-gray-700 pl-1">
|
||||
<span className="font-medium">{c.code}</span>{c.toothNumber ? ` #${c.toothNumber}` : ""} — {c.description}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
className="flex-1 h-8 text-xs bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, renderingProvider } = preauthReadyData;
|
||||
addMsg("user", "Confirm & open pre-auth");
|
||||
addMsg("bot", "Opening pre-auth form...");
|
||||
if (patient?.id && matchedCodes.length > 0) {
|
||||
sessionStorage.setItem(
|
||||
"chatbot_preauth_prefill",
|
||||
JSON.stringify({ codes: matchedCodes, siteKey, serviceDate, renderingProvider: renderingProvider ?? null })
|
||||
);
|
||||
}
|
||||
setChatbotPendingFiles(pendingFiles);
|
||||
const url = `/claims?newPatient=${patient?.id}&tab=preauth`;
|
||||
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
|
||||
}}
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
Confirm & Open PreAuth
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* CDT clarification — unknown procedure terms */}
|
||||
{step === "need-cdt-clarification" && cdtClarificationData && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user