import { useEffect, useRef, useState } from "react"; import { io as ioClient, Socket } from "socket.io-client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { CheckCircle, LoaderCircleIcon, X } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useAppDispatch } from "@/redux/hooks"; import { setTaskStatus } from "@/redux/slices/seleniumEligibilityCheckTaskSlice"; import { formatLocalDate } from "@/utils/dateUtils"; import { QK_PATIENTS_BASE } from "@/components/patients/patient-table"; const SOCKET_URL = import.meta.env.VITE_API_BASE_URL_BACKEND || (typeof window !== "undefined" ? window.location.origin : ""); // ---------- OTP Modal component ---------- interface DdmaOtpModalProps { open: boolean; onClose: () => void; onSubmit: (otp: string) => Promise | void; isSubmitting: boolean; } function DdmaOtpModal({ open, onClose, onSubmit, isSubmitting, }: DdmaOtpModalProps) { const [otp, setOtp] = useState(""); useEffect(() => { if (!open) setOtp(""); }, [open]); if (!open) return null; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!otp.trim()) return; await onSubmit(otp.trim()); }; return (

Enter OTP

We need the one-time password (OTP) sent by the Delta Dental MA portal to complete this eligibility check.

setOtp(e.target.value)} autoFocus />
); } // ---------- Main DDMA Eligibility button component ---------- interface DdmaEligibilityButtonProps { memberId: string; dateOfBirth: Date | null; firstName?: string; lastName?: string; isFormIncomplete: boolean; /** Called when backend has finished and PDF is ready */ onPdfReady: (pdfId: number, fallbackFilename: string | null) => void; } export function DdmaEligibilityButton({ memberId, dateOfBirth, firstName, lastName, isFormIncomplete, onPdfReady, }: DdmaEligibilityButtonProps) { const { toast } = useToast(); const dispatch = useAppDispatch(); const socketRef = useRef(null); const connectingRef = useRef | null>(null); const [sessionId, setSessionId] = useState(null); const [otpModalOpen, setOtpModalOpen] = useState(false); const [isStarting, setIsStarting] = useState(false); const [isSubmittingOtp, setIsSubmittingOtp] = useState(false); // Clean up socket on unmount useEffect(() => { return () => { if (socketRef.current) { socketRef.current.removeAllListeners(); socketRef.current.disconnect(); socketRef.current = null; } connectingRef.current = null; }; }, []); const closeSocket = () => { try { socketRef.current?.removeAllListeners(); socketRef.current?.disconnect(); } catch (e) { // ignore } finally { socketRef.current = null; } }; // Lazy socket setup: called only when we actually need it (first click) const ensureSocketConnected = async () => { // If already connected, nothing to do if (socketRef.current && socketRef.current.connected) { return; } // If a connection is in progress, reuse that promise if (connectingRef.current) { return connectingRef.current; } const promise = new Promise((resolve, reject) => { const socket = ioClient(SOCKET_URL, { withCredentials: true, }); socketRef.current = socket; socket.on("connect", () => { console.log("DDMA socket connected:", socket.id); resolve(); }); // connection error when first connecting (or later) socket.on("connect_error", (err: any) => { dispatch( setTaskStatus({ status: "error", message: "Connection failed", }) ); toast({ title: "Realtime connection failed", description: "Could not connect to realtime server. Retrying automatically...", variant: "destructive", }); // do not reject here because socket.io will attempt reconnection }); // socket.io will emit 'reconnect_attempt' for retries socket.on("reconnect_attempt", (attempt: number) => { dispatch( setTaskStatus({ status: "pending", message: `Realtime reconnect attempt #${attempt}`, }) ); }); // when reconnection failed after configured attempts socket.on("reconnect_failed", () => { dispatch( setTaskStatus({ status: "error", message: "Reconnect failed", }) ); toast({ title: "Realtime reconnect failed", description: "Connection to realtime server could not be re-established. Please try again later.", variant: "destructive", }); // terminal failure — cleanup and reject so caller can stop start flow closeSocket(); reject(new Error("Realtime reconnect failed")); }); socket.on("disconnect", (reason: any) => { dispatch( setTaskStatus({ status: "error", message: "Connection disconnected", }) ); toast({ title: "Connection Disconnected", description: "Connection to the server was lost. If a DDMA job was running it may have failed.", variant: "destructive", }); // clear sessionId/OTP modal setSessionId(null); setOtpModalOpen(false); }); // OTP required socket.on("selenium:otp_required", (payload: any) => { if (!payload?.session_id) return; setSessionId(payload.session_id); setOtpModalOpen(true); dispatch( setTaskStatus({ status: "pending", message: "OTP required for DDMA eligibility. Please enter the OTP.", }) ); }); // OTP submitted (optional UX) socket.on("selenium:otp_submitted", (payload: any) => { if (!payload?.session_id) return; dispatch( setTaskStatus({ status: "pending", message: "OTP submitted. Finishing DDMA eligibility check...", }) ); }); // Session update socket.on("selenium:session_update", (payload: any) => { const { session_id, status, final } = payload || {}; if (!session_id) return; if (status === "completed") { dispatch( setTaskStatus({ status: "success", message: "DDMA eligibility updated and PDF attached to patient documents.", }) ); toast({ title: "DDMA eligibility complete", description: "Patient status was updated and the eligibility PDF was saved.", variant: "default", }); const pdfId = final?.pdfFileId; if (pdfId) { const filename = final?.pdfFilename ?? `eligibility_ddma_${memberId}.pdf`; onPdfReady(Number(pdfId), filename); } setSessionId(null); setOtpModalOpen(false); } else if (status === "error") { const msg = payload?.message || final?.error || "DDMA eligibility session failed."; dispatch( setTaskStatus({ status: "error", message: msg, }) ); toast({ title: "DDMA selenium error", description: msg, variant: "destructive", }); // Ensure socket is torn down for this session (stop receiving stale events) try { closeSocket(); } catch (e) {} setSessionId(null); setOtpModalOpen(false); } queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE }); }); // explicit session error event (helpful) socket.on("selenium:session_error", (payload: any) => { const msg = payload?.message || "Selenium session error"; dispatch( setTaskStatus({ status: "error", message: msg, }) ); toast({ title: "Selenium session error", description: msg, variant: "destructive", }); // tear down socket to avoid stale updates try { closeSocket(); } catch (e) {} setSessionId(null); setOtpModalOpen(false); }); // If socket.io initial connection fails permanently (very rare: client-level) // set a longer timeout to reject the first attempt to connect. const initialConnectTimeout = setTimeout(() => { if (!socket.connected) { // if still not connected after 8s, treat as failure and reject so caller can handle it closeSocket(); reject(new Error("Realtime initial connection timeout")); } }, 8000); // When the connect resolves we should clear this timer socket.once("connect", () => { clearTimeout(initialConnectTimeout); }); }); // store promise to prevent multiple concurrent connections connectingRef.current = promise; try { await promise; } finally { connectingRef.current = null; } }; const startDdmaEligibility = async () => { if (!memberId || !dateOfBirth) { toast({ title: "Missing fields", description: "Member ID and Date of Birth are required.", variant: "destructive", }); return; } const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : ""; const payload = { memberId, dateOfBirth: formattedDob, firstName, lastName, insuranceSiteKey: "DDMA", // make sure this matches backend credential key }; try { setIsStarting(true); // 1) Ensure socket is connected (lazy) dispatch( setTaskStatus({ status: "pending", message: "Opening realtime channel for DDMA eligibility...", }) ); await ensureSocketConnected(); const socket = socketRef.current; if (!socket || !socket.connected) { throw new Error("Socket connection failed"); } const socketId = socket.id; // 2) Start the selenium job via backend dispatch( setTaskStatus({ status: "pending", message: "Starting DDMA eligibility check via selenium...", }) ); const response = await apiRequest( "POST", "/api/insurance-status-ddma/ddma-eligibility", { data: JSON.stringify(payload), socketId, } ); // If apiRequest threw, we would have caught above; but just in case it returns. let result: any = null; let bodyText = ""; try { // Try to parse JSON (works when backend returns JSON body) result = await response.json(); } catch (jsonErr) { // If JSON parsing fails, try to read raw text so we can show something meaningful try { bodyText = await response.text(); } catch (textErr) { bodyText = ""; } } // Determine error message robustly const backendError = // prefer explicit error field result?.error || // sometimes APIs return 'message' or 'detail' result?.message || result?.detail || // if JSON parse failed but bodyText contains something useful, use it (bodyText && bodyText.trim() ? bodyText.trim() : null); if (!response.ok) { // Log server response for debugging console.warn("DDMA start failed response:", { status: response.status, ok: response.ok, result, bodyText, }); // Throw with the backend message if available, otherwise throw generic throw new Error( backendError || `DDMA selenium start failed (status ${response.status})` ); } // Normal success path: optional: if backend returns non-error shape still check for result.error if (result?.error) { throw new Error(result.error); } if (result.status === "started" && result.session_id) { setSessionId(result.session_id as string); dispatch( setTaskStatus({ status: "pending", message: "DDMA eligibility job started. Waiting for OTP or final result...", }) ); } else { // fallback if backend returns immediate result dispatch( setTaskStatus({ status: "success", message: "DDMA eligibility completed.", }) ); } } catch (err: any) { console.error("startDdmaEligibility error:", err); dispatch( setTaskStatus({ status: "error", message: err?.message || "Failed to start DDMA eligibility", }) ); toast({ title: "DDMA selenium error", description: err?.message || "Failed to start DDMA eligibility", variant: "destructive", }); } finally { setIsStarting(false); } }; const handleSubmitOtp = async (otp: string) => { if (!sessionId || !socketRef.current || !socketRef.current.connected) { toast({ title: "Session not ready", description: "Could not submit OTP because the DDMA session or socket is not ready.", variant: "destructive", }); return; } try { setIsSubmittingOtp(true); const resp = await apiRequest( "POST", "/api/insurance-status-ddma/selenium/submit-otp", { session_id: sessionId, otp, socketId: socketRef.current.id, } ); const data = await resp.json(); if (!resp.ok || data.error) { throw new Error(data.error || "Failed to submit OTP"); } // from here we rely on websocket events (otp_submitted + session_update) setOtpModalOpen(false); } catch (err: any) { console.error("handleSubmitOtp error:", err); toast({ title: "Failed to submit OTP", description: err?.message || "Error forwarding OTP to selenium agent", variant: "destructive", }); } finally { setIsSubmittingOtp(false); } }; return ( <> setOtpModalOpen(false)} onSubmit={handleSubmitOtp} isSubmitting={isSubmittingOtp} /> ); }