feat: auto-trigger eligibility selenium from schedule right-click menu
- Remove "Claim Status" from appointment context menu - Rename "Eligibility Status" → "Check Eligibility" - Check Eligibility now navigates to insurance-status page and auto-starts the correct selenium flow based on the patient's stored insurance provider: MassHealth 21+ → MH Eligibility & History MassHealth <21 → CMSP Eligibility & History & Remaining Delta Dental MA → DDMA selenium Delta Dental Ins → Delta Ins selenium (OTP modal if needed) United Healthcare SCO → United SCO selenium DentaQuest/Tufts → Tufts SCO selenium Commonwealth Care Alliance → CCA selenium Unknown → scroll to Other provider checks section - Add autoTrigger/onAutoTriggered props to all five button components Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CheckCircle, LoaderCircleIcon } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -17,6 +17,8 @@ interface CCAEligibilityButtonProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isFormIncomplete: boolean;
|
||||
autoTrigger?: boolean;
|
||||
onAutoTriggered?: () => void;
|
||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||
}
|
||||
|
||||
@@ -26,12 +28,15 @@ export function CCAEligibilityButton({
|
||||
firstName,
|
||||
lastName,
|
||||
isFormIncomplete,
|
||||
autoTrigger,
|
||||
onAutoTriggered,
|
||||
onPdfReady,
|
||||
}: CCAEligibilityButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const autoTriggeredRef = useRef(false);
|
||||
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
|
||||
@@ -181,6 +186,14 @@ export function CCAEligibilityButton({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
|
||||
autoTriggeredRef.current = true;
|
||||
onAutoTriggered?.();
|
||||
handleStart();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoTrigger, isFormIncomplete]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="w-full"
|
||||
|
||||
@@ -88,6 +88,8 @@ interface DdmaEligibilityButtonProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isFormIncomplete: boolean;
|
||||
autoTrigger?: boolean;
|
||||
onAutoTriggered?: () => void;
|
||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||
}
|
||||
|
||||
@@ -97,15 +99,15 @@ export function DdmaEligibilityButton({
|
||||
firstName,
|
||||
lastName,
|
||||
isFormIncomplete,
|
||||
autoTrigger,
|
||||
onAutoTriggered,
|
||||
onPdfReady,
|
||||
}: DdmaEligibilityButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// session_id is provided by the backend once the Python agent starts the
|
||||
// browser session. We receive it via the selenium:ddma_session_started event
|
||||
// and need it to forward the OTP back.
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const autoTriggeredRef = useRef(false);
|
||||
|
||||
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
@@ -390,6 +392,14 @@ export function DdmaEligibilityButton({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
|
||||
autoTriggeredRef.current = true;
|
||||
onAutoTriggered?.();
|
||||
handleDdmaStart();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoTrigger, isFormIncomplete]);
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
|
||||
@@ -87,6 +87,8 @@ interface DeltaInsEligibilityButtonProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isFormIncomplete: boolean;
|
||||
autoTrigger?: boolean;
|
||||
onAutoTriggered?: () => void;
|
||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||
}
|
||||
|
||||
@@ -96,12 +98,15 @@ export function DeltaInsEligibilityButton({
|
||||
firstName,
|
||||
lastName,
|
||||
isFormIncomplete,
|
||||
autoTrigger,
|
||||
onAutoTriggered,
|
||||
onPdfReady,
|
||||
}: DeltaInsEligibilityButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const autoTriggeredRef = useRef(false);
|
||||
|
||||
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
@@ -321,6 +326,14 @@ export function DeltaInsEligibilityButton({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
|
||||
autoTriggeredRef.current = true;
|
||||
onAutoTriggered?.();
|
||||
handleStart();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoTrigger, isFormIncomplete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -87,6 +87,8 @@ interface TuftsSCOEligibilityButtonProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isFormIncomplete: boolean;
|
||||
autoTrigger?: boolean;
|
||||
onAutoTriggered?: () => void;
|
||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||
}
|
||||
|
||||
@@ -96,12 +98,15 @@ export function TuftsSCOEligibilityButton({
|
||||
firstName,
|
||||
lastName,
|
||||
isFormIncomplete,
|
||||
autoTrigger,
|
||||
onAutoTriggered,
|
||||
onPdfReady,
|
||||
}: TuftsSCOEligibilityButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const autoTriggeredRef = useRef(false);
|
||||
|
||||
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
@@ -318,6 +323,14 @@ export function TuftsSCOEligibilityButton({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
|
||||
autoTriggeredRef.current = true;
|
||||
onAutoTriggered?.();
|
||||
handleStart();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoTrigger, isFormIncomplete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -87,6 +87,8 @@ interface UnitedSCOEligibilityButtonProps {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isFormIncomplete: boolean;
|
||||
autoTrigger?: boolean;
|
||||
onAutoTriggered?: () => void;
|
||||
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||
}
|
||||
|
||||
@@ -96,12 +98,15 @@ export function UnitedSCOEligibilityButton({
|
||||
firstName,
|
||||
lastName,
|
||||
isFormIncomplete,
|
||||
autoTrigger,
|
||||
onAutoTriggered,
|
||||
onPdfReady,
|
||||
}: UnitedSCOEligibilityButtonProps) {
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const autoTriggeredRef = useRef(false);
|
||||
|
||||
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||
const [isStarting, setIsStarting] = useState(false);
|
||||
@@ -318,6 +323,14 @@ export function UnitedSCOEligibilityButton({
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
|
||||
autoTriggeredRef.current = true;
|
||||
onAutoTriggered?.();
|
||||
handleStart();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoTrigger, isFormIncomplete]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -955,15 +955,42 @@ export default function AppointmentsPage() {
|
||||
// -------------------
|
||||
// appointment page — update these handlers
|
||||
const handleCheckEligibility = (appointmentId: number) => {
|
||||
setLocation(
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=eligibility`
|
||||
);
|
||||
};
|
||||
const apt = appointments.find((a) => a.id === appointmentId);
|
||||
const patient = apt ? patientsFromDay.find((p) => p.id === apt.patientId) : null;
|
||||
const insuranceProvider = (patient as any)?.insuranceProvider as string | null ?? null;
|
||||
const p = insuranceProvider?.toLowerCase() ?? "";
|
||||
const isMassHealth = p.includes("masshealth");
|
||||
|
||||
const handleCheckClaimStatus = (appointmentId: number) => {
|
||||
setLocation(
|
||||
`/insurance-status?appointmentId=${appointmentId}&action=claim`
|
||||
);
|
||||
let autoCheck = "other-providers";
|
||||
|
||||
if (isMassHealth) {
|
||||
const dob = (patient as any)?.dateOfBirth;
|
||||
let age: number | null = null;
|
||||
if (dob) {
|
||||
const dobDate = typeof dob === "string" ? new Date(dob) : dob as Date;
|
||||
const today = new Date();
|
||||
age = today.getFullYear() - dobDate.getFullYear();
|
||||
const m = today.getMonth() - dobDate.getMonth();
|
||||
if (m < 0 || (m === 0 && today.getDate() < dobDate.getDate())) age--;
|
||||
}
|
||||
autoCheck = age !== null && age >= 21 ? "mh-history" : "cmsp";
|
||||
} else if (p.includes("delta dental ma")) {
|
||||
autoCheck = "ddma";
|
||||
} else if (p.includes("delta dental ins")) {
|
||||
autoCheck = "delta-ins";
|
||||
} else if (p.includes("united healthcare sco") || p.includes("united sco")) {
|
||||
autoCheck = "united-sco";
|
||||
} else if (p.includes("tufts") || p.includes("dentaquest")) {
|
||||
autoCheck = "tufts-sco";
|
||||
} else if (p.includes("commonwealth care alliance") || p.includes("cca")) {
|
||||
autoCheck = "cca";
|
||||
}
|
||||
|
||||
if (autoCheck === "other-providers") {
|
||||
setLocation(`/insurance-status?appointmentId=${appointmentId}&scrollTo=other-providers`);
|
||||
} else {
|
||||
setLocation(`/insurance-status?appointmentId=${appointmentId}&autoCheck=${autoCheck}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClaimsPreAuth = (appointmentId: number) => {
|
||||
@@ -1602,7 +1629,7 @@ export default function AppointmentsPage() {
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Eligibility Status
|
||||
Check Eligibility
|
||||
</span>
|
||||
</Item>
|
||||
|
||||
@@ -1650,16 +1677,6 @@ export default function AppointmentsPage() {
|
||||
</span>
|
||||
</Item>
|
||||
|
||||
{/* Claim Status */}
|
||||
<Item
|
||||
onClick={({ props }) => handleCheckClaimStatus(props.appointmentId)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-4 w-4" />
|
||||
Claim Status
|
||||
</span>
|
||||
</Item>
|
||||
|
||||
{/* Chat */}
|
||||
<Item onClick={({ props }) => handleChat(props.appointmentId)}>
|
||||
<span className="flex items-center gap-2 text-blue-600">
|
||||
|
||||
@@ -164,7 +164,9 @@ export default function InsuranceStatusPage() {
|
||||
const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null);
|
||||
const [cmspAccumulatorFilename, setCmspAccumulatorFilename] = useState<string | null>(null);
|
||||
|
||||
const pendingAutoCheck = useRef<"mh" | "cmsp" | null>(null);
|
||||
const pendingAutoCheck = useRef<"mh" | "mh-history" | "cmsp" | "ddma" | "delta-ins" | "united-sco" | "tufts-sco" | "cca" | null>(null);
|
||||
const [triggerTarget, setTriggerTarget] = useState<string | null>(null);
|
||||
const pendingScrollTo = useRef<string | null>(null);
|
||||
|
||||
// Prefill from chatbot
|
||||
useEffect(() => {
|
||||
@@ -603,15 +605,18 @@ export default function InsuranceStatusPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-trigger from chatbot after prefill
|
||||
// Auto-trigger after prefill (from chatbot or schedule page "Check Eligibility")
|
||||
useEffect(() => {
|
||||
if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return;
|
||||
const check = pendingAutoCheck.current;
|
||||
pendingAutoCheck.current = null;
|
||||
if (check === "mh") {
|
||||
if (check === "mh" || check === "mh-history") {
|
||||
handleMHEligibilityHistoryButton();
|
||||
} else {
|
||||
} else if (check === "cmsp") {
|
||||
handleCMSPButton();
|
||||
} else {
|
||||
// Button-component-handled providers: pass target to their autoTrigger prop
|
||||
setTriggerTarget(check);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [memberId, dateOfBirth]);
|
||||
@@ -635,66 +640,7 @@ export default function InsuranceStatusPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// handling case-1, when redirect happens from appointment page:
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const appointmentId = params.get("appointmentId");
|
||||
const action = params.get("action"); // 'eligibility' | 'claim'
|
||||
if (!appointmentId) return;
|
||||
const id = Number(appointmentId);
|
||||
if (Number.isNaN(id) || id <= 0) return;
|
||||
if (!action || (action !== "eligibility" && action !== "claim")) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/appointments/${id}/patient`);
|
||||
if (!res.ok) {
|
||||
let body: any = null;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch {}
|
||||
if (!cancelled) {
|
||||
toast({
|
||||
title: "Failed to load patient",
|
||||
description:
|
||||
body?.message ??
|
||||
body?.error ??
|
||||
`Could not fetch patient for appointment ${id}.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const patient = data?.patient ?? data;
|
||||
if (!cancelled && patient) {
|
||||
// set selectedPatient as before
|
||||
setSelectedPatient(patient as Patient);
|
||||
|
||||
clearUrlParams(["appointmentId", "action"]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
console.error("Error fetching patient for appointment:", err);
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err?.message ?? "An error occurred while fetching patient.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
// handling case-1, when redirect happens from appointment page:
|
||||
// Redirect from schedule page "Check Eligibility": prefill patient + optionally auto-trigger or scroll
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const appointmentId = params.get("appointmentId");
|
||||
@@ -703,6 +649,16 @@ export default function InsuranceStatusPage() {
|
||||
const id = Number(appointmentId);
|
||||
if (Number.isNaN(id) || id <= 0) return;
|
||||
|
||||
const autoCheck = params.get("autoCheck") as typeof pendingAutoCheck.current;
|
||||
const scrollTo = params.get("scrollTo");
|
||||
|
||||
const knownAutoChecks = ["mh", "mh-history", "cmsp", "ddma", "delta-ins", "united-sco", "tufts-sco", "cca"] as const;
|
||||
if (autoCheck && (knownAutoChecks as readonly string[]).includes(autoCheck)) {
|
||||
pendingAutoCheck.current = autoCheck;
|
||||
} else if (scrollTo) {
|
||||
pendingScrollTo.current = scrollTo;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
@@ -714,11 +670,8 @@ export default function InsuranceStatusPage() {
|
||||
const patient = data?.patient ?? data;
|
||||
|
||||
if (!cancelled && patient) {
|
||||
// ✅ ONLY prefill patient
|
||||
setSelectedPatient(patient as Patient);
|
||||
|
||||
// ✅ clean URL (no auto selenium)
|
||||
clearUrlParams(["appointmentId", "action"]);
|
||||
clearUrlParams(["appointmentId", "action", "autoCheck", "scrollTo"]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch patient from appointment", err);
|
||||
@@ -730,6 +683,23 @@ export default function InsuranceStatusPage() {
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
// Scroll to highlighted button after patient is loaded from schedule page (other insurance)
|
||||
useEffect(() => {
|
||||
if (!selectedPatient || !pendingScrollTo.current) return;
|
||||
const target = pendingScrollTo.current;
|
||||
pendingScrollTo.current = null;
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`eligibility-${target}`);
|
||||
if (!el) return;
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("ring-2", "ring-blue-500", "ring-offset-2", "transition-all");
|
||||
setTimeout(() => {
|
||||
el.classList.remove("ring-2", "ring-blue-500", "ring-offset-2", "transition-all");
|
||||
}, 2500);
|
||||
}, 300);
|
||||
}, [selectedPatient]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SeleniumTaskBanner
|
||||
@@ -852,6 +822,7 @@ export default function InsuranceStatusPage() {
|
||||
|
||||
<div className="flex flex-col-2 gap-4 mt-4">
|
||||
<Button
|
||||
id="eligibility-mh-history"
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityHistory}
|
||||
onClick={() => handleMHEligibilityHistoryButton()}
|
||||
@@ -870,6 +841,7 @@ export default function InsuranceStatusPage() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
id="eligibility-cmsp"
|
||||
className="w-full"
|
||||
disabled={isCheckingCMSP}
|
||||
onClick={() => handleCMSPButton()}
|
||||
@@ -889,7 +861,7 @@ export default function InsuranceStatusPage() {
|
||||
</div>
|
||||
|
||||
{/* TEMP PROVIDER BUTTONS */}
|
||||
<div className="space-y-4 mt-6">
|
||||
<div id="eligibility-other-providers" className="space-y-4 mt-6">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Other provider checks
|
||||
</h3>
|
||||
@@ -902,6 +874,8 @@ export default function InsuranceStatusPage() {
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "ddma"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
@@ -917,6 +891,8 @@ export default function InsuranceStatusPage() {
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "delta-ins"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
@@ -944,10 +920,12 @@ export default function InsuranceStatusPage() {
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "tufts-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`,
|
||||
fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
@@ -959,6 +937,8 @@ export default function InsuranceStatusPage() {
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "united-sco"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
@@ -974,6 +954,8 @@ export default function InsuranceStatusPage() {
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
autoTrigger={triggerTarget === "cca"}
|
||||
onAutoTriggered={() => setTriggerTarget(null)}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
|
||||
Reference in New Issue
Block a user