initial commit
This commit is contained in:
657
apps/Frontend/src/pages/insurance-status-page.tsx
Executable file
657
apps/Frontend/src/pages/insurance-status-page.tsx
Executable file
@@ -0,0 +1,657 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { CheckCircle, LoaderCircleIcon } from "lucide-react";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { PatientTable } from "@/components/patients/patient-table";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
|
||||
import {
|
||||
setTaskStatus,
|
||||
clearTaskStatus,
|
||||
} from "@/redux/slices/seleniumEligibilityCheckTaskSlice";
|
||||
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
|
||||
import { formatLocalDate, parseLocalDate } from "@/utils/dateUtils";
|
||||
import { InsertPatient, Patient } from "@repo/db/types";
|
||||
import { DateInput } from "@/components/ui/dateInput";
|
||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||
import { useLocation } from "wouter";
|
||||
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||
|
||||
export default function InsuranceStatusPage() {
|
||||
const { user } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const dispatch = useAppDispatch();
|
||||
const { status, message, show } = useAppSelector(
|
||||
(state) => state.seleniumEligibilityCheckTask,
|
||||
);
|
||||
const [selectedPatient, setSelectedPatient] = useState<Patient | null>(null);
|
||||
const [location] = useLocation();
|
||||
|
||||
// Insurance eligibility and claim check form fields
|
||||
const [memberId, setMemberId] = useState("");
|
||||
const [dateOfBirth, setDateOfBirth] = useState<Date | null>(null);
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
const isFormIncomplete = !memberId || !dateOfBirth;
|
||||
const [isCheckingEligibilityStatus, setIsCheckingEligibilityStatus] =
|
||||
useState(false);
|
||||
const [isCheckingClaimStatus, setIsCheckingClaimStatus] = useState(false);
|
||||
|
||||
// PDF preview modal state
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [previewPdfId, setPreviewPdfId] = useState<number | null>(null);
|
||||
const [previewFallbackFilename, setPreviewFallbackFilename] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
// Populate fields from selected patient
|
||||
useEffect(() => {
|
||||
if (selectedPatient) {
|
||||
setMemberId(selectedPatient.insuranceId ?? "");
|
||||
setFirstName(selectedPatient.firstName ?? "");
|
||||
setLastName(selectedPatient.lastName ?? "");
|
||||
|
||||
const dob =
|
||||
typeof selectedPatient.dateOfBirth === "string"
|
||||
? parseLocalDate(selectedPatient.dateOfBirth)
|
||||
: selectedPatient.dateOfBirth;
|
||||
setDateOfBirth(dob);
|
||||
} else {
|
||||
setMemberId("");
|
||||
setFirstName("");
|
||||
setLastName("");
|
||||
setDateOfBirth(null);
|
||||
}
|
||||
}, [selectedPatient]);
|
||||
|
||||
// Add patient mutation
|
||||
const addPatientMutation = useMutation({
|
||||
mutationFn: async (patient: InsertPatient) => {
|
||||
const res = await apiRequest("POST", "/api/patients/", patient);
|
||||
return res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Patient added successfully!",
|
||||
variant: "default",
|
||||
});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
const msg = error.message;
|
||||
|
||||
if (msg === "A patient with this insurance ID already exists.") {
|
||||
toast({
|
||||
title: "Patient already exists",
|
||||
description: msg,
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to add patient: ${msg}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// handle eligibility selenium
|
||||
const handleEligibilityCheckSelenium = async () => {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
|
||||
const data = {
|
||||
memberId,
|
||||
dateOfBirth: formattedDob,
|
||||
insuranceSiteKey: "MH",
|
||||
};
|
||||
try {
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "pending",
|
||||
message: "Sending Data to Selenium...",
|
||||
}),
|
||||
);
|
||||
const response = await apiRequest(
|
||||
"POST",
|
||||
"/api/insurance-status/eligibility-check",
|
||||
{ data: data },
|
||||
);
|
||||
// { data: JSON.stringify(data) },
|
||||
const result = await response.json();
|
||||
if (result.error) throw new Error(result.error);
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "success",
|
||||
message:
|
||||
"Patient status is updated, and its eligibility pdf is uploaded at Document Page.",
|
||||
}),
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "Selenium service done.",
|
||||
description:
|
||||
"Your Patient Eligibility is fetched and updated, Kindly search through the patient.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
|
||||
// If server returned pdfFileId: open preview modal
|
||||
if (result.pdfFileId) {
|
||||
setPreviewPdfId(Number(result.pdfFileId));
|
||||
// optional fallback name while header is parsed
|
||||
setPreviewFallbackFilename(
|
||||
result.pdfFilename ?? `eligibility_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "error",
|
||||
message: error.message || "Selenium submission failed",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Selenium service error",
|
||||
description: error.message || "An error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Claim Status Check Selenium
|
||||
const handleStatusCheckSelenium = async () => {
|
||||
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||
|
||||
const data = {
|
||||
memberId,
|
||||
dateOfBirth: formattedDob,
|
||||
insuranceSiteKey: "MH",
|
||||
};
|
||||
try {
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "pending",
|
||||
message: "Sending Data to Selenium...",
|
||||
}),
|
||||
);
|
||||
const response = await apiRequest(
|
||||
"POST",
|
||||
"/api/insurance-status/claim-status-check",
|
||||
{ data: JSON.stringify(data) },
|
||||
);
|
||||
const result = await response.json();
|
||||
if (result.error) throw new Error(result.error);
|
||||
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "success",
|
||||
message:
|
||||
"Claim status is updated, and its pdf is uploaded at Document Page.",
|
||||
}),
|
||||
);
|
||||
|
||||
toast({
|
||||
title: "Selenium service done.",
|
||||
description:
|
||||
"Your Claim Status is fetched and updated, Kindly search through the patient.",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
setSelectedPatient(null);
|
||||
|
||||
// If server returned pdfFileId: open preview modal
|
||||
if (result.pdfFileId) {
|
||||
setPreviewPdfId(Number(result.pdfFileId));
|
||||
// optional fallback name while header is parsed
|
||||
setPreviewFallbackFilename(
|
||||
result.pdfFilename ?? `eligibility_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}
|
||||
} catch (error: any) {
|
||||
dispatch(
|
||||
setTaskStatus({
|
||||
status: "error",
|
||||
message: error.message || "Selenium submission failed",
|
||||
}),
|
||||
);
|
||||
toast({
|
||||
title: "Selenium service error",
|
||||
description: error.message || "An error occurred.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddPatient = async () => {
|
||||
const newPatient: InsertPatient = {
|
||||
firstName,
|
||||
lastName,
|
||||
dateOfBirth: dateOfBirth,
|
||||
gender: "",
|
||||
phone: "",
|
||||
userId: user?.id ?? 1,
|
||||
insuranceId: memberId,
|
||||
};
|
||||
await addPatientMutation.mutateAsync(newPatient);
|
||||
};
|
||||
|
||||
// Handle insurance provider eligibility button clicks
|
||||
const handleMHEligibilityButton = async () => {
|
||||
// Form Fields check
|
||||
if (!memberId || !dateOfBirth) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description:
|
||||
"Please fill in all the required fields: Member ID, Date of Birth.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingEligibilityStatus(true);
|
||||
|
||||
try {
|
||||
await handleEligibilityCheckSelenium();
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
} finally {
|
||||
setIsCheckingEligibilityStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle insurance provider Status Check button clicks
|
||||
const handleMHStatusButton = async () => {
|
||||
// Form Fields check
|
||||
if (!memberId || !dateOfBirth || !firstName) {
|
||||
toast({
|
||||
title: "Missing Fields",
|
||||
description:
|
||||
"Please fill in all the required fields: Member ID, Date of Birth, First Name.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCheckingClaimStatus(true);
|
||||
|
||||
// Adding patient if same patient exists then it will skip.
|
||||
try {
|
||||
if (!selectedPatient) {
|
||||
await handleAddPatient();
|
||||
}
|
||||
|
||||
await handleStatusCheckSelenium();
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||
} finally {
|
||||
setIsCheckingClaimStatus(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
// handling case-1, when redirect happens from appointment page:
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const appointmentId = params.get("appointmentId");
|
||||
const action = params.get("action"); // 'eligibility' | 'claim'
|
||||
if (!appointmentId) return;
|
||||
const id = Number(appointmentId);
|
||||
if (Number.isNaN(id) || id <= 0) return;
|
||||
if (!action || (action !== "eligibility" && action !== "claim")) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/appointments/${id}/patient`);
|
||||
if (!res.ok) {
|
||||
let body: any = null;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch {}
|
||||
if (!cancelled) {
|
||||
toast({
|
||||
title: "Failed to load patient",
|
||||
description:
|
||||
body?.message ??
|
||||
body?.error ??
|
||||
`Could not fetch patient for appointment ${id}.`,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const patient = data?.patient ?? data;
|
||||
if (!cancelled && patient) {
|
||||
// set selectedPatient as before
|
||||
setSelectedPatient(patient as Patient);
|
||||
|
||||
clearUrlParams(["appointmentId", "action"]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (!cancelled) {
|
||||
console.error("Error fetching patient for appointment:", err);
|
||||
toast({
|
||||
title: "Error",
|
||||
description:
|
||||
err?.message ?? "An error occurred while fetching patient.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
// handling case-1, when redirect happens from appointment page:
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const appointmentId = params.get("appointmentId");
|
||||
if (!appointmentId) return;
|
||||
|
||||
const id = Number(appointmentId);
|
||||
if (Number.isNaN(id) || id <= 0) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await apiRequest("GET", `/api/appointments/${id}/patient`);
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
const patient = data?.patient ?? data;
|
||||
|
||||
if (!cancelled && patient) {
|
||||
// ✅ ONLY prefill patient
|
||||
setSelectedPatient(patient as Patient);
|
||||
|
||||
// ✅ clean URL (no auto selenium)
|
||||
clearUrlParams(["appointmentId", "action"]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch patient from appointment", err);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SeleniumTaskBanner
|
||||
status={status}
|
||||
message={message}
|
||||
show={show}
|
||||
onClear={() => dispatch(clearTaskStatus())}
|
||||
/>
|
||||
|
||||
<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 Eligibility and Claim Status
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Check insurance eligibility and Claim status.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Insurance Eligibility Check Form */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>Check Insurance Eligibility and Claim Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-4 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="memberId">Member ID</Label>
|
||||
<Input
|
||||
id="memberId"
|
||||
placeholder="Enter member ID"
|
||||
value={memberId}
|
||||
onChange={(e) => setMemberId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<DateInput
|
||||
label="Date of Birth"
|
||||
value={dateOfBirth}
|
||||
onChange={setDateOfBirth}
|
||||
disableFuture
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="firstName">First Name <span className="text-muted-foreground font-normal">(Optional)</span></Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
placeholder="Enter first name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lastName">Last Name <span className="text-muted-foreground font-normal">(Optional)</span></Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
placeholder="Enter last name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col-2 gap-4">
|
||||
<Button
|
||||
onClick={() => handleMHEligibilityButton()}
|
||||
className="w-full"
|
||||
disabled={isCheckingEligibilityStatus}
|
||||
>
|
||||
{isCheckingEligibilityStatus ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Eligibility
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => handleMHStatusButton()}
|
||||
className="w-full"
|
||||
disabled={isCheckingClaimStatus}
|
||||
>
|
||||
{isCheckingClaimStatus ? (
|
||||
<>
|
||||
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
MH Status
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* TEMP PROVIDER BUTTONS */}
|
||||
<div className="space-y-4 mt-6">
|
||||
<h3 className="text-sm font-medium text-muted-foreground">
|
||||
Other provider checks
|
||||
</h3>
|
||||
|
||||
{/* Row 1 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<DdmaEligibilityButton
|
||||
memberId={memberId}
|
||||
dateOfBirth={dateOfBirth}
|
||||
firstName={firstName}
|
||||
lastName={lastName}
|
||||
isFormIncomplete={isFormIncomplete}
|
||||
onPdfReady={(pdfId, fallbackFilename) => {
|
||||
setPreviewPdfId(pdfId);
|
||||
setPreviewFallbackFilename(
|
||||
fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`,
|
||||
);
|
||||
setPreviewOpen(true);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Metlife Dental
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
CCA
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Row 2 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Tufts SCO/SWH/Navi/Mass Gen
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
United SCO
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
United AAPR
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Row 3 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Aetna
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
variant="outline"
|
||||
disabled={isFormIncomplete}
|
||||
>
|
||||
<CheckCircle className="h-4 w-4 mr-2" />
|
||||
Altus
|
||||
</Button>
|
||||
<div /> {/* filler cell to keep grid shape */}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Patients Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Patient Records</CardTitle>
|
||||
<CardDescription>
|
||||
Select Patients and Check Their Eligibility
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PatientTable
|
||||
allowView={true}
|
||||
allowDelete={true}
|
||||
allowCheckbox={true}
|
||||
allowEdit={true}
|
||||
onSelectPatient={setSelectedPatient}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Pdf preview modal */}
|
||||
<PdfPreviewModal
|
||||
open={previewOpen}
|
||||
onClose={() => {
|
||||
setPreviewOpen(false);
|
||||
setPreviewPdfId(null);
|
||||
setPreviewFallbackFilename(null);
|
||||
}}
|
||||
pdfId={previewPdfId ?? undefined}
|
||||
fallbackFilename={previewFallbackFilename ?? undefined} // optional
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user