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:
@@ -167,6 +167,8 @@ const DIRECT_CODE_MAP: Record<string, string> = {
|
|||||||
// Core / post
|
// Core / post
|
||||||
"core bu": "D2950",
|
"core bu": "D2950",
|
||||||
"p/c": "D2954",
|
"p/c": "D2954",
|
||||||
|
"post": "D2954",
|
||||||
|
"pos": "D2954",
|
||||||
"post core": "D2954",
|
"post core": "D2954",
|
||||||
// Crowns
|
// Crowns
|
||||||
"recement": "D2920",
|
"recement": "D2920",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type InternalChatIntent =
|
|||||||
| "find_patient" // look up patient record only
|
| "find_patient" // look up patient record only
|
||||||
| "schedule_appointment" // add patient to today's (or specified) schedule
|
| "schedule_appointment" // add patient to today's (or specified) schedule
|
||||||
| "claim_only" // submit claim for procedures (no eligibility check)
|
| "claim_only" // submit claim for procedures (no eligibility check)
|
||||||
|
| "preauth" // submit pre-authorization for procedures
|
||||||
| "navigate_claims"
|
| "navigate_claims"
|
||||||
| "navigate_schedule"
|
| "navigate_schedule"
|
||||||
| "navigate_eligibility"
|
| "navigate_eligibility"
|
||||||
@@ -83,6 +84,11 @@ Intents:
|
|||||||
e.g. "claim perio exam, 2BW for John Smith"
|
e.g. "claim perio exam, 2BW for John Smith"
|
||||||
Use this when no eligibility check is requested — just billing/claiming services
|
Use this when no eligibility check is requested — just billing/claiming services
|
||||||
Always extract appointmentDate when a date or "today" is mentioned
|
Always extract appointmentDate when a date or "today" is mentioned
|
||||||
|
- preauth : submit a pre-authorization request for procedures
|
||||||
|
e.g. "preauth rct, post, crown for John Smith"
|
||||||
|
e.g. "pre auth #20 rct, post, crown for Zhiyuan Chen"
|
||||||
|
e.g. "pre-auth D3320, D2952, D2740 for Maria"
|
||||||
|
Use this when the user says "preauth", "pre auth", "pre-auth", or "prior auth"
|
||||||
- navigate_claims : open the claims page
|
- navigate_claims : open the claims page
|
||||||
- navigate_schedule : open the appointments/schedule page
|
- navigate_schedule : open the appointments/schedule page
|
||||||
- navigate_eligibility : open the insurance eligibility page
|
- navigate_eligibility : open the insurance eligibility page
|
||||||
@@ -99,8 +105,12 @@ Rules:
|
|||||||
Only include actual clinical procedures in procedureNames.
|
Only include actual clinical procedures in procedureNames.
|
||||||
- For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces:
|
- For composite fillings with a tooth number, preserve the EXACT notation including tooth# and surfaces:
|
||||||
e.g. "composite #29 O", "#8 MO", "composite #11 MOD" — keep the #number and surface letters together as one entry
|
e.g. "composite #29 O", "#8 MO", "composite #11 MOD" — keep the #number and surface letters together as one entry
|
||||||
|
- #number always means a TOOTH number (never a case or pre-auth reference). When a single #number appears before a comma-separated list of procedures, apply it to EVERY procedure in the list.
|
||||||
|
e.g. "#20 rct, post, crown" → ["#20 rct", "#20 post", "#20 crown"]
|
||||||
|
e.g. "preauth #20 rct, pos, crown" → ["#20 rct", "#20 pos", "#20 crown"]
|
||||||
|
e.g. "#14 rct, buildup, crown" → ["#14 rct", "#14 buildup", "#14 crown"]
|
||||||
- For RCT/root canal with a tooth number, preserve the tooth# in the entry:
|
- For RCT/root canal with a tooth number, preserve the tooth# in the entry:
|
||||||
e.g. "rct #29", "#14 root canal", "rct #6" — keep the #number with the procedure so the correct code can be selected
|
e.g. "rct #29", "#14 root canal", "rct #6", "#20 rct" — keep the #number with the procedure so the correct code can be selected
|
||||||
- For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry:
|
- For SRP with a quadrant abbreviation (UL, UR, LL, LR), keep the code and quadrant together as one entry:
|
||||||
e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code
|
e.g. "D4341 UL", "4341 LR", "D4342 UR" — the quadrant always travels with the SRP code
|
||||||
- For multiple PA X-rays with tooth numbers, expand each PA into its own entry:
|
- For multiple PA X-rays with tooth numbers, expand each PA into its own entry:
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface ChatResponse {
|
|||||||
| "need_insurance_clarification"
|
| "need_insurance_clarification"
|
||||||
| "appointment_created"
|
| "appointment_created"
|
||||||
| "claim_only_ready"
|
| "claim_only_ready"
|
||||||
|
| "preauth_ready"
|
||||||
| "need_appointment_selection"
|
| "need_appointment_selection"
|
||||||
| "need_cdt_clarification";
|
| "need_cdt_clarification";
|
||||||
actionData?: Record<string, any>;
|
actionData?: Record<string, any>;
|
||||||
@@ -325,6 +326,12 @@ export async function runInternalChatWorkflow(
|
|||||||
return await handleClaimOnly(classification, storage, customAliases);
|
return await handleClaimOnly(classification, storage, customAliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Pre-authorization ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (intent === "preauth") {
|
||||||
|
return await handlePreauth(classification, storage, customAliases);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Schedule appointment ───────────────────────────────────────────────────
|
// ── Schedule appointment ───────────────────────────────────────────────────
|
||||||
|
|
||||||
if (intent === "schedule_appointment") {
|
if (intent === "schedule_appointment") {
|
||||||
@@ -692,6 +699,70 @@ async function handleClaimOnly(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── preauth ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function handlePreauth(
|
||||||
|
c: ChatClassification,
|
||||||
|
storage: StorageLike,
|
||||||
|
customAliases: { phrase: string; cdtCode: string }[]
|
||||||
|
): Promise<ChatResponse> {
|
||||||
|
let patient: ResolvedPatient | null = null;
|
||||||
|
|
||||||
|
if (c.memberId?.trim()) {
|
||||||
|
const existing = await findPatientByMemberId(c.memberId.trim(), c.dob, storage);
|
||||||
|
if (existing) patient = patientToResult(existing);
|
||||||
|
} else if (c.patientName?.trim()) {
|
||||||
|
const raw = await findPatientByName(c.patientName.trim(), storage);
|
||||||
|
if (raw) patient = patientToResult(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patient) {
|
||||||
|
return { reply: "Please include a patient name or Member ID so I can look them up." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const procedureNames = stripAttachmentRefs(c.procedureNames ?? []);
|
||||||
|
if (procedureNames.length === 0) {
|
||||||
|
return { reply: "Please specify which procedures to pre-authorize (e.g. rct, post, crown)." };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cdtResults = lookupCdtCodes(procedureNames, customAliases);
|
||||||
|
const matched = cdtResults.filter((r) => r.code !== null);
|
||||||
|
const unmatched = cdtResults.filter((r) => r.code === null);
|
||||||
|
|
||||||
|
if (unmatched.length > 0) {
|
||||||
|
const phrases = unmatched.map((r) => r.input);
|
||||||
|
return {
|
||||||
|
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D3320)`,
|
||||||
|
action: "need_cdt_clarification",
|
||||||
|
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use explicit date from message; otherwise today+3 (pre-auth is for a future appointment)
|
||||||
|
let serviceDate: string = c.appointmentDate ?? (() => {
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() + 3);
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
|
||||||
|
const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
|
||||||
|
const [sy, sm, sd] = serviceDate.split("-");
|
||||||
|
const dateLabel = `${sm}/${sd}/${sy}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
reply: `Opening pre-auth for ${fullName} (tentative date ${dateLabel}): ${matched.map((r) => `${r.code} (${r.description})`).join(", ")}.`,
|
||||||
|
action: "preauth_ready",
|
||||||
|
actionData: {
|
||||||
|
patient,
|
||||||
|
siteKey,
|
||||||
|
serviceDate,
|
||||||
|
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
|
||||||
|
renderingProvider: c.renderingProvider ?? null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ─── schedule_appointment ─────────────────────────────────────────────────────
|
// ─── schedule_appointment ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
const DEFAULT_STAFF_ID = 1; // Column A
|
const DEFAULT_STAFF_ID = 1; // Column A
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ interface ClaimFormProps {
|
|||||||
autoSubmit?: boolean;
|
autoSubmit?: boolean;
|
||||||
/** When set, autoSubmit triggers this insurance's Selenium worker instead of MH */
|
/** When set, autoSubmit triggers this insurance's Selenium worker instead of MH */
|
||||||
autoSubmitSiteKey?: string;
|
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 */
|
/** When true: form saves to AppointmentProcedure (Select Procedures flow), shows only Save button */
|
||||||
proceduresOnly?: boolean;
|
proceduresOnly?: boolean;
|
||||||
onSubmit: (data: ClaimFormData) => Promise<Claim>;
|
onSubmit: (data: ClaimFormData) => Promise<Claim>;
|
||||||
@@ -107,6 +111,8 @@ export function ClaimForm({
|
|||||||
appointmentId,
|
appointmentId,
|
||||||
autoSubmit,
|
autoSubmit,
|
||||||
autoSubmitSiteKey,
|
autoSubmitSiteKey,
|
||||||
|
initialTab,
|
||||||
|
autoSubmitPreauth,
|
||||||
proceduresOnly = false,
|
proceduresOnly = false,
|
||||||
onHandleAppointmentSubmit,
|
onHandleAppointmentSubmit,
|
||||||
onHandleUpdatePatient,
|
onHandleUpdatePatient,
|
||||||
@@ -128,6 +134,7 @@ export function ClaimForm({
|
|||||||
|
|
||||||
const [prefillDone, setPrefillDone] = useState(false);
|
const [prefillDone, setPrefillDone] = useState(false);
|
||||||
const autoSubmittedRef = useRef(false);
|
const autoSubmittedRef = useRef(false);
|
||||||
|
const preauthAutoSubmittedRef = useRef(false);
|
||||||
// Read chatbot-requested rendering provider synchronously at mount (before any effects run)
|
// 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.
|
// so the npiProviders effect always sees it, even when the provider list is already cached.
|
||||||
const [chatbotRenderingProvider] = useState<string | null>(() => {
|
const [chatbotRenderingProvider] = useState<string | null>(() => {
|
||||||
@@ -568,6 +575,50 @@ export function ClaimForm({
|
|||||||
} catch {}
|
} 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
|
// Restore NPI provider from saved procedures when npiProviders list loads after 2b
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!savedProcNpiId || !npiProviders.length) return;
|
if (!savedProcNpiId || !npiProviders.length) return;
|
||||||
@@ -1891,6 +1942,26 @@ export function ClaimForm({
|
|||||||
}
|
}
|
||||||
}, [autoSubmit, autoSubmitSiteKey, prefillDone, isFormReady]);
|
}, [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)
|
// overlay click handler (close when clicking backdrop)
|
||||||
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
const onOverlayMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
// only close if clicked the backdrop itself (not inner modal)
|
// only close if clicked the backdrop itself (not inner modal)
|
||||||
@@ -1903,6 +1974,7 @@ export function ClaimForm({
|
|||||||
return () => {
|
return () => {
|
||||||
// reset when ClaimForm unmounts (modal closes)
|
// reset when ClaimForm unmounts (modal closes)
|
||||||
autoSubmittedRef.current = false;
|
autoSubmittedRef.current = false;
|
||||||
|
preauthAutoSubmittedRef.current = false;
|
||||||
setPrefillDone(false);
|
setPrefillDone(false);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -1913,7 +1985,7 @@ export function ClaimForm({
|
|||||||
onMouseDown={onOverlayMouseDown}
|
onMouseDown={onOverlayMouseDown}
|
||||||
>
|
>
|
||||||
<Card className="w-[90vw] h-[90vh] max-w-none overflow-auto bg-white relative">
|
<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">
|
<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">
|
<div className="flex flex-row items-center justify-between mb-2">
|
||||||
<CardTitle className="text-xl font-bold">
|
<CardTitle className="text-xl font-bold">
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ type Step =
|
|||||||
| "need-insurance-clarification"
|
| "need-insurance-clarification"
|
||||||
| "need-appointment-selection"
|
| "need-appointment-selection"
|
||||||
| "need-cdt-clarification"
|
| "need-cdt-clarification"
|
||||||
| "claim-ready";
|
| "claim-ready"
|
||||||
|
| "preauth-ready";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -158,6 +159,13 @@ export function ChatbotButton() {
|
|||||||
appointmentId: number | null;
|
appointmentId: number | null;
|
||||||
renderingProvider: string | null;
|
renderingProvider: string | null;
|
||||||
} | null>(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 [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -211,6 +219,7 @@ export function ChatbotButton() {
|
|||||||
setApptSelectionData(null);
|
setApptSelectionData(null);
|
||||||
setCdtClarificationData(null);
|
setCdtClarificationData(null);
|
||||||
setClaimReadyData(null);
|
setClaimReadyData(null);
|
||||||
|
setPreauthReadyData(null);
|
||||||
setPendingFiles([]);
|
setPendingFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -471,6 +480,19 @@ export function ChatbotButton() {
|
|||||||
return;
|
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");
|
setStep("menu");
|
||||||
} catch {
|
} catch {
|
||||||
replaceLastMsg("Sorry, something went wrong. Please try again.");
|
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 */}
|
{/* CDT clarification — unknown procedure terms */}
|
||||||
{step === "need-cdt-clarification" && cdtClarificationData && (
|
{step === "need-cdt-clarification" && cdtClarificationData && (
|
||||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 space-y-2">
|
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 space-y-2">
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export default function ClaimsPage() {
|
|||||||
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
||||||
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
||||||
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
|
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
|
||||||
|
const [chatbotInitialTab, setChatbotInitialTab] = useState<string | undefined>(undefined);
|
||||||
|
const [chatbotAutoSubmitPreauthSiteKey, setChatbotAutoSubmitPreauthSiteKey] = useState<string | undefined>(undefined);
|
||||||
// for redirect from appointment page directly, then passing to claimform
|
// for redirect from appointment page directly, then passing to claimform
|
||||||
const [selectedAppointmentId, setSelectedAppointmentId] = useState<
|
const [selectedAppointmentId, setSelectedAppointmentId] = useState<
|
||||||
number | null
|
number | null
|
||||||
@@ -258,6 +260,16 @@ export default function ClaimsPage() {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
// Check if chatbot requested preauth tab
|
||||||
|
try {
|
||||||
|
const preauthRaw = sessionStorage.getItem("chatbot_preauth_prefill");
|
||||||
|
if (preauthRaw) {
|
||||||
|
setChatbotInitialTab("preauth");
|
||||||
|
const parsed = JSON.parse(preauthRaw);
|
||||||
|
if (parsed?.siteKey) setChatbotAutoSubmitPreauthSiteKey(parsed.siteKey);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
handleNewClaim(id);
|
handleNewClaim(id);
|
||||||
clearUrlParams(["newPatient"]);
|
clearUrlParams(["newPatient"]);
|
||||||
}, [newPatient]);
|
}, [newPatient]);
|
||||||
@@ -946,6 +958,8 @@ export default function ClaimsPage() {
|
|||||||
setSelectedPatientId(null);
|
setSelectedPatientId(null);
|
||||||
setSelectedAppointmentId(null);
|
setSelectedAppointmentId(null);
|
||||||
setChatbotAutoSubmitSiteKey(undefined);
|
setChatbotAutoSubmitSiteKey(undefined);
|
||||||
|
setChatbotInitialTab(undefined);
|
||||||
|
setChatbotAutoSubmitPreauthSiteKey(undefined);
|
||||||
setIsClaimFormOpen(false);
|
setIsClaimFormOpen(false);
|
||||||
|
|
||||||
clearUrlParams(["newPatient", "appointmentId"]);
|
clearUrlParams(["newPatient", "appointmentId"]);
|
||||||
@@ -1078,6 +1092,8 @@ export default function ClaimsPage() {
|
|||||||
appointmentId={selectedAppointmentId ?? undefined}
|
appointmentId={selectedAppointmentId ?? undefined}
|
||||||
autoSubmit={mode === "direct" || !!chatbotAutoSubmitSiteKey}
|
autoSubmit={mode === "direct" || !!chatbotAutoSubmitSiteKey}
|
||||||
autoSubmitSiteKey={chatbotAutoSubmitSiteKey}
|
autoSubmitSiteKey={chatbotAutoSubmitSiteKey}
|
||||||
|
initialTab={chatbotInitialTab}
|
||||||
|
autoSubmitPreauth={chatbotAutoSubmitPreauthSiteKey}
|
||||||
proceduresOnly={mode === "procedures"}
|
proceduresOnly={mode === "procedures"}
|
||||||
onClose={closeClaim}
|
onClose={closeClaim}
|
||||||
onSubmit={handleClaimSubmit}
|
onSubmit={handleClaimSubmit}
|
||||||
|
|||||||
@@ -1093,7 +1093,13 @@ class AutomationUnitedDHClaimSubmit:
|
|||||||
)
|
)
|
||||||
self.driver.execute_script("arguments[0].style.display='block';", file_input)
|
self.driver.execute_script("arguments[0].style.display='block';", file_input)
|
||||||
file_input.send_keys(abs_path)
|
file_input.send_keys(abs_path)
|
||||||
time.sleep(1.5)
|
WebDriverWait(self.driver, 60).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Submit Claim')] | "
|
||||||
|
"//button[normalize-space(text())='Submit Claim'] | "
|
||||||
|
"//button[contains(normalize-space(.),'Submit Claim')]"
|
||||||
|
))
|
||||||
|
)
|
||||||
print(f"[UnitedDH Claim] step7: Attached: {os.path.basename(abs_path)}")
|
print(f"[UnitedDH Claim] step7: Attached: {os.path.basename(abs_path)}")
|
||||||
attached += 1
|
attached += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -550,13 +550,28 @@ class AutomationUnitedDHPreAuth:
|
|||||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng)
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng)
|
||||||
arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
||||||
arrow.click()
|
arrow.click()
|
||||||
first_option = WebDriverWait(self.driver, 5).until(
|
time.sleep(1)
|
||||||
EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option"))
|
try:
|
||||||
)
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
option_text = first_option.text.strip()
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
first_option.click()
|
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||||
print(f"[UnitedDH PreAuth] step1: Selected Treatment Location: {option_text}")
|
))
|
||||||
location_selected = True
|
)
|
||||||
|
summit_option.click()
|
||||||
|
print("[UnitedDH PreAuth] step1: Selected Treatment Location: Summit Dental Care")
|
||||||
|
location_selected = True
|
||||||
|
except TimeoutException:
|
||||||
|
try:
|
||||||
|
first_option = self.driver.find_element(By.XPATH,
|
||||||
|
"//ng-dropdown-panel//div[contains(@class,'ng-option')]"
|
||||||
|
)
|
||||||
|
option_text = first_option.text.strip()
|
||||||
|
first_option.click()
|
||||||
|
print(f"[UnitedDH PreAuth] step1: Selected Treatment Location (fallback): {option_text}")
|
||||||
|
location_selected = True
|
||||||
|
except Exception:
|
||||||
|
print("[UnitedDH PreAuth] step1: No options available in Treatment Location dropdown")
|
||||||
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[UnitedDH PreAuth] step1: Treatment Location selection failed: {e}")
|
print(f"[UnitedDH PreAuth] step1: Treatment Location selection failed: {e}")
|
||||||
|
|
||||||
@@ -693,6 +708,24 @@ class AutomationUnitedDHPreAuth:
|
|||||||
))
|
))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fill procedure date from tentative service date.
|
||||||
|
# If tentative date == today, user did not change it — use today + 3 days instead.
|
||||||
|
if self.serviceDate:
|
||||||
|
try:
|
||||||
|
from datetime import date, timedelta
|
||||||
|
service_date = date.fromisoformat(self.serviceDate[:10])
|
||||||
|
if service_date == date.today():
|
||||||
|
service_date = service_date + timedelta(days=3)
|
||||||
|
print("[UnitedDH PreAuth] step3: Tentative date is today — using today + 3 days")
|
||||||
|
proc_date_str = service_date.strftime("%m/%d/%Y")
|
||||||
|
proc_input = self.driver.find_element(By.ID, "procedureDate_Back")
|
||||||
|
proc_input.click()
|
||||||
|
proc_input.send_keys(Keys.CONTROL, "a")
|
||||||
|
proc_input.send_keys(proc_date_str)
|
||||||
|
print(f"[UnitedDH PreAuth] step3: Procedure date entered: {proc_date_str}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[UnitedDH PreAuth] step3: WARNING - Could not fill procedure date: {e}")
|
||||||
|
|
||||||
payer_selected = False
|
payer_selected = False
|
||||||
try:
|
try:
|
||||||
payer_selectors = [
|
payer_selectors = [
|
||||||
@@ -1019,7 +1052,15 @@ class AutomationUnitedDHPreAuth:
|
|||||||
)
|
)
|
||||||
self.driver.execute_script("arguments[0].removeAttribute('class');", file_input)
|
self.driver.execute_script("arguments[0].removeAttribute('class');", file_input)
|
||||||
file_input.send_keys(abs_path)
|
file_input.send_keys(abs_path)
|
||||||
time.sleep(1.5)
|
WebDriverWait(self.driver, 60).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[contains(normalize-space(.),'Submit Authorization') or "
|
||||||
|
"contains(normalize-space(.),'Submit Pre-Auth') or "
|
||||||
|
"contains(normalize-space(.),'Submit Pre Authorization') or "
|
||||||
|
"contains(normalize-space(.),'Submit Pre-Authorization') or "
|
||||||
|
"contains(normalize-space(.),'Submit Claim')]"
|
||||||
|
))
|
||||||
|
)
|
||||||
print(f"[UnitedDH PreAuth] step7: Attached: {os.path.basename(abs_path)}")
|
print(f"[UnitedDH PreAuth] step7: Attached: {os.path.basename(abs_path)}")
|
||||||
attached += 1
|
attached += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1085,15 +1126,17 @@ class AutomationUnitedDHPreAuth:
|
|||||||
lambda d: "status" in d.current_url.lower() or "history" in d.current_url.lower()
|
lambda d: "status" in d.current_url.lower() or "history" in d.current_url.lower()
|
||||||
or d.find_elements(By.XPATH, "//td | //th[contains(text(),'Reference')]")
|
or d.find_elements(By.XPATH, "//td | //th[contains(text(),'Reference')]")
|
||||||
)
|
)
|
||||||
time.sleep(4)
|
time.sleep(3)
|
||||||
print(f"[UnitedDH PreAuth] step9: Status & History URL: {self.driver.current_url}")
|
print(f"[UnitedDH PreAuth] step9: Status & History URL: {self.driver.current_url}")
|
||||||
|
|
||||||
self.driver.refresh()
|
# Wait for table rows with actual data (not just headers)
|
||||||
print("[UnitedDH PreAuth] step9: Page refreshed — waiting for table to reload")
|
try:
|
||||||
WebDriverWait(self.driver, 30).until(
|
WebDriverWait(self.driver, 20).until(
|
||||||
EC.presence_of_element_located((By.XPATH, "//table//tr[td]"))
|
EC.presence_of_element_located((By.XPATH, "//table//tr[td]"))
|
||||||
)
|
)
|
||||||
time.sleep(4)
|
time.sleep(2)
|
||||||
|
except Exception:
|
||||||
|
print("[UnitedDH PreAuth] step9: Table rows did not appear — proceeding with body scan")
|
||||||
|
|
||||||
preauth_number = None
|
preauth_number = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user