feat: MH eligibility & history, CMSP eligibility & history & remaining

- Add MH Eligibility & History button: runs full MH eligibility flow then
  clicks member ID → service history, prints both PDFs via CDP, opens
  dual side-by-side PDF modal (eligibility auto-downloads, history does not)
- Add CMSP Eligibility & History & Remaining button: same flow plus
  navigates back to member details, clicks View Accumulator, prints
  accumulator PDF via CDP; opens 3-panel side-by-side PDF modal
- Generalize DualPdfPreviewModal to accept panels[] array (works for 2 or 3 PDFs)
- Auto-download eligibility PDF via direct API URL to avoid Chrome Safe
  Browsing pause on blob: URL downloads
- New backend processors, job types, and routes for both flows
- New Python Selenium workers with stable CSS selectors (ng-bind, href*)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-13 23:29:55 -04:00
parent 131733564e
commit 06526cd1bc
11 changed files with 1868 additions and 2 deletions

View File

@@ -0,0 +1,196 @@
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient";
export interface PdfPanelConfig {
pdfId?: number | null;
fallbackFilename?: string | null;
label: string;
autoDownload?: boolean;
}
interface Props {
open: boolean;
onClose: () => void;
panels: PdfPanelConfig[];
title?: string;
}
function parseFilename(header: string | null): string | null {
if (!header) return null;
const starMatch = header.match(/filename\*\s*=\s*([^;]+)/i);
if (starMatch?.[1]) {
const raw = starMatch[1].trim().replace(/^"(.*)"$/, "$1");
const parts = raw.split("''");
if (parts.length === 2 && parts[1]) {
try { return decodeURIComponent(parts[1]); } catch { return parts[1]; }
}
try { return decodeURIComponent(raw); } catch { return raw; }
}
const quoted = header.match(/filename\s*=\s*"([^"]+)"/i);
if (quoted?.[1]) return quoted[1].trim();
const plain = header.match(/filename\s*=\s*([^;]+)/i);
if (plain?.[1]) return plain[1].trim().replace(/^"(.*)"$/, "$1");
return null;
}
function usePdfBlob(open: boolean, pdfId?: number | null, fallbackFilename?: string | null) {
const [blobUrl, setBlobUrl] = useState<string | null>(null);
const [filename, setFilename] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open || !pdfId) return;
let objectUrl: string | null = null;
let aborted = false;
(async () => {
setLoading(true);
setError(null);
try {
const res = await apiRequest("GET", `/api/documents/pdf-files/${pdfId}`);
if (!res?.ok) {
const txt = await res?.text().catch(() => "");
throw new Error(txt || `Failed to fetch PDF: ${res?.status}`);
}
const header = res.headers?.get?.("content-disposition") ?? null;
const finalName = parseFilename(header) ?? fallbackFilename ?? `file_${pdfId}.pdf`;
setFilename(finalName);
const buf = await res.arrayBuffer();
if (aborted) return;
const blob = new Blob([buf], { type: "application/pdf" });
objectUrl = URL.createObjectURL(blob);
setBlobUrl(objectUrl);
} catch (e: any) {
if (e?.name === "AbortError") return;
setError(e?.message ?? "Failed to fetch PDF");
} finally {
setLoading(false);
}
})();
return () => {
aborted = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
setBlobUrl(null);
setError(null);
setLoading(false);
setFilename(null);
};
}, [open, pdfId, fallbackFilename]);
return { blobUrl, filename, loading, error };
}
function PdfPanel({ config, open }: { config: PdfPanelConfig; open: boolean }) {
const { blobUrl, filename, loading, error } = usePdfBlob(
open,
config.pdfId,
config.fallbackFilename
);
// Auto-download via direct API URL to avoid Chrome Safe Browsing pause
useEffect(() => {
if (!config.autoDownload || !config.pdfId || !filename) return;
const a = document.createElement("a");
a.href = `/api/documents/pdf-files/${config.pdfId}`;
a.download = filename;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}, [config.autoDownload, config.pdfId, filename]);
const handleDownload = () => {
if (!config.pdfId || !filename) return;
const a = document.createElement("a");
a.href = `/api/documents/pdf-files/${config.pdfId}`;
a.download = filename;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
return (
<div className="flex flex-col flex-1 min-w-0 border-r last:border-r-0">
<div className="flex items-center justify-between px-3 py-2 border-b bg-gray-50 shrink-0">
<div className="flex flex-col min-w-0">
<span className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{config.label}
</span>
<span className="text-sm font-medium truncate" title={filename ?? undefined}>
{filename ?? "Loading…"}
</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleDownload}
disabled={!config.pdfId || !filename}
className="shrink-0 ml-2"
>
Download
</Button>
</div>
<div className="flex-1 overflow-hidden p-2">
{loading && (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
Loading PDF
</div>
)}
{error && (
<div className="flex items-center justify-center h-full text-sm text-destructive">
Error: {error}
</div>
)}
{!config.pdfId && !loading && (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
No PDF available
</div>
)}
{blobUrl && (
<iframe
title={config.label}
src={blobUrl}
className="w-full h-full border rounded"
style={{ minHeight: 0 }}
/>
)}
</div>
</div>
);
}
export function DualPdfPreviewModal({ open, onClose, panels, title }: Props) {
if (!open) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div
className="bg-white rounded-lg shadow-lg flex flex-col"
style={{ width: "92vw", height: "88vh", maxWidth: 1600 }}
>
<div className="flex items-center justify-between px-4 py-3 border-b shrink-0">
<h3 className="text-base font-semibold">
{title ?? "PDF Preview"}
</h3>
<Button variant="ghost" size="sm" onClick={onClose}>
Close
</Button>
</div>
<div className="flex flex-1 min-h-0 divide-x">
{panels.map((panel, i) => (
<PdfPanel key={i} config={panel} open={open} />
))}
</div>
</div>
</div>
);
}

