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:
@@ -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 & 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 & History & 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 & 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 & Service History"
|
||||
panels={[
|
||||
{
|
||||
pdfId: dualEligibilityPdfId,
|
||||
fallbackFilename: dualEligibilityFilename,
|
||||
label: "Eligibility",
|
||||
autoDownload: true,
|
||||
},
|
||||
{
|
||||
pdfId: dualHistoryPdfId,
|
||||
fallbackFilename: dualHistoryFilename,
|
||||
label: "Service History",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user