closeClaim() was clearing chatbotAutoSubmitSiteKey synchronously right after the selenium job kicked off, before the job's async completion handler could check it. This was masked for all but the last claim in the batch because closeClaim() also navigated away (unmounting the page) whenever more claims were queued. On the last claim the page stayed mounted, so the completion handler read a stale cleared value and let the PDF preview popup through. Track the site key in a ref that survives closeClaim() and is cleared only after the job's own completion handler consumes it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1509 lines
63 KiB
TypeScript
Executable File
1509 lines
63 KiB
TypeScript
Executable File
import { useState, useEffect, useMemo, useRef } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardContent,
|
|
CardDescription,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
import { Calendar } from "@/components/ui/calendar";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
import { formatLocalDate } from "@/utils/dateUtils";
|
|
import { ClaimForm } from "@/components/claims/claim-form";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useAuth } from "@/hooks/use-auth";
|
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
import { useLocation, useSearch } from "wouter";
|
|
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
|
|
import {
|
|
setTaskStatus,
|
|
clearTaskStatus,
|
|
} from "@/redux/slices/seleniumTaskSlice";
|
|
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
|
import { useJobStatus } from "@/hooks/use-job-status";
|
|
import ClaimsRecentTable, {
|
|
QK_CLAIMS_BASE,
|
|
} from "@/components/claims/claims-recent-table";
|
|
import { QK_APPOINTMENTS_BASE } from "@/pages/appointments-page";
|
|
import ClaimsOfPatientModal from "@/components/claims/claims-of-patient-table";
|
|
import {
|
|
Claim,
|
|
UpdatePatient,
|
|
} from "@repo/db/types";
|
|
import ClaimDocumentsUploadMultiple from "@/components/claims/claim-document-upload-modal";
|
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
|
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
|
import { socket } from "@/lib/socket";
|
|
import { useLicense } from "@/hooks/use-license";
|
|
|
|
export default function ClaimsPage() {
|
|
const [isClaimFormOpen, setIsClaimFormOpen] = useState(false);
|
|
const [selectedPatientId, setSelectedPatientId] = useState<number | null>(null);
|
|
const [chatbotAutoSubmitSiteKey, setChatbotAutoSubmitSiteKey] = useState<string | undefined>(undefined);
|
|
// Mirrors chatbotAutoSubmitSiteKey but survives closeClaim(), which resets the state
|
|
// synchronously right after the selenium job is kicked off — well before the job's async
|
|
// completion handlers (which gate the PDF preview popup) actually run.
|
|
const chatbotAutoSubmitSiteKeyRef = useRef<string | undefined>(undefined);
|
|
const [chatbotInitialTab, setChatbotInitialTab] = useState<string | undefined>(undefined);
|
|
const [chatbotAutoSubmitPreauthSiteKey, setChatbotAutoSubmitPreauthSiteKey] = useState<string | undefined>(undefined);
|
|
// for redirect from appointment page directly, then passing to claimform
|
|
const [selectedAppointmentId, setSelectedAppointmentId] = useState<
|
|
number | null
|
|
>(null);
|
|
// PDF preview modal state
|
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
const [previewPdfId, setPreviewPdfId] = useState<number | string | null>(null);
|
|
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<string | null>(null);
|
|
|
|
// Preauth followup state
|
|
const [preauthFollowupFrom, setPreauthFollowupFrom] = useState<Date | undefined>(undefined);
|
|
const [preauthFollowupTo, setPreauthFollowupTo] = useState<Date | undefined>(undefined);
|
|
const [preauthFollowupFromOpen, setPreauthFollowupFromOpen] = useState(false);
|
|
const [preauthFollowupToOpen, setPreauthFollowupToOpen] = useState(false);
|
|
const dispatch = useAppDispatch();
|
|
const { status, message, show } = useAppSelector(
|
|
(state) => state.seleniumTasks.claimSubmit
|
|
);
|
|
const { isLicensed } = useLicense();
|
|
|
|
// Track pending selenium jobs so we can react to completion via socket
|
|
const [pendingClaimJobId, setPendingClaimJobId] = useState<string | null>(null);
|
|
// DDMA claim OTP modal
|
|
const [ddmaClaimOtpOpen, setDdmaClaimOtpOpen] = useState(false);
|
|
const [ddmaClaimOtpSubmitting, setDdmaClaimOtpSubmitting] = useState(false);
|
|
const ddmaClaimSessionIdRef = useRef<string | null>(null);
|
|
const [unitedDHClaimOtpOpen, setUnitedDHClaimOtpOpen] = useState(false);
|
|
const [unitedDHClaimOtpSubmitting, setUnitedDHClaimOtpSubmitting] = useState(false);
|
|
const unitedDHClaimSessionIdRef = useRef<string | null>(null);
|
|
const [unitedDHPreAuthOtpOpen, setUnitedDHPreAuthOtpOpen] = useState(false);
|
|
const [unitedDHPreAuthOtpSubmitting, setUnitedDHPreAuthOtpSubmitting] = useState(false);
|
|
const unitedDHPreAuthSessionIdRef = useRef<string | null>(null);
|
|
const [tuftsSCOClaimOtpOpen, setTuftsSCOClaimOtpOpen] = useState(false);
|
|
const [tuftsSCOClaimOtpSubmitting, setTuftsSCOClaimOtpSubmitting] = useState(false);
|
|
const tuftsSCOClaimSessionIdRef = useRef<string | null>(null);
|
|
const [tuftsSCOPreAuthOtpOpen, setTuftsSCOPreAuthOtpOpen] = useState(false);
|
|
const [tuftsSCOPreAuthOtpSubmitting, setTuftsSCOPreAuthOtpSubmitting] = useState(false);
|
|
const tuftsSCOPreAuthSessionIdRef = useRef<string | null>(null);
|
|
const pendingClaimMeta = useRef<{
|
|
patientId: number | null;
|
|
groupKey: "INSURANCE_CLAIM" | "INSURANCE_CLAIM_PREAUTH";
|
|
}>({ patientId: null, groupKey: "INSURANCE_CLAIM" });
|
|
const { status: jobStatus, result: jobResult, socketId } = useJobStatus(pendingClaimJobId);
|
|
const { toast } = useToast();
|
|
const { user } = useAuth();
|
|
|
|
// Update patient mutation
|
|
const updatePatientMutation = useMutation({
|
|
mutationFn: async ({
|
|
id,
|
|
patient,
|
|
}: {
|
|
id: number;
|
|
patient: UpdatePatient;
|
|
}) => {
|
|
const res = await apiRequest("PUT", `/api/patients/${id}`, patient);
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
toast({
|
|
title: "Success",
|
|
description: "Patient updated successfully!",
|
|
variant: "default",
|
|
});
|
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
|
},
|
|
onError: (error) => {
|
|
toast({
|
|
title: "Error",
|
|
description: `Failed to update patient: ${error.message}`,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// create claim mutation
|
|
const createClaimMutation = useMutation({
|
|
mutationFn: async (claimData: any) => {
|
|
const res = await apiRequest("POST", "/api/claims/", claimData);
|
|
return res.json();
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
|
|
|
toast({
|
|
title: "Claim created successfully",
|
|
variant: "default",
|
|
});
|
|
},
|
|
onError: (error: any) => {
|
|
toast({
|
|
title: "Error submitting claim",
|
|
description: error.message,
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// React to selenium job completing via socket
|
|
useEffect(() => {
|
|
if (!pendingClaimJobId) return;
|
|
if (jobStatus === "completed" && jobResult) {
|
|
setPendingClaimJobId(null);
|
|
queryClient.invalidateQueries({ queryKey: ["claims-recent"] });
|
|
|
|
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
|
if (jobResult.pdfFileId) {
|
|
advanceAiClaimQueue();
|
|
const advancedToNext = advanceChatbotClaimQueue();
|
|
if (!advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
|
|
setPreviewPdfId(jobResult.pdfFileId);
|
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
|
setPreviewOpen(true);
|
|
}
|
|
chatbotAutoSubmitSiteKeyRef.current = undefined;
|
|
dispatch(setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "success",
|
|
message: jobResult.claimNumber
|
|
? `CCA claim submitted — Encounter ID: ${jobResult.claimNumber}`
|
|
: "CCA claim submitted successfully",
|
|
}));
|
|
} else {
|
|
// MH claim: needs PDF download step
|
|
handleMHSeleniumPdfDownload(
|
|
jobResult,
|
|
pendingClaimMeta.current.patientId,
|
|
pendingClaimMeta.current.groupKey
|
|
);
|
|
}
|
|
} else if (jobStatus === "failed") {
|
|
setPendingClaimJobId(null);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: "Selenium job failed" }));
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [jobStatus, jobResult, pendingClaimJobId]);
|
|
|
|
// small helper: remove given query params from the current URL (silent, no reload)
|
|
const clearUrlParams = (params: string[]) => {
|
|
try {
|
|
const url = new URL(window.location.href);
|
|
let changed = false;
|
|
for (const p of params) {
|
|
if (url.searchParams.has(p)) {
|
|
url.searchParams.delete(p);
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
// Use wouter's setLocation so its internal search state updates too
|
|
const newRelative = url.pathname + (url.search ? url.search : "") + url.hash;
|
|
setWouterLocation(newRelative, { replace: true });
|
|
}
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
// case1: - this params are set by pdf extraction/patient page or either by patient-add-form. then used in claim page here.
|
|
const [location, setWouterLocation] = useLocation();
|
|
const search = useSearch();
|
|
|
|
const { newPatient, mode } = useMemo(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return {
|
|
newPatient: params.get("newPatient"),
|
|
mode: params.get("mode"), // direct | manual | null};
|
|
};
|
|
}, [location, search]);
|
|
|
|
const handleNewClaim = (patientId: number, appointmentId?: number) => {
|
|
setSelectedPatientId(patientId);
|
|
|
|
// case: when redirect happerns from appointment page.
|
|
if (appointmentId) {
|
|
setSelectedAppointmentId(appointmentId);
|
|
}
|
|
setIsClaimFormOpen(true);
|
|
};
|
|
|
|
// if ?newPatient=<id> is present, open claim form directly
|
|
useEffect(() => {
|
|
if (!newPatient) return;
|
|
|
|
const id = Number(newPatient);
|
|
if (!Number.isFinite(id) || id <= 0) return;
|
|
|
|
// Check if chatbot requested auto-submit with a specific insurance
|
|
try {
|
|
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed?.autoSubmit && parsed?.siteKey) {
|
|
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
|
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// Check if chatbot requested preauth tab
|
|
try {
|
|
const preauthRaw = sessionStorage.getItem("chatbot_preauth_prefill");
|
|
if (preauthRaw) {
|
|
setChatbotInitialTab("preauth");
|
|
const parsed = JSON.parse(preauthRaw);
|
|
if (parsed?.siteKey) setChatbotAutoSubmitPreauthSiteKey(parsed.siteKey);
|
|
}
|
|
} catch {}
|
|
|
|
handleNewClaim(id);
|
|
clearUrlParams(["newPatient"]);
|
|
}, [newPatient]);
|
|
|
|
// case2 - redirect from appointment page:
|
|
// If ?appointmentId= is present (and no ?newPatient param), fetch appointment->patient and open claim form
|
|
useEffect(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const appointmentIdParam = params.get("appointmentId");
|
|
|
|
// If newPatient flow is present, prefer it and do nothing here
|
|
if (newPatient) return;
|
|
if (!appointmentIdParam) return;
|
|
|
|
const appointmentId = Number(appointmentIdParam);
|
|
if (!Number.isFinite(appointmentId) || appointmentId <= 0) return;
|
|
|
|
let cancelled = false;
|
|
|
|
(async () => {
|
|
try {
|
|
const res = await apiRequest(
|
|
"GET",
|
|
`/api/appointments/${appointmentId}`
|
|
);
|
|
if (!res.ok) {
|
|
let body: any = null;
|
|
try {
|
|
body = await res.json();
|
|
} catch {}
|
|
if (!cancelled) {
|
|
toast({
|
|
title: "Failed to load appointment",
|
|
description:
|
|
body?.message ??
|
|
body?.error ??
|
|
`Could not fetch appointment ${appointmentId}.`,
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const appointment = await res.json();
|
|
const patientId = appointment?.patientId;
|
|
|
|
if (!cancelled && patientId) {
|
|
// Check if chatbot requested auto-submit with a specific insurance
|
|
try {
|
|
const raw = sessionStorage.getItem("chatbot_claim_prefill");
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed?.autoSubmit && parsed?.siteKey) {
|
|
setChatbotAutoSubmitSiteKey(parsed.siteKey);
|
|
chatbotAutoSubmitSiteKeyRef.current = parsed.siteKey;
|
|
}
|
|
}
|
|
} catch {}
|
|
handleNewClaim(patientId, appointmentId);
|
|
clearUrlParams(["appointmentId"]);
|
|
}
|
|
} catch (err: any) {
|
|
if (!cancelled) {
|
|
console.error("Error fetching patient for appointment:", err);
|
|
toast({
|
|
title: "Error",
|
|
description: err?.message ?? "Failed to fetch patient.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [location, search, newPatient]);
|
|
|
|
// 2. Update Patient ( for insuranceId and Insurance Provider)
|
|
const handleUpdatePatient = (patient: UpdatePatient & { id?: number }) => {
|
|
if (patient) {
|
|
const { id, ...sanitizedPatient } = patient;
|
|
updatePatientMutation.mutate({
|
|
id: Number(patient.id),
|
|
patient: sanitizedPatient,
|
|
});
|
|
} else {
|
|
toast({
|
|
title: "Error",
|
|
description: "Cannot update patient: No patient or user found",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// Advance the chatbot_claim_queue / chatbot_check_claim_queue after selenium PDF is saved.
|
|
// NOT called from closeClaim — called only after the selenium job completes and PDF is downloaded.
|
|
// Returns true if it navigated on to another queued patient (caller should skip the PDF
|
|
// preview popup in that case — the form is about to be replaced by the next patient's claim).
|
|
const advanceChatbotClaimQueue = (): boolean => {
|
|
try {
|
|
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
const remaining = parsed?.remaining as any[] | undefined;
|
|
const matchedCodes = parsed?.matchedCodes ?? [];
|
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
|
if (remaining && remaining.length > 0) {
|
|
const [next, ...rest] = remaining;
|
|
if (rest.length > 0) {
|
|
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
|
} else {
|
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
|
}
|
|
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
|
codes: matchedCodes,
|
|
siteKey: next.siteKey,
|
|
patientId: next.patient?.id ?? null,
|
|
memberId: next.memberId,
|
|
dob: next.dob,
|
|
serviceDate: null,
|
|
renderingProvider,
|
|
}));
|
|
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
|
memberId: next.memberId,
|
|
dob: next.dob,
|
|
autoCheck: next.autoCheck,
|
|
}));
|
|
setTimeout(() => {
|
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
|
setWouterLocation("/insurance-status");
|
|
}, 500);
|
|
return true;
|
|
}
|
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
|
}
|
|
} catch {}
|
|
|
|
try {
|
|
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
const remaining = parsed?.remaining as any[] | undefined;
|
|
const sharedMatchedCodes = parsed?.matchedCodes ?? [];
|
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
|
if (remaining && remaining.length > 0) {
|
|
const [next, ...rest] = remaining;
|
|
if (rest.length > 0) {
|
|
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes: sharedMatchedCodes, renderingProvider }));
|
|
} else {
|
|
sessionStorage.removeItem("chatbot_claim_queue");
|
|
}
|
|
const codes = next.matchedCodes?.length > 0 ? next.matchedCodes : sharedMatchedCodes;
|
|
if (next.patient?.id && codes.length > 0) {
|
|
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
|
codes,
|
|
siteKey: next.siteKey,
|
|
serviceDate: next.serviceDate,
|
|
autoSubmit: true,
|
|
renderingProvider,
|
|
dob: next.patient.dateOfBirth ?? null,
|
|
}));
|
|
}
|
|
setTimeout(() => {
|
|
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
|
}, 500);
|
|
return true;
|
|
}
|
|
sessionStorage.removeItem("chatbot_claim_queue");
|
|
}
|
|
} catch {}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Advance the ai_claim_queue by removing the first item (current patient).
|
|
// Called only on successful submission so that cancel leaves the queue intact.
|
|
const advanceAiClaimQueue = () => {
|
|
try {
|
|
const raw = sessionStorage.getItem("ai_claim_queue");
|
|
if (!raw) return;
|
|
const parsed = JSON.parse(raw);
|
|
if (!parsed?.pendingResume || !Array.isArray(parsed.appointments)) return;
|
|
const [, ...rest] = parsed.appointments;
|
|
if (rest.length > 0) {
|
|
sessionStorage.setItem("ai_claim_queue", JSON.stringify({ ...parsed, appointments: rest }));
|
|
} else {
|
|
sessionStorage.removeItem("ai_claim_queue");
|
|
}
|
|
} catch {}
|
|
};
|
|
|
|
// 3. create or update claim (update when claimId is present)
|
|
const handleClaimSubmit = async (claimData: any): Promise<Claim> => {
|
|
const { isDraft, claimId, uploadedFiles: _uf, ...cleanData } = claimData;
|
|
|
|
if (claimId) {
|
|
// Update existing saved claim (PUT never creates a Payment)
|
|
const res = await apiRequest("PUT", `/api/claims/${claimId}`, cleanData);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.message || "Failed to update claim");
|
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
|
if (!isDraft) toast({ title: "Claim updated successfully", variant: "default" });
|
|
return data;
|
|
}
|
|
|
|
// New claim: draft saves skip Payment creation
|
|
const url = isDraft ? "/api/claims/?draft=true" : "/api/claims/";
|
|
const res = await apiRequest("POST", url, cleanData);
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
|
// NOTE: advanceChatbotClaimQueue() is NOT called here. This function only creates
|
|
// the claim DB record; for selenium-based insurances (MH/CCA/DDMA/Tufts/United) the
|
|
// actual submission + PDF happens afterward. Advancing the queue here would move to
|
|
// the next patient before the current patient's selenium job (and PDF) finishes.
|
|
// The queue is advanced later, once the selenium job completes (see
|
|
// handleMHSeleniumPdfDownload and the CCA branch in the job-status useEffect).
|
|
if (!isDraft) advanceAiClaimQueue();
|
|
return data;
|
|
};
|
|
|
|
// 4. handle selenium sybmiting Mass Health claim
|
|
const handleMHClaimSubmitSelenium = async (data: any) => {
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(data));
|
|
if (socketId) formData.append("socketId", socketId);
|
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
|
|
|
uploadedFiles.forEach((file: File) => {
|
|
if (file.type === "application/pdf") {
|
|
formData.append("pdfs", file);
|
|
} else if (file.type.startsWith("image/")) {
|
|
formData.append("images", file);
|
|
}
|
|
});
|
|
|
|
try {
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "pending",
|
|
message: "Submitting claim to Selenium...",
|
|
})
|
|
);
|
|
const response = await apiRequest(
|
|
"POST",
|
|
"/api/claims/selenium-claim",
|
|
formData
|
|
);
|
|
const result1 = await response.json();
|
|
if (result1.error) throw new Error(result1.error);
|
|
|
|
// Job is queued — wait for socket completion before downloading PDF
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
|
|
setPendingClaimJobId(result1.jobId);
|
|
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "pending",
|
|
message: "Submitted to Selenium. Awaiting result...",
|
|
})
|
|
);
|
|
|
|
toast({
|
|
title: "Selenium service notified",
|
|
description: "Claim submitted to Selenium. Waiting for confirmation...",
|
|
variant: "default",
|
|
});
|
|
} catch (error: any) {
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "error",
|
|
message: error.message || "Selenium submission failed",
|
|
})
|
|
);
|
|
toast({
|
|
title: "Selenium service error",
|
|
description: error.message || "An error occurred.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// CCA claim selenium handler
|
|
const handleCCAClaimSubmitSelenium = async (data: any) => {
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(data));
|
|
if (socketId) formData.append("socketId", socketId);
|
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
|
uploadedFiles.forEach((file: File) => {
|
|
if (file.type === "application/pdf") {
|
|
formData.append("pdfs", file);
|
|
} else if (file.type.startsWith("image/")) {
|
|
formData.append("images", file);
|
|
}
|
|
});
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting CCA claim..." }));
|
|
const response = await apiRequest("POST", "/api/claims/cca-claim", formData);
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
// Track job so the completion useEffect fires when Selenium finishes
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
|
|
setPendingClaimJobId(result.jobId);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "CCA claim queued. Awaiting Selenium..." }));
|
|
toast({ title: "CCA Claim queued", description: "Selenium is processing the claim.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "CCA claim failed" }));
|
|
toast({ title: "CCA Claim error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
// DDMA claim OTP submit handler
|
|
const handleDdmaClaimOtpSubmit = async (otp: string) => {
|
|
const sessionId = ddmaClaimSessionIdRef.current;
|
|
if (!sessionId) return;
|
|
try {
|
|
setDdmaClaimOtpSubmitting(true);
|
|
const resp = await apiRequest("POST", "/api/claims/ddma-claim/selenium/submit-otp", {
|
|
session_id: sessionId,
|
|
otp,
|
|
socketId,
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
|
|
setDdmaClaimOtpOpen(false);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing DDMA claim..." }));
|
|
} catch (err: any) {
|
|
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
|
|
} finally {
|
|
setDdmaClaimOtpSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// DDMA claim selenium handler
|
|
const handleDDMAClaimSubmitSelenium = async (data: any) => {
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting Delta MA claim..." }));
|
|
const response = await apiRequest("POST", "/api/claims/ddma-claim", {
|
|
data,
|
|
socketId,
|
|
});
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
|
|
setPendingClaimJobId(result.jobId);
|
|
|
|
// Listen for session_id so we can forward OTP if needed
|
|
const jobId = result.jobId;
|
|
const onSessionStarted = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
ddmaClaimSessionIdRef.current = ev.session_id ?? null;
|
|
};
|
|
const onOtpRequired = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
if (ev.session_id) ddmaClaimSessionIdRef.current = ev.session_id;
|
|
setDdmaClaimOtpOpen(true);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for Delta MA. Please enter the code." }));
|
|
};
|
|
const onDone = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
socket.off("selenium:ddma_claim_started", onSessionStarted);
|
|
socket.off("selenium:otp_required", onOtpRequired);
|
|
socket.off("job:update", onDone);
|
|
setDdmaClaimOtpOpen(false);
|
|
ddmaClaimSessionIdRef.current = null;
|
|
};
|
|
socket.on("selenium:ddma_claim_started", onSessionStarted);
|
|
socket.on("selenium:otp_required", onOtpRequired);
|
|
socket.on("job:update", onDone);
|
|
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Delta MA claim queued. Awaiting Selenium..." }));
|
|
toast({ title: "Delta MA Claim queued", description: "Selenium is opening the claim form.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "Delta MA claim failed" }));
|
|
toast({ title: "Delta MA Claim error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const handleUnitedDHClaimOtpSubmit = async (otp: string) => {
|
|
const sessionId = unitedDHClaimSessionIdRef.current;
|
|
if (!sessionId) return;
|
|
try {
|
|
setUnitedDHClaimOtpSubmitting(true);
|
|
const resp = await apiRequest("POST", "/api/claims/uniteddh-claim/selenium/submit-otp", {
|
|
session_id: sessionId,
|
|
otp,
|
|
socketId,
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
|
|
setUnitedDHClaimOtpOpen(false);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing United/DentalHub claim..." }));
|
|
} catch (err: any) {
|
|
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
|
|
} finally {
|
|
setUnitedDHClaimOtpSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleUnitedDHPreAuthOtpSubmit = async (otp: string) => {
|
|
const sessionId = unitedDHPreAuthSessionIdRef.current;
|
|
if (!sessionId) return;
|
|
try {
|
|
setUnitedDHPreAuthOtpSubmitting(true);
|
|
const resp = await apiRequest("POST", "/api/claims/uniteddh-preauth/selenium/submit-otp", {
|
|
session_id: sessionId,
|
|
otp,
|
|
socketId,
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
|
|
setUnitedDHPreAuthOtpOpen(false);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing United/DentalHub pre-authorization..." }));
|
|
} catch (err: any) {
|
|
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
|
|
} finally {
|
|
setUnitedDHPreAuthOtpSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// United/DentalHub pre-auth selenium handler
|
|
const handleUnitedDHPreAuthSubmitSelenium = async (data: any) => {
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting United/DentalHub pre-authorization..." }));
|
|
const response = await apiRequest("POST", "/api/claims/uniteddh-preauth", {
|
|
data,
|
|
socketId,
|
|
});
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM_PREAUTH" };
|
|
setPendingClaimJobId(result.jobId);
|
|
|
|
const jobId = result.jobId;
|
|
const onSessionStarted = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
unitedDHPreAuthSessionIdRef.current = ev.session_id ?? null;
|
|
};
|
|
const onOtpRequired = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
if (ev.session_id) unitedDHPreAuthSessionIdRef.current = ev.session_id;
|
|
setUnitedDHPreAuthOtpOpen(true);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for United/DentalHub. Please enter the code." }));
|
|
};
|
|
const onDone = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
socket.off("selenium:uniteddh_preauth_started", onSessionStarted);
|
|
socket.off("selenium:otp_required", onOtpRequired);
|
|
socket.off("job:update", onDone);
|
|
setUnitedDHPreAuthOtpOpen(false);
|
|
unitedDHPreAuthSessionIdRef.current = null;
|
|
};
|
|
socket.on("selenium:uniteddh_preauth_started", onSessionStarted);
|
|
socket.on("selenium:otp_required", onOtpRequired);
|
|
socket.on("job:update", onDone);
|
|
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "United/DentalHub pre-auth queued. Awaiting Selenium..." }));
|
|
toast({ title: "United/DentalHub PreAuth queued", description: "Selenium is opening the pre-authorization form.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "United/DentalHub pre-auth failed" }));
|
|
toast({ title: "United/DentalHub PreAuth error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const handleTuftsSCOPreAuthOtpSubmit = async (otp: string) => {
|
|
const sessionId = tuftsSCOPreAuthSessionIdRef.current;
|
|
if (!sessionId) return;
|
|
try {
|
|
setTuftsSCOPreAuthOtpSubmitting(true);
|
|
const resp = await apiRequest("POST", "/api/claims/tuftssco-preauth/selenium/submit-otp", {
|
|
session_id: sessionId,
|
|
otp,
|
|
socketId,
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
|
|
setTuftsSCOPreAuthOtpOpen(false);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing Tufts SCO pre-authorization..." }));
|
|
} catch (err: any) {
|
|
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
|
|
} finally {
|
|
setTuftsSCOPreAuthOtpSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// Tufts SCO pre-auth selenium handler
|
|
const handleTuftsSCOPreAuthSubmitSelenium = async (data: any) => {
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting Tufts SCO pre-authorization..." }));
|
|
const response = await apiRequest("POST", "/api/claims/tuftssco-preauth", {
|
|
data,
|
|
socketId,
|
|
});
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM_PREAUTH" };
|
|
setPendingClaimJobId(result.jobId);
|
|
|
|
const jobId = result.jobId;
|
|
const onSessionStarted = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
tuftsSCOPreAuthSessionIdRef.current = ev.session_id ?? null;
|
|
};
|
|
const onOtpRequired = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
if (ev.session_id) tuftsSCOPreAuthSessionIdRef.current = ev.session_id;
|
|
setTuftsSCOPreAuthOtpOpen(true);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for Tufts SCO. Please enter the code." }));
|
|
};
|
|
const onDone = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
socket.off("selenium:tuftssco_preauth_started", onSessionStarted);
|
|
socket.off("selenium:otp_required", onOtpRequired);
|
|
socket.off("job:update", onDone);
|
|
setTuftsSCOPreAuthOtpOpen(false);
|
|
tuftsSCOPreAuthSessionIdRef.current = null;
|
|
};
|
|
socket.on("selenium:tuftssco_preauth_started", onSessionStarted);
|
|
socket.on("selenium:otp_required", onOtpRequired);
|
|
socket.on("job:update", onDone);
|
|
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Tufts SCO pre-auth queued. Awaiting Selenium..." }));
|
|
toast({ title: "Tufts SCO PreAuth queued", description: "Selenium is opening the pre-authorization form.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "Tufts SCO pre-auth failed" }));
|
|
toast({ title: "Tufts SCO PreAuth error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
// United/DentalHub claim selenium handler
|
|
const handleUnitedDHClaimSubmitSelenium = async (data: any) => {
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting United/DentalHub claim..." }));
|
|
const response = await apiRequest("POST", "/api/claims/uniteddh-claim", {
|
|
data,
|
|
socketId,
|
|
});
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
|
|
setPendingClaimJobId(result.jobId);
|
|
|
|
const jobId = result.jobId;
|
|
const onSessionStarted = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
unitedDHClaimSessionIdRef.current = ev.session_id ?? null;
|
|
};
|
|
const onOtpRequired = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
if (ev.session_id) unitedDHClaimSessionIdRef.current = ev.session_id;
|
|
setUnitedDHClaimOtpOpen(true);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for United/DentalHub. Please enter the code." }));
|
|
};
|
|
const onDone = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
socket.off("selenium:uniteddh_claim_started", onSessionStarted);
|
|
socket.off("selenium:otp_required", onOtpRequired);
|
|
socket.off("job:update", onDone);
|
|
setUnitedDHClaimOtpOpen(false);
|
|
unitedDHClaimSessionIdRef.current = null;
|
|
};
|
|
socket.on("selenium:uniteddh_claim_started", onSessionStarted);
|
|
socket.on("selenium:otp_required", onOtpRequired);
|
|
socket.on("job:update", onDone);
|
|
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "United/DentalHub claim queued. Awaiting Selenium..." }));
|
|
toast({ title: "United/DentalHub Claim queued", description: "Selenium is opening the claim form.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "United/DentalHub claim failed" }));
|
|
toast({ title: "United/DentalHub Claim error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const handleTuftsSCOClaimOtpSubmit = async (otp: string) => {
|
|
const sessionId = tuftsSCOClaimSessionIdRef.current;
|
|
if (!sessionId) return;
|
|
try {
|
|
setTuftsSCOClaimOtpSubmitting(true);
|
|
const resp = await apiRequest("POST", "/api/claims/tuftssco-claim/selenium/submit-otp", {
|
|
session_id: sessionId,
|
|
otp,
|
|
socketId,
|
|
});
|
|
const data = await resp.json();
|
|
if (!resp.ok || data.error) throw new Error(data.error || "Failed to submit OTP");
|
|
setTuftsSCOClaimOtpOpen(false);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP submitted. Continuing Tufts SCO claim..." }));
|
|
} catch (err: any) {
|
|
toast({ title: "Failed to submit OTP", description: err?.message || "Error submitting OTP", variant: "destructive" });
|
|
} finally {
|
|
setTuftsSCOClaimOtpSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// Tufts SCO claim selenium handler
|
|
const handleTuftsSCOClaimSubmitSelenium = async (data: any) => {
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting Tufts SCO claim..." }));
|
|
const response = await apiRequest("POST", "/api/claims/tuftssco-claim", {
|
|
data,
|
|
socketId,
|
|
});
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM" };
|
|
setPendingClaimJobId(result.jobId);
|
|
|
|
const jobId = result.jobId;
|
|
const onSessionStarted = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
tuftsSCOClaimSessionIdRef.current = ev.session_id ?? null;
|
|
};
|
|
const onOtpRequired = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
if (ev.session_id) tuftsSCOClaimSessionIdRef.current = ev.session_id;
|
|
setTuftsSCOClaimOtpOpen(true);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "OTP required for Tufts SCO. Please enter the code." }));
|
|
};
|
|
const onDone = (ev: any) => {
|
|
if (String(ev?.jobId) !== String(jobId)) return;
|
|
socket.off("selenium:tuftssco_claim_started", onSessionStarted);
|
|
socket.off("selenium:otp_required", onOtpRequired);
|
|
socket.off("job:update", onDone);
|
|
setTuftsSCOClaimOtpOpen(false);
|
|
tuftsSCOClaimSessionIdRef.current = null;
|
|
};
|
|
socket.on("selenium:tuftssco_claim_started", onSessionStarted);
|
|
socket.on("selenium:otp_required", onOtpRequired);
|
|
socket.on("job:update", onDone);
|
|
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Tufts SCO claim queued. Awaiting Selenium..." }));
|
|
toast({ title: "Tufts SCO Claim queued", description: "Selenium is opening the claim form.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "Tufts SCO claim failed" }));
|
|
toast({ title: "Tufts SCO Claim error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
// CCA pre-auth selenium handler
|
|
const handleCCAPreAuthSubmitSelenium = async (data: any) => {
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(data));
|
|
if (socketId) formData.append("socketId", socketId);
|
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
|
uploadedFiles.forEach((file: File) => {
|
|
if (file.type === "application/pdf") {
|
|
formData.append("pdfs", file);
|
|
} else if (file.type.startsWith("image/")) {
|
|
formData.append("images", file);
|
|
}
|
|
});
|
|
try {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "Submitting CCA PreAuth..." }));
|
|
const response = await apiRequest("POST", "/api/claims/cca-preauth", formData);
|
|
const result = await response.json();
|
|
if (result.error) throw new Error(result.error);
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM_PREAUTH" };
|
|
setPendingClaimJobId(result.jobId);
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "pending", message: "CCA PreAuth queued. Awaiting Selenium..." }));
|
|
toast({ title: "CCA PreAuth queued", description: "Selenium is processing the pre-authorization.", variant: "default" });
|
|
} catch (error: any) {
|
|
dispatch(setTaskStatus({ key: "claimSubmit", status: "error", message: error.message || "CCA PreAuth failed" }));
|
|
toast({ title: "CCA PreAuth error", description: error.message || "An error occurred.", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
// 5. selenium pdf download handler
|
|
const handleMHSeleniumPdfDownload = async (
|
|
data: any,
|
|
selectedPatientId: number | null,
|
|
groupTitleKey: "INSURANCE_CLAIM" | "INSURANCE_CLAIM_PREAUTH"
|
|
) => {
|
|
try {
|
|
if (!selectedPatientId) {
|
|
throw new Error("Missing patientId");
|
|
}
|
|
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "pending",
|
|
message: "Downloading PDF from Selenium...",
|
|
})
|
|
);
|
|
|
|
const res = await apiRequest("POST", "/api/claims/selenium/fetchpdf", {
|
|
patientId: selectedPatientId,
|
|
pdf_url: data.pdf_url,
|
|
groupTitleKey,
|
|
});
|
|
const result = await res.json();
|
|
if (result.error) throw new Error(result.error);
|
|
|
|
// Invalidate queries to refresh the Documents area
|
|
queryClient.invalidateQueries({ queryKey: ["groupPdfs"] });
|
|
queryClient.invalidateQueries({ queryKey: ["pdf-groups"] });
|
|
queryClient.invalidateQueries({ queryKey: ["claims-recent"] });
|
|
// Invalidate appointments so the schedule page picks up the new claim color
|
|
queryClient.invalidateQueries({ queryKey: QK_APPOINTMENTS_BASE });
|
|
|
|
const isPreAuth = groupTitleKey === "INSURANCE_CLAIM_PREAUTH";
|
|
const preAuthNumber = data.preAuthNumber ?? data.result?.preAuthNumber ?? null;
|
|
const claimNumberForToast = data.claimNumber ?? null;
|
|
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "success",
|
|
message: isPreAuth
|
|
? `PreAuth submitted & PDF saved.${preAuthNumber ? ` PreAuth #: ${preAuthNumber}` : ""}`
|
|
: claimNumberForToast
|
|
? `Claim submitted — Claim #: ${claimNumberForToast}`
|
|
: "Claim submitted & PDF downloaded successfully.",
|
|
})
|
|
);
|
|
|
|
toast({
|
|
title: isPreAuth ? "PreAuth Submitted" : "Success",
|
|
description: isPreAuth
|
|
? preAuthNumber
|
|
? `PreAuth Number: ${preAuthNumber} — PDF saved to Documents.`
|
|
: "PreAuth submitted! PDF saved to Documents page."
|
|
: claimNumberForToast
|
|
? `Claim #: ${claimNumberForToast} — PDF saved to Documents.`
|
|
: "Claim submitted successfully! PDF saved to Documents page.",
|
|
duration: isPreAuth ? 10000 : 5000,
|
|
});
|
|
|
|
let advancedToNext = false;
|
|
if (!isPreAuth) {
|
|
advanceAiClaimQueue();
|
|
advancedToNext = advanceChatbotClaimQueue();
|
|
}
|
|
|
|
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
|
// (skip if we're about to navigate to the next queued patient's claim,
|
|
// or if this claim was submitted via the AI chat column)
|
|
if (result.pdfFileId && !advancedToNext && !chatbotAutoSubmitSiteKeyRef.current) {
|
|
setPreviewPdfId(result.pdfFileId);
|
|
setPreviewFallbackFilename(result.fileName ?? null);
|
|
setPreviewOpen(true);
|
|
}
|
|
chatbotAutoSubmitSiteKeyRef.current = undefined;
|
|
|
|
return result;
|
|
} catch (error: any) {
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "error",
|
|
message: error.message || "Failed to download PDF",
|
|
})
|
|
);
|
|
toast({
|
|
title: "Error",
|
|
description: error.message || "Failed to fetch PDF",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
|
|
// 6. close claim
|
|
const closeClaim = () => {
|
|
setSelectedPatientId(null);
|
|
setSelectedAppointmentId(null);
|
|
setChatbotAutoSubmitSiteKey(undefined);
|
|
setChatbotInitialTab(undefined);
|
|
setChatbotAutoSubmitPreauthSiteKey(undefined);
|
|
setIsClaimFormOpen(false);
|
|
|
|
clearUrlParams(["newPatient", "appointmentId"]);
|
|
|
|
// If an AI claim queue is pending resume, go straight back to appointments
|
|
try {
|
|
const raw = sessionStorage.getItem("ai_claim_queue");
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed?.pendingResume && Array.isArray(parsed.appointments) && parsed.appointments.length > 0) {
|
|
setWouterLocation("/appointments");
|
|
return;
|
|
}
|
|
}
|
|
} catch {}
|
|
|
|
// Chatbot batch queues (chatbot_claim_queue, chatbot_check_claim_queue) are NOT
|
|
// advanced here — they are advanced in advanceChatbotClaimQueue() which is called
|
|
// only after the selenium job completes and PDF is downloaded. This prevents the
|
|
// next patient's claim from starting before the current patient's PDF is saved.
|
|
};
|
|
|
|
// Pre Auth section
|
|
const handleMHClaimPreAuthSubmitSelenium = async (data: any) => {
|
|
const formData = new FormData();
|
|
formData.append("data", JSON.stringify(data));
|
|
if (socketId) formData.append("socketId", socketId);
|
|
const uploadedFiles: File[] = data.uploadedFiles ?? [];
|
|
|
|
uploadedFiles.forEach((file: File) => {
|
|
if (file.type === "application/pdf") {
|
|
formData.append("pdfs", file);
|
|
} else if (file.type.startsWith("image/")) {
|
|
formData.append("images", file);
|
|
}
|
|
});
|
|
|
|
try {
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "pending",
|
|
message: "Submitting claim pre auth to Selenium...",
|
|
})
|
|
);
|
|
const response = await apiRequest(
|
|
"POST",
|
|
"/api/claims/selenium-claim-pre-auth",
|
|
formData
|
|
);
|
|
const result1 = await response.json();
|
|
if (result1.error) throw new Error(result1.error);
|
|
|
|
// Job is queued — wait for socket completion before downloading PDF
|
|
pendingClaimMeta.current = { patientId: selectedPatientId, groupKey: "INSURANCE_CLAIM_PREAUTH" };
|
|
setPendingClaimJobId(result1.jobId);
|
|
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "pending",
|
|
message: "Submitted to Selenium. Awaiting result...",
|
|
})
|
|
);
|
|
|
|
toast({
|
|
title: "Selenium service notified",
|
|
description: "Pre-auth submitted to Selenium. Waiting for confirmation...",
|
|
variant: "default",
|
|
});
|
|
} catch (error: any) {
|
|
dispatch(
|
|
setTaskStatus({
|
|
key: "claimSubmit",
|
|
status: "error",
|
|
message: error.message || "Selenium submission failed",
|
|
})
|
|
);
|
|
toast({
|
|
title: "Selenium service error",
|
|
description: error.message || "An error occurred.",
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
return (
|
|
<div>
|
|
<SeleniumTaskBanner
|
|
status={status}
|
|
message={message}
|
|
show={show}
|
|
onClear={() => dispatch(clearTaskStatus("claimSubmit"))}
|
|
/>
|
|
|
|
<div className="container mx-auto space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">
|
|
Insurance Claims
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage and submit insurance claims for patients
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Claims by Patients also handles new claims */}
|
|
<ClaimsOfPatientModal onNewClaim={handleNewClaim} />
|
|
|
|
{/* Recent Claims Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Recently Submitted Claims</CardTitle>
|
|
<CardDescription>
|
|
View and manage all recent claims information
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ClaimsRecentTable
|
|
allowEdit={true}
|
|
allowView={true}
|
|
allowDelete={true}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Preauth Followup Section */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Preauth Followup</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<div className="flex items-center gap-2">
|
|
<Label className="flex items-center">From</Label>
|
|
<Popover
|
|
open={preauthFollowupFromOpen}
|
|
onOpenChange={setPreauthFollowupFromOpen}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className="w-[140px] justify-start text-left font-normal"
|
|
>
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{preauthFollowupFrom
|
|
? formatLocalDate(preauthFollowupFrom)
|
|
: "Pick Date"}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto">
|
|
<Calendar
|
|
mode="single"
|
|
selected={preauthFollowupFrom}
|
|
onSelect={(date) => {
|
|
setPreauthFollowupFrom(date);
|
|
setPreauthFollowupFromOpen(false);
|
|
}}
|
|
onClose={() => setPreauthFollowupFromOpen(false)}
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Label className="flex items-center">To</Label>
|
|
<Popover
|
|
open={preauthFollowupToOpen}
|
|
onOpenChange={setPreauthFollowupToOpen}
|
|
>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
className="w-[140px] justify-start text-left font-normal"
|
|
>
|
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
|
{preauthFollowupTo
|
|
? formatLocalDate(preauthFollowupTo)
|
|
: "Pick Date"}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-auto">
|
|
<Calendar
|
|
mode="single"
|
|
selected={preauthFollowupTo}
|
|
onSelect={(date) => {
|
|
setPreauthFollowupTo(date);
|
|
setPreauthFollowupToOpen(false);
|
|
}}
|
|
onClose={() => setPreauthFollowupToOpen(false)}
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap gap-2 items-center">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
// TODO: define CCA PA followup logic
|
|
}}
|
|
>
|
|
CCA PA
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
// TODO: define United PA followup logic
|
|
}}
|
|
>
|
|
United PA
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
// TODO: define Tufts PA followup logic
|
|
}}
|
|
>
|
|
Tufts PA
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => {
|
|
// TODO: define All recent PA followup logic
|
|
}}
|
|
>
|
|
All recent PA
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* File Upload Zone */}
|
|
<ClaimDocumentsUploadMultiple />
|
|
|
|
{/* Claim Form Modal */}
|
|
{isClaimFormOpen && selectedPatientId !== null && (
|
|
<ClaimForm
|
|
patientId={selectedPatientId}
|
|
appointmentId={selectedAppointmentId ?? undefined}
|
|
autoSubmit={mode === "direct" || !!chatbotAutoSubmitSiteKey}
|
|
autoSubmitSiteKey={chatbotAutoSubmitSiteKey}
|
|
initialTab={chatbotInitialTab}
|
|
autoSubmitPreauth={chatbotAutoSubmitPreauthSiteKey}
|
|
proceduresOnly={mode === "procedures"}
|
|
onClose={closeClaim}
|
|
onSubmit={handleClaimSubmit}
|
|
onHandleUpdatePatient={handleUpdatePatient}
|
|
onHandleForMHSeleniumClaim={handleMHClaimSubmitSelenium}
|
|
onHandleForMHSeleniumClaimPreAuth={handleMHClaimPreAuthSubmitSelenium}
|
|
onHandleForCCASeleniumClaim={handleCCAClaimSubmitSelenium}
|
|
onHandleForCCASeleniumPreAuth={handleCCAPreAuthSubmitSelenium}
|
|
onHandleForUnitedDHSeleniumPreAuth={handleUnitedDHPreAuthSubmitSelenium}
|
|
onHandleForDDMASeleniumClaim={handleDDMAClaimSubmitSelenium}
|
|
onHandleForUnitedDHSeleniumClaim={handleUnitedDHClaimSubmitSelenium}
|
|
onHandleForTuftsSCOSeleniumPreAuth={handleTuftsSCOPreAuthSubmitSelenium}
|
|
onHandleForTuftsSCOSeleniumClaim={handleTuftsSCOClaimSubmitSelenium}
|
|
isLicensed={isLicensed}
|
|
/>
|
|
)}
|
|
|
|
{/* PDF Preview Modal */}
|
|
<PdfPreviewModal
|
|
open={previewOpen}
|
|
onClose={() => {
|
|
setPreviewOpen(false);
|
|
setPreviewPdfId(null);
|
|
setPreviewFallbackFilename(null);
|
|
}}
|
|
pdfId={previewPdfId}
|
|
fallbackFilename={previewFallbackFilename}
|
|
/>
|
|
|
|
{/* DDMA Claim OTP Modal */}
|
|
{ddmaClaimOtpOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Enter OTP — Delta MA Claim</h2>
|
|
<button type="button" onClick={() => setDdmaClaimOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
The Delta Dental MA portal requires a one-time password (OTP) to continue claim submission.
|
|
</p>
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
|
|
if (input?.value.trim()) handleDdmaClaimOtpSubmit(input.value.trim());
|
|
}} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="ddma-claim-otp" className="text-sm font-medium">OTP</label>
|
|
<input
|
|
id="ddma-claim-otp"
|
|
name="otp"
|
|
placeholder="Enter OTP code"
|
|
autoFocus
|
|
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setDdmaClaimOtpOpen(false)} disabled={ddmaClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
|
|
<button type="submit" disabled={ddmaClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
|
|
{ddmaClaimOtpSubmitting ? "Submitting..." : "Submit OTP"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tufts SCO Claim OTP Modal */}
|
|
{tuftsSCOClaimOtpOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Enter OTP — Tufts SCO Claim</h2>
|
|
<button type="button" onClick={() => setTuftsSCOClaimOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
The Tufts SCO (DentaQuest) portal requires a one-time password (OTP) to continue claim submission.
|
|
</p>
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
|
|
if (input?.value.trim()) handleTuftsSCOClaimOtpSubmit(input.value.trim());
|
|
}} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="tuftssco-claim-otp" className="text-sm font-medium">OTP</label>
|
|
<input
|
|
id="tuftssco-claim-otp"
|
|
name="otp"
|
|
placeholder="Enter OTP code"
|
|
autoFocus
|
|
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setTuftsSCOClaimOtpOpen(false)} disabled={tuftsSCOClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
|
|
<button type="submit" disabled={tuftsSCOClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
|
|
{tuftsSCOClaimOtpSubmitting ? "Submitting..." : "Submit OTP"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* United/DentalHub PreAuth OTP Modal */}
|
|
{unitedDHPreAuthOtpOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Enter OTP — United/DentalHub PreAuth</h2>
|
|
<button type="button" onClick={() => setUnitedDHPreAuthOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
The United/DentalHub portal requires a one-time password (OTP) to continue pre-authorization submission.
|
|
</p>
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
|
|
if (input?.value.trim()) handleUnitedDHPreAuthOtpSubmit(input.value.trim());
|
|
}} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="uniteddh-preauth-otp" className="text-sm font-medium">OTP</label>
|
|
<input
|
|
id="uniteddh-preauth-otp"
|
|
name="otp"
|
|
placeholder="Enter OTP code"
|
|
autoFocus
|
|
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setUnitedDHPreAuthOtpOpen(false)} disabled={unitedDHPreAuthOtpSubmitting}
|
|
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
|
|
<button type="submit" disabled={unitedDHPreAuthOtpSubmitting}
|
|
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
|
|
{unitedDHPreAuthOtpSubmitting ? "Submitting..." : "Submit OTP"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tufts SCO PreAuth OTP Modal */}
|
|
{tuftsSCOPreAuthOtpOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Enter OTP — Tufts SCO PreAuth</h2>
|
|
<button type="button" onClick={() => setTuftsSCOPreAuthOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
The Tufts SCO (DentaQuest) portal requires a one-time password (OTP) to continue pre-authorization submission.
|
|
</p>
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
|
|
if (input?.value.trim()) handleTuftsSCOPreAuthOtpSubmit(input.value.trim());
|
|
}} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="tuftssco-preauth-otp" className="text-sm font-medium">OTP</label>
|
|
<input
|
|
id="tuftssco-preauth-otp"
|
|
name="otp"
|
|
placeholder="Enter OTP code"
|
|
autoFocus
|
|
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setTuftsSCOPreAuthOtpOpen(false)} disabled={tuftsSCOPreAuthOtpSubmitting}
|
|
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
|
|
<button type="submit" disabled={tuftsSCOPreAuthOtpSubmitting}
|
|
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
|
|
{tuftsSCOPreAuthOtpSubmitting ? "Submitting..." : "Submit OTP"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* United/DentalHub Claim OTP Modal */}
|
|
{unitedDHClaimOtpOpen && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold">Enter OTP — United/DentalHub Claim</h2>
|
|
<button type="button" onClick={() => setUnitedDHClaimOtpOpen(false)} className="text-slate-500 hover:text-slate-800">✕</button>
|
|
</div>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
The United/DentalHub portal requires a one-time password (OTP) to continue claim submission.
|
|
</p>
|
|
<form onSubmit={(e) => {
|
|
e.preventDefault();
|
|
const input = (e.currentTarget.elements.namedItem("otp") as HTMLInputElement);
|
|
if (input?.value.trim()) handleUnitedDHClaimOtpSubmit(input.value.trim());
|
|
}} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label htmlFor="uniteddh-claim-otp" className="text-sm font-medium">OTP</label>
|
|
<input
|
|
id="uniteddh-claim-otp"
|
|
name="otp"
|
|
placeholder="Enter OTP code"
|
|
autoFocus
|
|
className="w-full border rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary"
|
|
/>
|
|
</div>
|
|
<div className="flex justify-end gap-3">
|
|
<button type="button" onClick={() => setUnitedDHClaimOtpOpen(false)} disabled={unitedDHClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm border rounded-md hover:bg-slate-50 disabled:opacity-50">Cancel</button>
|
|
<button type="submit" disabled={unitedDHClaimOtpSubmitting}
|
|
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary/90 disabled:opacity-50">
|
|
{unitedDHClaimOtpSubmitting ? "Submitting..." : "Submit OTP"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|