feat(check-all-eligibility) - v1

This commit is contained in:
2025-10-29 19:42:54 +05:30
parent 296a77fa61
commit 9a3da21695
6 changed files with 548 additions and 13 deletions

View File

@@ -21,6 +21,7 @@ import {
StickyNote,
Shield,
FileCheck,
LoaderCircleIcon,
} from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { Calendar } from "@/components/ui/calendar";
@@ -44,6 +45,12 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { Label } from "@/components/ui/label";
import { useAppDispatch, useAppSelector } from "@/redux/hooks";
import {
clearTaskStatus,
setTaskStatus,
} from "@/redux/slices/seleniumEligibilityBatchCheckTaskSlice";
import { SeleniumTaskBanner } from "@/components/ui/selenium-task-banner";
// Define types for scheduling
interface TimeSlot {
@@ -93,6 +100,14 @@ export default function AppointmentsPage() {
open: boolean;
appointmentId?: number;
}>({ open: false });
const dispatch = useAppDispatch();
const batchTask = useAppSelector(
(state) => state.seleniumEligibilityBatchCheckTask
);
const [isCheckingAllElig, setIsCheckingAllElig] = useState(false);
const [processedAppointmentIds, setProcessedAppointmentIds] = useState<
Record<number, boolean>
>({});
const [, setLocation] = useLocation();
@@ -688,8 +703,166 @@ export default function AppointmentsPage() {
console.log(`Opening clinic notes for appointment: ${appointmentId}`);
};
const handleCheckAllEligibilities = async () => {
if (!user) {
toast({
title: "Unauthorized",
description: "Please login to perform this action.",
variant: "destructive",
});
return;
}
const dateParam = formattedSelectedDate; // existing variable in your component
// Start: set redux task status (visible globally)
dispatch(
setTaskStatus({
status: "pending",
message: `Checking eligibility for appointments on ${dateParam}...`,
})
);
setIsCheckingAllElig(true);
setProcessedAppointmentIds({});
try {
const res = await apiRequest(
"POST",
`/api/insurance-status/appointments/check-all-eligibilities?date=${dateParam}`,
{}
);
// read body for all cases so we can show per-appointment info
let body: any;
try {
body = await res.json();
} catch (e) {
body = null;
}
if (!res.ok) {
const errMsg = body?.error ?? `Server error ${res.status}`;
// global error
dispatch(
setTaskStatus({
status: "error",
message: `Batch eligibility failed: ${errMsg}`,
})
);
toast({
title: "Batch check failed",
description: errMsg,
variant: "destructive",
});
return;
}
const results: any[] = Array.isArray(body?.results) ? body.results : [];
// Map appointmentId -> appointment so we can show human friendly toasts
const appointmentMap = new Map<number, Appointment>();
for (const a of appointments) {
if (a && typeof a.id === "number")
appointmentMap.set(a.id as number, a);
}
// Counters for summary
let successCount = 0;
let skippedCount = 0;
let warningCount = 0;
// Show toast for each skipped appointment (error) and for warnings.
for (const r of results) {
const aptId = Number(r.appointmentId);
const apt = appointmentMap.get(aptId);
const patientName = apt
? patientsFromDay.find((p) => p.id === apt.patientId)
? `${patientsFromDay.find((p) => p.id === apt.patientId)!.firstName ?? ""} ${patientsFromDay.find((p) => p.id === apt.patientId)!.lastName ?? ""}`.trim()
: `patient#${apt.patientId ?? "?"}`
: `appointment#${aptId}`;
const aptTime = apt ? `${apt.date ?? ""} ${apt.startTime ?? ""}` : "";
if (r.error) {
skippedCount++;
toast({
title: `Skipped: ${patientName}`,
description: `${aptTime}${r.error}`,
variant: "destructive",
});
console.warn("[batch skipped]", aptId, r.error);
} else if (r.warning) {
warningCount++;
toast({
title: `Warning: ${patientName}`,
description: `${aptTime}${r.warning}`,
variant: "destructive",
});
console.info("[batch warning]", aptId, r.warning);
} else if (r.processed) {
successCount++;
// optional: show small non-intrusive toast or nothing for each success.
// comment-in to notify successes (may create many toasts):
// toast({ title: `Processed: ${patientName}`, description: `${aptTime}`, variant: "default" });
} else {
// fallback: treat as skipped
skippedCount++;
toast({
title: `Skipped: ${patientName}`,
description: `${aptTime} — Unknown reason`,
variant: "destructive",
});
}
}
// Invalidate queries so UI repaints with updated patient statuses
queryClient.invalidateQueries({
queryKey: qkAppointmentsDay(formattedSelectedDate),
});
// global success status (summary)
dispatch(
setTaskStatus({
status: skippedCount > 0 ? "error" : "success",
message: `Batch processed ${results.length} appointments — success: ${successCount}, warnings: ${warningCount}, skipped: ${skippedCount}.`,
})
);
// also show final toast summary
toast({
title: "Batch complete",
description: `Processed ${results.length} appointments — success: ${successCount}, warnings: ${warningCount}, skipped: ${skippedCount}.`,
variant: skippedCount > 0 ? "destructive" : "default",
});
} catch (err: any) {
console.error("[check-all-eligibilities] error", err);
dispatch(
setTaskStatus({
status: "error",
message: `Batch eligibility error: ${err?.message ?? String(err)}`,
})
);
toast({
title: "Batch check failed",
description: err?.message ?? String(err),
variant: "destructive",
});
} finally {
setIsCheckingAllElig(false);
// intentionally do not clear task status here so banner persists until user dismisses it
}
};
return (
<div className="">
<div>
<SeleniumTaskBanner
status={batchTask.status}
message={batchTask.message}
show={batchTask.show}
onClear={() => dispatch(clearTaskStatus())}
/>
<div className="container mx-auto">
<div className="flex justify-between items-center mb-6">
<div>
@@ -700,17 +873,36 @@ export default function AppointmentsPage() {
View and manage the dental practice schedule
</p>
</div>
<Button
onClick={() => {
setEditingAppointment(undefined);
setIsAddModalOpen(true);
}}
className="gap-1"
disabled={isLoading}
>
<Plus className="h-4 w-4" />
New Appointment
</Button>
<div className="flex justify-between gap-2">
<Button
onClick={() => {
setEditingAppointment(undefined);
setIsAddModalOpen(true);
}}
disabled={isLoading}
>
<Plus className="h-4 w-4" />
New Appointment
</Button>
<Button
onClick={() => handleCheckAllEligibilities()}
disabled={isLoading || isCheckingAllElig}
>
{isCheckingAllElig ? (
<>
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
Checking...
</>
) : (
<>
<Shield className="h-4 w-4 mr-2" />
Check all eligibilities
</>
)}
</Button>
</div>
</div>
{/* Context Menu */}