View File

@@ -9,6 +9,7 @@ interface Props {
onClose: () => void;
pdfId?: number | null;
fallbackFilename?: string | null;
autoDownload?: boolean;
}
function parseFilenameFromContentDisposition(header: string | null): string | null {
@@ -50,6 +51,7 @@ export function PdfPreviewModal({
onClose,
pdfId,
fallbackFilename = null,
autoDownload = false,
}: Props) {
const [fileBlobUrl, setFileBlobUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
@@ -99,6 +101,19 @@ export function PdfPreviewModal({
const blob = new Blob([arrayBuffer], { type: "application/pdf" });
objectUrl = URL.createObjectURL(blob);
setFileBlobUrl(objectUrl);
if (autoDownload) {
const a = document.createElement("a");
// Use the direct API URL so Chrome sees a proper HTTP response with
// Content-Disposition: attachment headers, which bypasses the Safe
// Browsing pause that blob: URL downloads trigger on Linux/Chrome.
a.href = `/api/documents/pdf-files/${pdfId}`;
a.download = finalName;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
} catch (err: any) {
if (err && (err.name === "AbortError" || err.message === "The user aborted a request.")) {
return;

View File

@@ -34,6 +34,7 @@ import { InsertPatient, Patient } from "@repo/db/types";
import { DateInput } from "@/components/ui/dateInput";
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
import { DualPdfPreviewModal } from "@/components/insurance-status/dual-pdf-preview-modal";
import { useLocation } from "wouter";
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-modal";
@@ -63,6 +64,9 @@ export default function InsuranceStatusPage() {
useState(false);
const [isCheckingEligibilityClaimsPreAuth, setIsCheckingEligibilityClaimsPreAuth] =
useState(false);
const [isCheckingEligibilityHistory, setIsCheckingEligibilityHistory] =
useState(false);
const [isCheckingCMSP, setIsCheckingCMSP] = useState(false);
// AI Call Insurance section
const [aiCallOpen, setAiCallOpen] = useState(false);
@@ -90,6 +94,22 @@ export default function InsuranceStatusPage() {
string | null
>(null);
// Dual PDF modal state (used by MH Eligibility & History)
const [dualPreviewOpen, setDualPreviewOpen] = useState(false);
const [dualEligibilityPdfId, setDualEligibilityPdfId] = useState<number | null>(null);
const [dualEligibilityFilename, setDualEligibilityFilename] = useState<string | null>(null);
const [dualHistoryPdfId, setDualHistoryPdfId] = useState<number | null>(null);
const [dualHistoryFilename, setDualHistoryFilename] = useState<string | null>(null);
// Triple PDF modal state (used by CMSP Eligibility & History & Remaining)
const [cmspPreviewOpen, setCmspPreviewOpen] = useState(false);
const [cmspEligibilityPdfId, setCmspEligibilityPdfId] = useState<number | null>(null);
const [cmspEligibilityFilename, setCmspEligibilityFilename] = useState<string | null>(null);
const [cmspHistoryPdfId, setCmspHistoryPdfId] = useState<number | null>(null);
const [cmspHistoryFilename, setCmspHistoryFilename] = useState<string | null>(null);
const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null);
const [cmspAccumulatorFilename, setCmspAccumulatorFilename] = useState<string | null>(null);
// Populate fields from selected patient
useEffect(() => {
if (selectedPatient) {
@@ -386,6 +406,166 @@ export default function InsuranceStatusPage() {
}
};
// MH Eligibility & History — eligibility check + service history, saves both PDFs, auto-downloads eligibility PDF only
const handleMHEligibilityHistoryButton = async () => {
if (!memberId || !dateOfBirth) {
toast({
title: "Missing Fields",
description: "Please fill in all the required fields: Member ID, Date of Birth.",
variant: "destructive",
});
return;
}
setIsCheckingEligibilityHistory(true);
try {
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
const data = {
memberId,
dateOfBirth: formattedDob,
insuranceSiteKey: "MH",
firstName: firstName || undefined,
lastName: lastName || undefined,
};
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
const response = await apiRequest(
"POST",
"/api/insurance-status/eligibility-history-check",
{ data, socketId: socket.id },
);
const enqueueResult = await response.json();
if (enqueueResult.error) throw new Error(enqueueResult.error);
const jobId = enqueueResult.jobId;
if (!jobId) throw new Error("No jobId returned from server");
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
const jobResult = await new Promise<any>((resolve, reject) => {
const handler = (payload: any) => {
if (String(payload.jobId) !== String(jobId)) return;
if (payload.status === "active") {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: payload.message ?? "Selenium running..." }));
} else if (payload.status === "completed") {
socket.off("job:update", handler);
resolve(payload.result ?? {});
} else if (payload.status === "failed") {
socket.off("job:update", handler);
reject(new Error(payload.error ?? "Selenium job failed"));
}
};
socket.on("job:update", handler);
});
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "Eligibility and service history PDFs saved to Documents." }));
toast({
title: "Done",
description: "Eligibility and service history PDFs saved. Eligibility PDF downloading now.",
variant: "default",
});
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
// Open both PDFs side by side in the dual modal
if (jobResult.pdfFileId || jobResult.historyPdfFileId) {
setDualEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
setDualEligibilityFilename(jobResult.pdfFilename ?? `eligibility_${memberId}.pdf`);
setDualHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
setDualHistoryFilename(jobResult.historyPdfFilename ?? `eligibility_history_${memberId}.pdf`);
setDualPreviewOpen(true);
}
} catch (error: any) {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
} finally {
setIsCheckingEligibilityHistory(false);
}
};
// CMSP Eligibility & History & Remaining — eligibility + service history + accumulator PDFs
const handleCMSPButton = async () => {
if (!memberId || !dateOfBirth) {
toast({
title: "Missing Fields",
description: "Please fill in Member ID and Date of Birth.",
variant: "destructive",
});
return;
}
setIsCheckingCMSP(true);
try {
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
const data = {
memberId,
dateOfBirth: formattedDob,
insuranceSiteKey: "MH",
firstName: firstName || undefined,
lastName: lastName || undefined,
};
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Sending Data to Selenium..." }));
const response = await apiRequest(
"POST",
"/api/insurance-status/cmsp-eligibility-history-remaining-check",
{ data, socketId: socket.id },
);
const enqueueResult = await response.json();
if (enqueueResult.error) throw new Error(enqueueResult.error);
const jobId = enqueueResult.jobId;
if (!jobId) throw new Error("No jobId returned from server");
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: "Selenium browser starting..." }));
const jobResult = await new Promise<any>((resolve, reject) => {
const handler = (payload: any) => {
if (String(payload.jobId) !== String(jobId)) return;
if (payload.status === "active") {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "pending", message: payload.message ?? "Selenium running..." }));
} else if (payload.status === "completed") {
socket.off("job:update", handler);
resolve(payload.result ?? {});
} else if (payload.status === "failed") {
socket.off("job:update", handler);
reject(new Error(payload.error ?? "Selenium job failed"));
}
};
socket.on("job:update", handler);
});
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "success", message: "CMSP PDFs saved to Documents." }));
toast({
title: "Done",
description: "Eligibility, history, and accumulator PDFs saved.",
variant: "default",
});
setSelectedPatient(null);
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
// Open 3-panel modal
if (jobResult.pdfFileId || jobResult.historyPdfFileId || jobResult.accumulatorPdfFileId) {
setCmspEligibilityPdfId(jobResult.pdfFileId ? Number(jobResult.pdfFileId) : null);
setCmspEligibilityFilename(jobResult.pdfFilename ?? `cmsp_eligibility_${memberId}.pdf`);
setCmspHistoryPdfId(jobResult.historyPdfFileId ? Number(jobResult.historyPdfFileId) : null);
setCmspHistoryFilename(jobResult.historyPdfFilename ?? `cmsp_history_${memberId}.pdf`);
setCmspAccumulatorPdfId(jobResult.accumulatorPdfFileId ? Number(jobResult.accumulatorPdfFileId) : null);
setCmspAccumulatorFilename(jobResult.accumulatorPdfFilename ?? `cmsp_accumulator_${memberId}.pdf`);
setCmspPreviewOpen(true);
}
} catch (error: any) {
dispatch(setTaskStatus({ key: "eligibilityCheck", status: "error", message: error.message || "Selenium submission failed" }));
toast({ title: "Selenium service error", description: error.message || "An error occurred.", variant: "destructive" });
} finally {
setIsCheckingCMSP(false);
}
};
// small helper: remove given query params from the current URL (silent, no reload)
const clearUrlParams = (params: string[]) => {
try {
@@ -620,6 +800,44 @@ export default function InsuranceStatusPage() {
</Button>
</div>
<div className="flex flex-col-2 gap-4 mt-4">
<Button
className="w-full"
disabled={isCheckingEligibilityHistory}
onClick={() => handleMHEligibilityHistoryButton()}
>
{isCheckingEligibilityHistory ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
MH Eligibility &amp; History
</>
)}
</Button>
<Button
className="w-full"
disabled={isCheckingCMSP}
onClick={() => handleCMSPButton()}
>
{isCheckingCMSP ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Processing...
</>
) : (
<>
<CheckCircle className="h-4 w-4 mr-2" />
CMSP Eligibility &amp; History &amp; Remaining
</>
)}
</Button>
</div>
{/* TEMP PROVIDER BUTTONS */}
<div className="space-y-4 mt-6">
<h3 className="text-sm font-medium text-muted-foreground">
@@ -937,7 +1155,67 @@ export default function InsuranceStatusPage() {
setPreviewFallbackFilename(null);
}}
pdfId={previewPdfId ?? undefined}
fallbackFilename={previewFallbackFilename ?? undefined} // optional
fallbackFilename={previewFallbackFilename ?? undefined}
autoDownload
/>
{/* Triple PDF modal for CMSP — eligibility, history, accumulator side by side */}
<DualPdfPreviewModal
open={cmspPreviewOpen}
onClose={() => {
setCmspPreviewOpen(false);
setCmspEligibilityPdfId(null);
setCmspEligibilityFilename(null);
setCmspHistoryPdfId(null);
setCmspHistoryFilename(null);
setCmspAccumulatorPdfId(null);
setCmspAccumulatorFilename(null);
}}
title="CMSP Eligibility, History &amp; Remaining"
panels={[
{
pdfId: cmspEligibilityPdfId,
fallbackFilename: cmspEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: cmspHistoryPdfId,
fallbackFilename: cmspHistoryFilename,
label: "Service History",
},
{
pdfId: cmspAccumulatorPdfId,
fallbackFilename: cmspAccumulatorFilename,
label: "Accumulator (Remaining)",
},
]}
/>
{/* Dual PDF modal for MH Eligibility & History — both PDFs side by side */}
<DualPdfPreviewModal
open={dualPreviewOpen}
onClose={() => {
setDualPreviewOpen(false);
setDualEligibilityPdfId(null);
setDualEligibilityFilename(null);
setDualHistoryPdfId(null);
setDualHistoryFilename(null);
}}
title="MH Eligibility &amp; Service History"
panels={[
{
pdfId: dualEligibilityPdfId,
fallbackFilename: dualEligibilityFilename,
label: "Eligibility",
autoDownload: true,
},
{
pdfId: dualHistoryPdfId,
fallbackFilename: dualHistoryFilename,
label: "Service History",
},
]}
/>
</div>
);