feat: AI chat file uploads, RCT/PA tooth mapping, check+claim flow, service date column
- Chatbot: add paperclip button to attach X-ray/PDF files to claim submissions; files flow through chatbotFileStore into claim-form uploadedFiles for Selenium - CDT lookup: auto-select D3310/D3320/D3330 by tooth number for RCT; strip #NN from procedure names before alias lookup so "1 pa, #3" → D0220 tooth 3; add "1 pa" alias for D0220; expand multi-PA notation via AI prompt rule - AI classifier: add navigate_eligibility intent ("check mh" → /insurance-status); fix duplicate intent entry; add RCT and multi-PA prompt rules - Check+claim flow: pass serviceDate through check_and_claim_ready actionData; tryClaimFromChatbot skips PDF preview and navigates straight to claims with memberId fallback lookup; wired into all provider onPdfReady callbacks - Claims table: add Service Date column between Patient Name and Submission Date Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,7 @@ const DIRECT_CODE_MAP: Record<string, string> = {
|
||||
"4bw": "D0274",
|
||||
"4 bw": "D0274",
|
||||
"pa": "D0220",
|
||||
"1 pa": "D0220",
|
||||
"first pa": "D0220",
|
||||
"2nd pa": "D0230",
|
||||
"additional pa": "D0230",
|
||||
@@ -238,6 +239,11 @@ function extractToothSurface(input: string): { toothNumber?: string; toothSurfac
|
||||
const FRONT_TEETH = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]);
|
||||
const BACK_TEETH = new Set([1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 28, 29, 30, 31, 32]);
|
||||
|
||||
// RCT tooth classification by tooth type
|
||||
const RCT_ANTERIOR = new Set([6, 7, 8, 9, 10, 11, 22, 23, 24, 25, 26, 27]); // D3310
|
||||
const RCT_PREMOLAR = new Set([4, 5, 12, 13, 20, 21, 28, 29]); // D3320
|
||||
const RCT_MOLAR = new Set([1, 2, 3, 14, 15, 16, 17, 18, 19, 30, 31, 32]); // D3330
|
||||
|
||||
/**
|
||||
* Parse composite filling notation like "#29 OB", "composite #8 MO", "tooth 11 MOD".
|
||||
* Returns the correct D233x/D239x code based on tooth location and surface count.
|
||||
@@ -271,6 +277,43 @@ function parseCompositeCode(input: string): CdtMatch | null {
|
||||
return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum), toothSurface: surfaces.toUpperCase() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse RCT/root-canal notation like "rct #29", "#14 rct", "root canal #6".
|
||||
* Returns D3310 (anterior), D3320 (premolar), or D3330 (molar) based on tooth number.
|
||||
*/
|
||||
function parseRctCode(input: string): CdtMatch | null {
|
||||
const lower = input.toLowerCase();
|
||||
const isRct = /\b(rct|root\s*canal)\b/.test(lower);
|
||||
if (!isRct) return null;
|
||||
|
||||
// Extract tooth number from "#NN" or bare number adjacent to "rct"/"root canal"
|
||||
let toothNum: number | null = null;
|
||||
const hashMatch = input.match(/#(\d{1,2})/);
|
||||
if (hashMatch) {
|
||||
toothNum = parseInt(hashMatch[1]!, 10);
|
||||
} else {
|
||||
// Fallback: bare number in input (e.g. "rct 29", "29 rct")
|
||||
const numMatch = input.match(/\b(\d{1,2})\b/);
|
||||
if (numMatch) toothNum = parseInt(numMatch[1]!, 10);
|
||||
}
|
||||
|
||||
if (!toothNum || toothNum < 1 || toothNum > 32) return null;
|
||||
|
||||
let code: string;
|
||||
if (RCT_ANTERIOR.has(toothNum)) {
|
||||
code = "D3310";
|
||||
} else if (RCT_PREMOLAR.has(toothNum)) {
|
||||
code = "D3320";
|
||||
} else if (RCT_MOLAR.has(toothNum)) {
|
||||
code = "D3330";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
||||
return { code, description: row?.Description ?? code, input, toothNumber: String(toothNum) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Score how well a set of query tokens matches a code's description tokens.
|
||||
* Each matched token contributes 1 point; shorter descriptions get a bonus
|
||||
@@ -347,10 +390,14 @@ export function lookupCdtCodes(
|
||||
const cleaned = name.trim().toLowerCase();
|
||||
// Extract tooth# from "#NN" notation — applies to any procedure
|
||||
const toothInfo = extractToothSurface(name);
|
||||
// Version of cleaned with tooth notation stripped, for alias matching
|
||||
// e.g. "1 pa, #3" → "1 pa" so it hits the "1 pa" alias
|
||||
const strippedCleaned = cleaned.replace(/,?\s*#\d{1,2}\b/g, "").replace(/\s+/g, " ").trim();
|
||||
|
||||
// 1. Custom alias exact match (highest priority)
|
||||
if (customMap[cleaned]) {
|
||||
const code = customMap[cleaned]!;
|
||||
const customKey = customMap[cleaned] ? cleaned : customMap[strippedCleaned] ? strippedCleaned : null;
|
||||
if (customKey) {
|
||||
const code = customMap[customKey]!;
|
||||
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
||||
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||
}
|
||||
@@ -359,16 +406,21 @@ export function lookupCdtCodes(
|
||||
const compositeMatch = parseCompositeCode(name);
|
||||
if (compositeMatch) return compositeMatch; // already carries toothNumber/toothSurface
|
||||
|
||||
// 2b. RCT by tooth# — D3310/D3320/D3330 based on tooth position
|
||||
const rctMatch = parseRctCode(name);
|
||||
if (rctMatch) return rctMatch;
|
||||
|
||||
// 3. Hardcoded direct code map (short abbreviations / codes not in MH schedule)
|
||||
if (DIRECT_CODE_MAP[cleaned]) {
|
||||
const code = DIRECT_CODE_MAP[cleaned]!;
|
||||
const directKey = DIRECT_CODE_MAP[cleaned] ? cleaned : DIRECT_CODE_MAP[strippedCleaned] ? strippedCleaned : null;
|
||||
if (directKey) {
|
||||
const code = DIRECT_CODE_MAP[directKey]!;
|
||||
const row = ALL_CODES.find((r) => r["Procedure Code"].toUpperCase() === code);
|
||||
return { code, description: row?.Description ?? code, input: name, ...toothInfo };
|
||||
}
|
||||
|
||||
// 4. Hardcoded alias + keyword search
|
||||
const match = matchOne(name);
|
||||
if (match) return { ...match, ...toothInfo };
|
||||
// 4. Hardcoded alias + keyword search (try stripped name so tooth# doesn't break matching)
|
||||
const match = matchOne(strippedCleaned !== cleaned ? strippedCleaned : name);
|
||||
if (match) return { ...match, input: name, ...toothInfo };
|
||||
|
||||
return {
|
||||
code: null,
|
||||
|
||||
@@ -11,6 +11,7 @@ export type InternalChatIntent =
|
||||
| "claim_only" // submit claim for procedures (no eligibility check)
|
||||
| "navigate_claims"
|
||||
| "navigate_schedule"
|
||||
| "navigate_eligibility"
|
||||
| "general";
|
||||
|
||||
export interface ChatClassification {
|
||||
@@ -76,6 +77,8 @@ Intents:
|
||||
Always extract appointmentDate when a date or "today" is mentioned
|
||||
- navigate_claims : open the claims page
|
||||
- navigate_schedule : open the appointments/schedule page
|
||||
- navigate_eligibility : open the insurance eligibility page
|
||||
e.g. "check mh", "check masshealth", "open eligibility", "go to eligibility", "check insurance"
|
||||
- general : anything else
|
||||
|
||||
Rules:
|
||||
@@ -83,9 +86,16 @@ Rules:
|
||||
(e.g. "perio exam", "adult cleaning", "D0120") — do NOT translate to codes
|
||||
- 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
|
||||
- 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
|
||||
- For multiple PA X-rays with tooth numbers, expand each PA into its own entry:
|
||||
"1 pa, #T" for the first tooth, "2nd pa, #T" for each additional tooth
|
||||
e.g. "2 PA (#30, 15)" → ["1 pa, #30", "2nd pa, #15"]
|
||||
e.g. "3 PA (#3, 14, 30)" → ["1 pa, #3", "2nd pa, #14", "2nd pa, #30"]
|
||||
e.g. "2 pa #3 #14" → ["1 pa, #3", "2nd pa, #14"]
|
||||
- insuranceHint is only set when the user explicitly names an insurance in the message
|
||||
- Keep fallbackReply to 1-2 sentences
|
||||
- For navigate intents, fallbackReply = "Opening the [page] page..."
|
||||
- For navigate intents, fallbackReply = "Opening the [page] page..." (e.g. "Opening the eligibility page...")
|
||||
- appointmentDate applies to BOTH schedule_appointment AND claim_only/check_and_claim:
|
||||
always set it to today's date (${today}) when the user says "today", "this visit", or similar
|
||||
set it to the specified date when the user mentions a date (e.g. "05/15/2026")
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface CdtResult {
|
||||
code: string | null;
|
||||
description: string;
|
||||
input: string;
|
||||
toothNumber?: string;
|
||||
toothSurface?: string;
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
@@ -181,6 +183,13 @@ export async function runInternalChatWorkflow(
|
||||
actionData: { url: "/appointments" },
|
||||
};
|
||||
}
|
||||
if (intent === "navigate_eligibility") {
|
||||
return {
|
||||
reply: classification.fallbackReply ?? "Opening the eligibility page...",
|
||||
action: "navigate",
|
||||
actionData: { url: "/insurance-status" },
|
||||
};
|
||||
}
|
||||
|
||||
// ── Find patient (record only, no eligibility) ──────────────────────────────
|
||||
|
||||
@@ -451,6 +460,7 @@ async function handleCheckAndClaim(
|
||||
autoCheck: siteKeyToAutoCheck(siteKey),
|
||||
cdtResults,
|
||||
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface })),
|
||||
serviceDate: c.appointmentDate ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||
import { takeChatbotPendingFiles } from "@/lib/chatbotFileStore";
|
||||
import {
|
||||
Claim,
|
||||
ClaimFileMeta,
|
||||
@@ -493,6 +494,13 @@ export function ClaimForm({
|
||||
// Prefill service lines (and optional service date) from chatbot claim_only flow
|
||||
useEffect(() => {
|
||||
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
||||
const chatbotFiles = takeChatbotPendingFiles();
|
||||
if (!raw && chatbotFiles.length === 0) return;
|
||||
|
||||
if (chatbotFiles.length > 0) {
|
||||
setForm((prev) => ({ ...prev, uploadedFiles: chatbotFiles }));
|
||||
}
|
||||
|
||||
if (!raw) return;
|
||||
try {
|
||||
const { codes, serviceDate } = JSON.parse(raw) as {
|
||||
|
||||
@@ -316,6 +316,7 @@ export default function ClaimsRecentTable({
|
||||
<TableHead>Claim No</TableHead>
|
||||
<TableHead>PreAuth No</TableHead>
|
||||
<TableHead>Patient Name</TableHead>
|
||||
<TableHead>Service Date</TableHead>
|
||||
<TableHead>Submission Date</TableHead>
|
||||
<TableHead>Insurance Provider</TableHead>
|
||||
<TableHead>Member ID</TableHead>
|
||||
@@ -400,6 +401,11 @@ export default function ClaimsRecentTable({
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900">
|
||||
{claim.serviceDate ? formatDateToHumanReadable(claim.serviceDate) : "—"}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900">
|
||||
{formatDateToHumanReadable(claim.createdAt!)}
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
Send,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
Paperclip,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useLocation } from "wouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import { setChatbotPendingFiles } from "@/lib/chatbotFileStore";
|
||||
|
||||
type Step =
|
||||
| "menu"
|
||||
@@ -59,6 +61,7 @@ interface CheckAndClaimData {
|
||||
siteKey: string;
|
||||
autoCheck: string;
|
||||
matchedCodes: { code: string; description: string }[];
|
||||
serviceDate?: string | null;
|
||||
}
|
||||
|
||||
let msgCounter = 0;
|
||||
@@ -153,10 +156,12 @@ export function ChatbotButton() {
|
||||
serviceDate: string;
|
||||
appointmentId: number | null;
|
||||
} | null>(null);
|
||||
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
|
||||
const [, setLocation] = useLocation();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const pasteRef = useRef<HTMLTextAreaElement>(null);
|
||||
const freeTextRef = useRef<HTMLTextAreaElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
@@ -204,6 +209,7 @@ export function ChatbotButton() {
|
||||
setApptSelectionData(null);
|
||||
setCdtClarificationData(null);
|
||||
setClaimReadyData(null);
|
||||
setPendingFiles([]);
|
||||
};
|
||||
|
||||
// Full reset including message history and stored session
|
||||
@@ -306,6 +312,7 @@ export function ChatbotButton() {
|
||||
patientId: checkAndClaimData.patient?.id ?? null,
|
||||
memberId: checkAndClaimData.memberId,
|
||||
dob: checkAndClaimData.dob,
|
||||
serviceDate: checkAndClaimData.serviceDate ?? null,
|
||||
})
|
||||
);
|
||||
prefillAndNavigate(checkAndClaimData.memberId, checkAndClaimData.dob, checkAndClaimData.autoCheck);
|
||||
@@ -329,7 +336,12 @@ export function ChatbotButton() {
|
||||
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text, history, clientDate });
|
||||
const data = await res.json();
|
||||
|
||||
replaceLastMsg(data.reply ?? "Sorry, I couldn't process that.");
|
||||
const claimActions = new Set(["claim_only_ready", "check_and_claim_ready", "need_appointment_selection"]);
|
||||
const attachmentSuffix =
|
||||
pendingFiles.length > 0 && claimActions.has(data.action)
|
||||
? ` (📎 ${pendingFiles.length} attachment${pendingFiles.length > 1 ? "s" : ""} will be included)`
|
||||
: "";
|
||||
replaceLastMsg((data.reply ?? "Sorry, I couldn't process that.") + attachmentSuffix);
|
||||
|
||||
if (data.action === "navigate" && data.actionData?.url) {
|
||||
setTimeout(() => { setLocation(data.actionData.url); setOpen(false); resetStep(); }, 800);
|
||||
@@ -365,6 +377,7 @@ export function ChatbotButton() {
|
||||
siteKey: data.actionData.siteKey,
|
||||
autoCheck: data.actionData.autoCheck,
|
||||
matchedCodes: data.actionData.matchedCodes ?? [],
|
||||
serviceDate: data.actionData.serviceDate ?? null,
|
||||
});
|
||||
setStep("check-and-claim-ready");
|
||||
return;
|
||||
@@ -720,6 +733,7 @@ export function ChatbotButton() {
|
||||
siteKey: data.actionData.siteKey,
|
||||
autoCheck: data.actionData.autoCheck,
|
||||
matchedCodes: data.actionData.matchedCodes ?? [],
|
||||
serviceDate: data.actionData.serviceDate ?? null,
|
||||
});
|
||||
setStep("check-and-claim-ready");
|
||||
} else if (data.action === "eligibility_id_ready" && data.actionData) {
|
||||
@@ -772,6 +786,7 @@ export function ChatbotButton() {
|
||||
autoSubmit: true,
|
||||
})
|
||||
);
|
||||
setChatbotPendingFiles(pendingFiles);
|
||||
markJobStarted();
|
||||
setTimeout(() => {
|
||||
setLocation(`/claims?appointmentId=${opt.appointmentId}`);
|
||||
@@ -826,6 +841,7 @@ export function ChatbotButton() {
|
||||
JSON.stringify({ codes: matchedCodes, siteKey, serviceDate, autoSubmit: true })
|
||||
);
|
||||
}
|
||||
setChatbotPendingFiles(pendingFiles);
|
||||
markJobStarted();
|
||||
const url = appointmentId
|
||||
? `/claims?appointmentId=${appointmentId}`
|
||||
@@ -891,6 +907,7 @@ export function ChatbotButton() {
|
||||
if (patient?.id && matchedCodes?.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({ codes: matchedCodes, siteKey, serviceDate, autoSubmit: true }));
|
||||
}
|
||||
setChatbotPendingFiles(pendingFiles);
|
||||
const url = appointmentId ? `/claims?appointmentId=${appointmentId}` : `/claims?newPatient=${patient?.id}`;
|
||||
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
|
||||
} else {
|
||||
@@ -915,7 +932,28 @@ export function ChatbotButton() {
|
||||
{/* Persistent free-text input */}
|
||||
{showFreeTextInput && (
|
||||
<div className="shrink-0 border-t bg-white px-3 py-2">
|
||||
<div className="flex items-end gap-2">
|
||||
{/* Attached file chips */}
|
||||
{pendingFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 pb-1.5">
|
||||
{pendingFiles.map((f, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="flex items-center gap-1 text-[10px] bg-blue-50 border border-blue-200 rounded px-1.5 py-0.5 text-blue-700 max-w-[140px]"
|
||||
>
|
||||
<Paperclip className="h-2.5 w-2.5 shrink-0" />
|
||||
<span className="truncate">{f.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPendingFiles((prev) => prev.filter((_, j) => j !== i))}
|
||||
className="shrink-0 hover:text-red-500 ml-0.5"
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-end gap-1.5">
|
||||
<textarea
|
||||
ref={freeTextRef}
|
||||
rows={2}
|
||||
@@ -926,6 +964,16 @@ export function ChatbotButton() {
|
||||
disabled={step === "ai-loading"}
|
||||
className="flex-1 resize-none rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-9 w-9 p-0 shrink-0 text-gray-400 hover:text-gray-600"
|
||||
title="Attach X-ray or document"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={step === "ai-loading"}
|
||||
>
|
||||
<Paperclip className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-9 w-9 p-0 shrink-0"
|
||||
@@ -939,7 +987,21 @@ export function ChatbotButton() {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 mt-1 pl-1">Enter to send · Shift+Enter for new line</p>
|
||||
<p className="text-[10px] text-gray-400 mt-1 pl-1">Enter to send · Shift+Enter for new line · 📎 attach files</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*,application/pdf"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
if (files.length > 0) {
|
||||
setPendingFiles((prev) => [...prev, ...files].slice(0, 5));
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
12
apps/Frontend/src/lib/chatbotFileStore.ts
Normal file
12
apps/Frontend/src/lib/chatbotFileStore.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
let pendingFiles: File[] = [];
|
||||
|
||||
export function setChatbotPendingFiles(files: File[]) {
|
||||
pendingFiles = [...files];
|
||||
}
|
||||
|
||||
/** Returns and clears the stored files (consume-once). */
|
||||
export function takeChatbotPendingFiles(): File[] {
|
||||
const files = [...pendingFiles];
|
||||
pendingFiles = [];
|
||||
return files;
|
||||
}
|
||||
@@ -522,6 +522,8 @@ export default function InsuranceStatusPage() {
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (claimed) return;
|
||||
setSelectedPatient(null);
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
|
||||
@@ -649,6 +651,41 @@ export default function InsuranceStatusPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// After any eligibility check succeeds, check if chatbot wanted auto-claim.
|
||||
// PDF is already saved to Documents by the Selenium worker before this is called.
|
||||
// Returns true if a chatbot claim was pending (caller should skip opening the preview).
|
||||
const tryClaimFromChatbot = async (resolvedPatientId?: number | null): Promise<boolean> => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_claim_codes");
|
||||
if (!raw) return false;
|
||||
const { codes, siteKey, patientId, memberId: storedMemberId, serviceDate } = JSON.parse(raw);
|
||||
sessionStorage.removeItem("chatbot_claim_codes");
|
||||
|
||||
let pid: number | null = resolvedPatientId ?? patientId ?? null;
|
||||
|
||||
// Fallback: look up patient by memberId if patientId wasn't stored
|
||||
if (!pid && storedMemberId) {
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/patients/by-insurance-id?insuranceId=${encodeURIComponent(storedMemberId)}`);
|
||||
if (res.ok) {
|
||||
const p = await res.json();
|
||||
pid = p?.id ?? null;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!codes?.length || !pid) return false;
|
||||
|
||||
sessionStorage.setItem(
|
||||
"chatbot_claim_prefill",
|
||||
JSON.stringify({ codes, siteKey, serviceDate: serviceDate ?? null, autoSubmit: true })
|
||||
);
|
||||
setLocation(`/claims?newPatient=${pid}`);
|
||||
return true;
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@@ -886,12 +923,13 @@ export default function InsuranceStatusPage() {
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "ddma"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (!claimed) {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -905,12 +943,13 @@ export default function InsuranceStatusPage() {
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "delta-ins"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (!claimed) {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -944,12 +983,13 @@ export default function InsuranceStatusPage() {
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "tufts-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (!claimed) {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -963,12 +1003,13 @@ export default function InsuranceStatusPage() {
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "united-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (!claimed) {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -982,12 +1023,13 @@ export default function InsuranceStatusPage() {
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "cca"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_cca_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
onPdfReady={async (pdfId, fallbackFilename) => {
|
||||
const claimed = await tryClaimFromChatbot(selectedPatient?.id);
|
||||
if (!claimed) {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(fallbackFilename ?? `eligibility_cca_${memberId}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user