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:
Gitead
2026-05-17 00:12:09 -04:00
parent 4127861d06
commit edec03e893
7 changed files with 154 additions and 93 deletions

View File

@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { CheckCircle, LoaderCircleIcon } from "lucide-react"; import { CheckCircle, LoaderCircleIcon } from "lucide-react";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
@@ -17,6 +17,8 @@ interface CCAEligibilityButtonProps {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
isFormIncomplete: boolean; isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
} }
@@ -26,12 +28,15 @@ export function CCAEligibilityButton({
firstName, firstName,
lastName, lastName,
isFormIncomplete, isFormIncomplete,
autoTrigger,
onAutoTriggered,
onPdfReady, onPdfReady,
}: CCAEligibilityButtonProps) { }: CCAEligibilityButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const sessionIdRef = useRef<string | null>(null); const sessionIdRef = useRef<string | null>(null);
const autoTriggeredRef = useRef(false);
const [isStarting, setIsStarting] = useState(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 ( return (
<Button <Button
className="w-full" className="w-full"

View File

@@ -88,6 +88,8 @@ interface DdmaEligibilityButtonProps {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
isFormIncomplete: boolean; isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
} }
@@ -97,15 +99,15 @@ export function DdmaEligibilityButton({
firstName, firstName,
lastName, lastName,
isFormIncomplete, isFormIncomplete,
autoTrigger,
onAutoTriggered,
onPdfReady, onPdfReady,
}: DdmaEligibilityButtonProps) { }: DdmaEligibilityButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const dispatch = useAppDispatch(); 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 sessionIdRef = useRef<string | null>(null);
const autoTriggeredRef = useRef(false);
const [otpModalOpen, setOtpModalOpen] = useState(false); const [otpModalOpen, setOtpModalOpen] = useState(false);
const [isStarting, setIsStarting] = 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 ──────────────────────────────────────────────────────────────── // ── Render ────────────────────────────────────────────────────────────────
return ( return (

View File

@@ -87,6 +87,8 @@ interface DeltaInsEligibilityButtonProps {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
isFormIncomplete: boolean; isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
} }
@@ -96,12 +98,15 @@ export function DeltaInsEligibilityButton({
firstName, firstName,
lastName, lastName,
isFormIncomplete, isFormIncomplete,
autoTrigger,
onAutoTriggered,
onPdfReady, onPdfReady,
}: DeltaInsEligibilityButtonProps) { }: DeltaInsEligibilityButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const sessionIdRef = useRef<string | null>(null); const sessionIdRef = useRef<string | null>(null);
const autoTriggeredRef = useRef(false);
const [otpModalOpen, setOtpModalOpen] = useState(false); const [otpModalOpen, setOtpModalOpen] = useState(false);
const [isStarting, setIsStarting] = 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 ( return (
<> <>
<Button <Button

View File

@@ -87,6 +87,8 @@ interface TuftsSCOEligibilityButtonProps {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
isFormIncomplete: boolean; isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
} }
@@ -96,12 +98,15 @@ export function TuftsSCOEligibilityButton({
firstName, firstName,
lastName, lastName,
isFormIncomplete, isFormIncomplete,
autoTrigger,
onAutoTriggered,
onPdfReady, onPdfReady,
}: TuftsSCOEligibilityButtonProps) { }: TuftsSCOEligibilityButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const sessionIdRef = useRef<string | null>(null); const sessionIdRef = useRef<string | null>(null);
const autoTriggeredRef = useRef(false);
const [otpModalOpen, setOtpModalOpen] = useState(false); const [otpModalOpen, setOtpModalOpen] = useState(false);
const [isStarting, setIsStarting] = 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 ( return (
<> <>
<Button <Button

View File

@@ -87,6 +87,8 @@ interface UnitedSCOEligibilityButtonProps {
firstName?: string; firstName?: string;
lastName?: string; lastName?: string;
isFormIncomplete: boolean; isFormIncomplete: boolean;
autoTrigger?: boolean;
onAutoTriggered?: () => void;
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
} }
@@ -96,12 +98,15 @@ export function UnitedSCOEligibilityButton({
firstName, firstName,
lastName, lastName,
isFormIncomplete, isFormIncomplete,
autoTrigger,
onAutoTriggered,
onPdfReady, onPdfReady,
}: UnitedSCOEligibilityButtonProps) { }: UnitedSCOEligibilityButtonProps) {
const { toast } = useToast(); const { toast } = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const sessionIdRef = useRef<string | null>(null); const sessionIdRef = useRef<string | null>(null);
const autoTriggeredRef = useRef(false);
const [otpModalOpen, setOtpModalOpen] = useState(false); const [otpModalOpen, setOtpModalOpen] = useState(false);
const [isStarting, setIsStarting] = 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 ( return (
<> <>
<Button <Button

View File

@@ -955,15 +955,42 @@ export default function AppointmentsPage() {
// ------------------- // -------------------
// appointment page — update these handlers // appointment page — update these handlers
const handleCheckEligibility = (appointmentId: number) => { const handleCheckEligibility = (appointmentId: number) => {
setLocation( const apt = appointments.find((a) => a.id === appointmentId);
`/insurance-status?appointmentId=${appointmentId}&action=eligibility` 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) => { let autoCheck = "other-providers";
setLocation(
`/insurance-status?appointmentId=${appointmentId}&action=claim` 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) => { const handleClaimsPreAuth = (appointmentId: number) => {
@@ -1602,7 +1629,7 @@ export default function AppointmentsPage() {
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Shield className="h-4 w-4" /> <Shield className="h-4 w-4" />
Eligibility Status Check Eligibility
</span> </span>
</Item> </Item>
@@ -1650,16 +1677,6 @@ export default function AppointmentsPage() {
</span> </span>
</Item> </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 */} {/* Chat */}
<Item onClick={({ props }) => handleChat(props.appointmentId)}> <Item onClick={({ props }) => handleChat(props.appointmentId)}>
<span className="flex items-center gap-2 text-blue-600"> <span className="flex items-center gap-2 text-blue-600">

View File

@@ -164,7 +164,9 @@ export default function InsuranceStatusPage() {
const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null); const [cmspAccumulatorPdfId, setCmspAccumulatorPdfId] = useState<number | null>(null);
const [cmspAccumulatorFilename, setCmspAccumulatorFilename] = useState<string | 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 // Prefill from chatbot
useEffect(() => { 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(() => { useEffect(() => {
if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return; if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return;
const check = pendingAutoCheck.current; const check = pendingAutoCheck.current;
pendingAutoCheck.current = null; pendingAutoCheck.current = null;
if (check === "mh") { if (check === "mh" || check === "mh-history") {
handleMHEligibilityHistoryButton(); handleMHEligibilityHistoryButton();
} else { } else if (check === "cmsp") {
handleCMSPButton(); handleCMSPButton();
} else {
// Button-component-handled providers: pass target to their autoTrigger prop
setTriggerTarget(check);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [memberId, dateOfBirth]); }, [memberId, dateOfBirth]);
@@ -635,66 +640,7 @@ export default function InsuranceStatusPage() {
} }
}; };
// 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");
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:
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const appointmentId = params.get("appointmentId"); const appointmentId = params.get("appointmentId");
@@ -703,6 +649,16 @@ export default function InsuranceStatusPage() {
const id = Number(appointmentId); const id = Number(appointmentId);
if (Number.isNaN(id) || id <= 0) return; 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; let cancelled = false;
(async () => { (async () => {
@@ -714,11 +670,8 @@ export default function InsuranceStatusPage() {
const patient = data?.patient ?? data; const patient = data?.patient ?? data;
if (!cancelled && patient) { if (!cancelled && patient) {
// ✅ ONLY prefill patient
setSelectedPatient(patient as Patient); setSelectedPatient(patient as Patient);
clearUrlParams(["appointmentId", "action", "autoCheck", "scrollTo"]);
// ✅ clean URL (no auto selenium)
clearUrlParams(["appointmentId", "action"]);
} }
} catch (err) { } catch (err) {
console.error("Failed to fetch patient from appointment", err); console.error("Failed to fetch patient from appointment", err);
@@ -730,6 +683,23 @@ export default function InsuranceStatusPage() {
}; };
}, [location]); }, [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 ( return (
<div> <div>
<SeleniumTaskBanner <SeleniumTaskBanner
@@ -852,6 +822,7 @@ export default function InsuranceStatusPage() {
<div className="flex flex-col-2 gap-4 mt-4"> <div className="flex flex-col-2 gap-4 mt-4">
<Button <Button
id="eligibility-mh-history"
className="w-full" className="w-full"
disabled={isCheckingEligibilityHistory} disabled={isCheckingEligibilityHistory}
onClick={() => handleMHEligibilityHistoryButton()} onClick={() => handleMHEligibilityHistoryButton()}
@@ -870,6 +841,7 @@ export default function InsuranceStatusPage() {
</Button> </Button>
<Button <Button
id="eligibility-cmsp"
className="w-full" className="w-full"
disabled={isCheckingCMSP} disabled={isCheckingCMSP}
onClick={() => handleCMSPButton()} onClick={() => handleCMSPButton()}
@@ -889,7 +861,7 @@ export default function InsuranceStatusPage() {
</div> </div>
{/* TEMP PROVIDER BUTTONS */} {/* 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"> <h3 className="text-sm font-medium text-muted-foreground">
Other provider checks Other provider checks
</h3> </h3>
@@ -902,6 +874,8 @@ export default function InsuranceStatusPage() {
firstName={firstName} firstName={firstName}
lastName={lastName} lastName={lastName}
isFormIncomplete={isFormIncomplete} isFormIncomplete={isFormIncomplete}
autoTrigger={triggerTarget === "ddma"}
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={(pdfId, fallbackFilename) => { onPdfReady={(pdfId, fallbackFilename) => {
setPreviewPdfId(pdfId); setPreviewPdfId(pdfId);
setPreviewFallbackFilename( setPreviewFallbackFilename(
@@ -917,6 +891,8 @@ export default function InsuranceStatusPage() {
firstName={firstName} firstName={firstName}
lastName={lastName} lastName={lastName}
isFormIncomplete={isFormIncomplete} isFormIncomplete={isFormIncomplete}
autoTrigger={triggerTarget === "delta-ins"}
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={(pdfId, fallbackFilename) => { onPdfReady={(pdfId, fallbackFilename) => {
setPreviewPdfId(pdfId); setPreviewPdfId(pdfId);
setPreviewFallbackFilename( setPreviewFallbackFilename(
@@ -944,10 +920,12 @@ export default function InsuranceStatusPage() {
firstName={firstName} firstName={firstName}
lastName={lastName} lastName={lastName}
isFormIncomplete={isFormIncomplete} isFormIncomplete={isFormIncomplete}
autoTrigger={triggerTarget === "tufts-sco"}
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={(pdfId, fallbackFilename) => { onPdfReady={(pdfId, fallbackFilename) => {
setPreviewPdfId(pdfId); setPreviewPdfId(pdfId);
setPreviewFallbackFilename( setPreviewFallbackFilename(
fallbackFilename ?? `eligibility_unitedsco_${memberId}.pdf`, fallbackFilename ?? `eligibility_tuftssco_${memberId}.pdf`,
); );
setPreviewOpen(true); setPreviewOpen(true);
}} }}
@@ -959,6 +937,8 @@ export default function InsuranceStatusPage() {
firstName={firstName} firstName={firstName}
lastName={lastName} lastName={lastName}
isFormIncomplete={isFormIncomplete} isFormIncomplete={isFormIncomplete}
autoTrigger={triggerTarget === "united-sco"}
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={(pdfId, fallbackFilename) => { onPdfReady={(pdfId, fallbackFilename) => {
setPreviewPdfId(pdfId); setPreviewPdfId(pdfId);
setPreviewFallbackFilename( setPreviewFallbackFilename(
@@ -974,6 +954,8 @@ export default function InsuranceStatusPage() {
firstName={firstName} firstName={firstName}
lastName={lastName} lastName={lastName}
isFormIncomplete={isFormIncomplete} isFormIncomplete={isFormIncomplete}
autoTrigger={triggerTarget === "cca"}
onAutoTriggered={() => setTriggerTarget(null)}
onPdfReady={(pdfId, fallbackFilename) => { onPdfReady={(pdfId, fallbackFilename) => {
setPreviewPdfId(pdfId); setPreviewPdfId(pdfId);
setPreviewFallbackFilename( setPreviewFallbackFilename(