feat: add multi_claim intent for different procedures per patient + fix batch claim PDF race
- Add multi_claim intent so AI correctly handles "claim X for patient A and Y for patient B" instead of applying all procedures to all patients (batch_claim) - Each patient carries their own matchedCodes in the queue - Fix batch claim PDF race condition: chatbot queue no longer advances in closeClaim(), instead advances after selenium PDF is downloaded (matching column claim behavior) - Align United SCO eligibility worker with claim worker: only fill subscriber ID + DOB, use treatmentLocation by ID instead of arrow-wrapper click Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ type Step =
|
||||
| "need-cdt-clarification"
|
||||
| "claim-ready"
|
||||
| "batch-claim-ready"
|
||||
| "multi-claim-ready"
|
||||
| "batch-check-and-claim-ready"
|
||||
| "preauth-ready";
|
||||
|
||||
@@ -589,6 +590,16 @@ export function ChatbotButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "multi_claim_ready" && data.actionData) {
|
||||
setBatchClaimData({
|
||||
queue: data.actionData.queue ?? [],
|
||||
matchedCodes: [],
|
||||
renderingProvider: data.actionData.renderingProvider ?? null,
|
||||
});
|
||||
setStep("multi-claim-ready");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.action === "claim_only_ready" && data.actionData) {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
||||
setClaimReadyData({
|
||||
@@ -638,6 +649,7 @@ export function ChatbotButton() {
|
||||
step === "batch-eligibility-ready" ||
|
||||
step === "check-and-claim-ready" ||
|
||||
step === "batch-claim-ready" ||
|
||||
step === "multi-claim-ready" ||
|
||||
step === "batch-check-and-claim-ready" ||
|
||||
step === "need-insurance-clarification" ||
|
||||
step === "need-appointment-selection";
|
||||
@@ -1193,6 +1205,72 @@ export function ChatbotButton() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi claim ready — different procedures per patient */}
|
||||
{step === "multi-claim-ready" && batchClaimData && (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-orange-800">
|
||||
Claim for {batchClaimData.queue.length} patients:
|
||||
</p>
|
||||
{batchClaimData.queue.map((item: any, i: number) => (
|
||||
<div key={i} className="pl-2 space-y-0.5">
|
||||
<p className="text-xs text-orange-600 font-medium">
|
||||
{i + 1}. {item.patient.firstName} {item.patient.lastName}
|
||||
</p>
|
||||
{item.matchedCodes?.length > 0 && (
|
||||
<div className="pl-3">
|
||||
{item.matchedCodes.map((c: any) => (
|
||||
<p key={c.code} className="text-xs text-gray-700">
|
||||
<span className="font-medium">{c.code}</span> — {c.description}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8 text-xs bg-orange-600 hover:bg-orange-700 text-white"
|
||||
onClick={() => {
|
||||
const { queue, renderingProvider } = batchClaimData;
|
||||
addMsg("user", `Claim all ${queue.length} patients`);
|
||||
addMsg("bot", `Claiming ${queue.length} patients one by one...`);
|
||||
const [first, ...rest] = queue as any[];
|
||||
if (rest.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({
|
||||
remaining: rest,
|
||||
matchedCodes: null,
|
||||
renderingProvider,
|
||||
}));
|
||||
}
|
||||
if (first!.patient.id && first!.matchedCodes?.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||
codes: first!.matchedCodes,
|
||||
siteKey: first!.siteKey,
|
||||
serviceDate: first!.serviceDate,
|
||||
autoSubmit: true,
|
||||
renderingProvider: renderingProvider ?? null,
|
||||
dob: first!.patient.dateOfBirth ?? null,
|
||||
}));
|
||||
}
|
||||
setChatbotPendingFiles(pendingFiles);
|
||||
markJobStarted();
|
||||
const url = first!.appointmentId
|
||||
? `/claims?appointmentId=${first!.appointmentId}`
|
||||
: `/claims?newPatient=${first!.patient.id}`;
|
||||
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
|
||||
}}
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
Claim All ({batchClaimData.queue.length} patients)
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Claim ready — confirm before submitting */}
|
||||
{step === "claim-ready" && claimReadyData && (() => {
|
||||
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
|
||||
|
||||
@@ -143,6 +143,7 @@ export default function ClaimsPage() {
|
||||
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
||||
if (jobResult.pdfFileId) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
setPreviewPdfId(jobResult.pdfFileId);
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||
setPreviewOpen(true);
|
||||
@@ -333,6 +334,82 @@ export default function ClaimsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Advance the chatbot_claim_queue / chatbot_check_claim_queue after selenium PDF is saved.
|
||||
// NOT called from closeClaim — called only after the selenium job completes and PDF is downloaded.
|
||||
const advanceChatbotClaimQueue = () => {
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const remaining = parsed?.remaining as any[] | undefined;
|
||||
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||
if (remaining && remaining.length > 0) {
|
||||
const [next, ...rest] = remaining;
|
||||
if (rest.length > 0) {
|
||||
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||
} else {
|
||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||
}
|
||||
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
||||
codes: matchedCodes,
|
||||
siteKey: next.siteKey,
|
||||
patientId: next.patient?.id ?? null,
|
||||
memberId: next.memberId,
|
||||
dob: next.dob,
|
||||
serviceDate: null,
|
||||
renderingProvider,
|
||||
}));
|
||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
||||
memberId: next.memberId,
|
||||
dob: next.dob,
|
||||
autoCheck: next.autoCheck,
|
||||
}));
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setWouterLocation("/insurance-status");
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const remaining = parsed?.remaining as any[] | undefined;
|
||||
const sharedMatchedCodes = parsed?.matchedCodes ?? [];
|
||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||
if (remaining && remaining.length > 0) {
|
||||
const [next, ...rest] = remaining;
|
||||
if (rest.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes: sharedMatchedCodes, renderingProvider }));
|
||||
} else {
|
||||
sessionStorage.removeItem("chatbot_claim_queue");
|
||||
}
|
||||
const codes = next.matchedCodes?.length > 0 ? next.matchedCodes : sharedMatchedCodes;
|
||||
if (next.patient?.id && codes.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||
codes,
|
||||
siteKey: next.siteKey,
|
||||
serviceDate: next.serviceDate,
|
||||
autoSubmit: true,
|
||||
renderingProvider,
|
||||
dob: next.patient.dateOfBirth ?? null,
|
||||
}));
|
||||
}
|
||||
setTimeout(() => {
|
||||
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_claim_queue");
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
// Advance the ai_claim_queue by removing the first item (current patient).
|
||||
// Called only on successful submission so that cancel leaves the queue intact.
|
||||
const advanceAiClaimQueue = () => {
|
||||
@@ -370,7 +447,10 @@ export default function ClaimsPage() {
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
||||
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
||||
if (!isDraft) advanceAiClaimQueue();
|
||||
if (!isDraft) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -884,7 +964,10 @@ export default function ClaimsPage() {
|
||||
duration: isPreAuth ? 10000 : 5000,
|
||||
});
|
||||
|
||||
if (!isPreAuth) advanceAiClaimQueue();
|
||||
if (!isPreAuth) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
}
|
||||
|
||||
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
||||
if (result.pdfFileId) {
|
||||
@@ -933,78 +1016,10 @@ export default function ClaimsPage() {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If a chatbot batch check+claim queue is pending, navigate to eligibility for the next patient
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const remaining = parsed?.remaining as any[] | undefined;
|
||||
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||
if (remaining && remaining.length > 0) {
|
||||
const [next, ...rest] = remaining;
|
||||
if (rest.length > 0) {
|
||||
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||
} else {
|
||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||
}
|
||||
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
||||
codes: matchedCodes,
|
||||
siteKey: next.siteKey,
|
||||
patientId: next.patient?.id ?? null,
|
||||
memberId: next.memberId,
|
||||
dob: next.dob,
|
||||
serviceDate: null,
|
||||
renderingProvider,
|
||||
}));
|
||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
||||
memberId: next.memberId,
|
||||
dob: next.dob,
|
||||
autoCheck: next.autoCheck,
|
||||
}));
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setWouterLocation("/insurance-status");
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If a chatbot batch claim queue is pending, open the next patient
|
||||
try {
|
||||
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
const remaining = parsed?.remaining as any[] | undefined;
|
||||
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||
if (remaining && remaining.length > 0) {
|
||||
const [next, ...rest] = remaining;
|
||||
if (rest.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||
} else {
|
||||
sessionStorage.removeItem("chatbot_claim_queue");
|
||||
}
|
||||
if (next.patient?.id && matchedCodes.length > 0) {
|
||||
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||
codes: matchedCodes,
|
||||
siteKey: next.siteKey,
|
||||
serviceDate: next.serviceDate,
|
||||
autoSubmit: true,
|
||||
renderingProvider,
|
||||
dob: next.patient.dateOfBirth ?? null,
|
||||
}));
|
||||
}
|
||||
setTimeout(() => {
|
||||
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
sessionStorage.removeItem("chatbot_claim_queue");
|
||||
}
|
||||
} catch {}
|
||||
// Chatbot batch queues (chatbot_claim_queue, chatbot_check_claim_queue) are NOT
|
||||
// advanced here — they are advanced in advanceChatbotClaimQueue() which is called
|
||||
// only after the selenium job completes and PDF is downloaded. This prevents the
|
||||
// next patient's claim from starting before the current patient's PDF is saved.
|
||||
};
|
||||
|
||||
// Pre Auth section
|
||||
|
||||
Reference in New Issue
Block a user