feat(check-all-eligibility) - v1
This commit is contained in:
@@ -473,4 +473,286 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/appointments/check-all-eligibilities",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
// Query param: date=YYYY-MM-DD (required)
|
||||||
|
const date = String(req.query.date ?? "").trim();
|
||||||
|
if (!date) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing date query param (YYYY-MM-DD)" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.user || !req.user.id) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track any paths that couldn't be cleaned immediately so we can try again at the end
|
||||||
|
const remainingCleanupPaths = new Set<string>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1) fetch appointments for the day (reuse your storage API)
|
||||||
|
const dayAppointments = await storage.getAppointmentsByDateForUser(
|
||||||
|
date,
|
||||||
|
req.user.id
|
||||||
|
);
|
||||||
|
if (!Array.isArray(dayAppointments)) {
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Failed to load appointments for date" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Array<any> = [];
|
||||||
|
|
||||||
|
// process sequentially so selenium agent / python semaphore isn't overwhelmed
|
||||||
|
for (const apt of dayAppointments) {
|
||||||
|
// For each appointment we keep a per-appointment seleniumResult so we can cleanup its files
|
||||||
|
let seleniumResult: any = undefined;
|
||||||
|
|
||||||
|
const resultItem: any = {
|
||||||
|
appointmentId: apt.id,
|
||||||
|
patientId: apt.patientId ?? null,
|
||||||
|
processed: false,
|
||||||
|
error: null,
|
||||||
|
pdfFileId: null,
|
||||||
|
patientUpdateStatus: null,
|
||||||
|
warning: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// fetch patient record (use getPatient or getPatientById depending on your storage)
|
||||||
|
const patient = apt.patientId
|
||||||
|
? await storage.getPatient(apt.patientId)
|
||||||
|
: null;
|
||||||
|
const memberId = (patient?.insuranceId ?? "").toString().trim();
|
||||||
|
|
||||||
|
// create a readable patient label for error messages
|
||||||
|
const patientLabel = patient
|
||||||
|
? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() ||
|
||||||
|
`patient#${patient.id}`
|
||||||
|
: `patient#${apt.patientId ?? "unknown"}`;
|
||||||
|
|
||||||
|
const aptLabel = `appointment#${apt.id}${apt.date ? ` (${apt.date}${apt.startTime ? ` ${apt.startTime}` : ""})` : ""}`;
|
||||||
|
|
||||||
|
if (!memberId) {
|
||||||
|
resultItem.error = `Missing insuranceId for ${patientLabel} — skipping ${aptLabel}`;
|
||||||
|
|
||||||
|
results.push(resultItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare eligibility data; prefer patient DOB + name if present
|
||||||
|
const dob = patient?.dateOfBirth ? patient.dateOfBirth : null; // string | Date
|
||||||
|
const payload = {
|
||||||
|
memberId,
|
||||||
|
dateOfBirth: dob,
|
||||||
|
insuranceSiteKey: "MH",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get credentials for this user+site
|
||||||
|
const credentials =
|
||||||
|
await storage.getInsuranceCredentialByUserAndSiteKey(
|
||||||
|
req.user.id,
|
||||||
|
payload.insuranceSiteKey
|
||||||
|
);
|
||||||
|
if (!credentials) {
|
||||||
|
resultItem.error = `No insurance credentials found for siteKey — skipping ${aptLabel} for ${patientLabel}`;
|
||||||
|
results.push(resultItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich payload
|
||||||
|
const enriched = {
|
||||||
|
...payload,
|
||||||
|
massdhpUsername: credentials.username,
|
||||||
|
massdhpPassword: credentials.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
// forward to selenium agent (sequential)
|
||||||
|
try {
|
||||||
|
seleniumResult =
|
||||||
|
await forwardToSeleniumInsuranceEligibilityAgent(enriched);
|
||||||
|
} catch (seleniumErr: any) {
|
||||||
|
resultItem.error = `Selenium agent failed for ${patientLabel} (${aptLabel}): ${seleniumErr?.message ?? String(seleniumErr)}`;
|
||||||
|
results.push(resultItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt extraction (if pdf_path present)
|
||||||
|
const extracted: any = {};
|
||||||
|
if (
|
||||||
|
seleniumResult?.pdf_path &&
|
||||||
|
seleniumResult.pdf_path.endsWith(".pdf")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const pdfPath = seleniumResult.pdf_path;
|
||||||
|
const pdfBuffer = await fs.readFile(pdfPath);
|
||||||
|
|
||||||
|
const extraction = await forwardToPatientDataExtractorService({
|
||||||
|
buffer: pdfBuffer,
|
||||||
|
originalname: path.basename(pdfPath),
|
||||||
|
mimetype: "application/pdf",
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (extraction.name) {
|
||||||
|
const parts = splitName(extraction.name);
|
||||||
|
extracted.firstName = parts.firstName;
|
||||||
|
extracted.lastName = parts.lastName;
|
||||||
|
}
|
||||||
|
} catch (extractErr: any) {
|
||||||
|
resultItem.warning = `Extraction failed: ${extractErr?.message ?? String(extractErr)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create or update patient by insuranceId — prefer extracted name
|
||||||
|
const preferFirst = extracted.firstName ?? null;
|
||||||
|
const preferLast = extracted.lastName ?? null;
|
||||||
|
try {
|
||||||
|
await createOrUpdatePatientByInsuranceId({
|
||||||
|
insuranceId: memberId,
|
||||||
|
firstName: preferFirst,
|
||||||
|
lastName: preferLast,
|
||||||
|
dob: payload.dateOfBirth,
|
||||||
|
userId: req.user.id,
|
||||||
|
});
|
||||||
|
} catch (patientOpErr: any) {
|
||||||
|
resultItem.error = `Failed to create/update patient ${patientLabel} for ${aptLabel}: ${patientOpErr?.message ?? String(patientOpErr)}`;
|
||||||
|
results.push(resultItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetch patient again
|
||||||
|
const updatedPatient =
|
||||||
|
await storage.getPatientByInsuranceId(memberId);
|
||||||
|
if (!updatedPatient || !updatedPatient.id) {
|
||||||
|
resultItem.error = `Patient not found after create/update for ${patientLabel} (${aptLabel})`;
|
||||||
|
results.push(resultItem);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update patient status based on seleniumResult.eligibility
|
||||||
|
const newStatus =
|
||||||
|
seleniumResult?.eligibility === "Y" ? "active" : "inactive";
|
||||||
|
await storage.updatePatient(updatedPatient.id, { status: newStatus });
|
||||||
|
resultItem.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
|
// If PDF exists, upload to PdfGroup (ELIGIBILITY_STATUS)
|
||||||
|
if (
|
||||||
|
seleniumResult?.pdf_path &&
|
||||||
|
seleniumResult.pdf_path.endsWith(".pdf")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const pdfBuf = await fs.readFile(seleniumResult.pdf_path);
|
||||||
|
const groupTitle = "Eligibility Status";
|
||||||
|
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||||
|
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||||
|
updatedPatient.id,
|
||||||
|
groupTitleKey
|
||||||
|
);
|
||||||
|
if (!group) {
|
||||||
|
group = await storage.createPdfGroup(
|
||||||
|
updatedPatient.id,
|
||||||
|
groupTitle,
|
||||||
|
groupTitleKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!group?.id)
|
||||||
|
throw new Error("Failed to create/find pdf group");
|
||||||
|
|
||||||
|
const created = await storage.createPdfFile(
|
||||||
|
group.id,
|
||||||
|
path.basename(seleniumResult.pdf_path),
|
||||||
|
pdfBuf
|
||||||
|
);
|
||||||
|
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
resultItem.pdfFileId = Number(created.id);
|
||||||
|
} else if (typeof created === "number") {
|
||||||
|
resultItem.pdfFileId = created;
|
||||||
|
} else if (created && (created as any).id) {
|
||||||
|
resultItem.pdfFileId = (created as any).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultItem.processed = true;
|
||||||
|
} catch (pdfErr: any) {
|
||||||
|
resultItem.warning = `PDF upload failed for ${patientLabel} (${aptLabel}): ${pdfErr?.message ?? String(pdfErr)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// no pdf; still mark processed true (status updated)
|
||||||
|
resultItem.processed = true;
|
||||||
|
resultItem.pdfFileId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push(resultItem);
|
||||||
|
} catch (err: any) {
|
||||||
|
resultItem.error = `Unexpected error for appointment#${apt.id}: ${err?.message ?? String(err)}`;
|
||||||
|
results.push(resultItem);
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
"[batch eligibility] unexpected error for appointment",
|
||||||
|
apt.id,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Per-appointment cleanup: always try to remove selenium temp files for this appointment
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
seleniumResult &&
|
||||||
|
(seleniumResult.pdf_path || seleniumResult.ss_path)
|
||||||
|
) {
|
||||||
|
// prefer pdf_path, fallback to ss_path
|
||||||
|
const candidatePath =
|
||||||
|
seleniumResult.pdf_path ?? seleniumResult.ss_path;
|
||||||
|
try {
|
||||||
|
await emptyFolderContainingFile(candidatePath);
|
||||||
|
} catch (cleanupErr: any) {
|
||||||
|
console.warn(
|
||||||
|
`[batch cleanup] failed to clean ${candidatePath} for appointment ${apt.id}`,
|
||||||
|
cleanupErr
|
||||||
|
);
|
||||||
|
// remember path for final cleanup attempt
|
||||||
|
remainingCleanupPaths.add(candidatePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (cleanupOuterErr: any) {
|
||||||
|
console.warn(
|
||||||
|
"[batch cleanup] unexpected error during per-appointment cleanup",
|
||||||
|
cleanupOuterErr
|
||||||
|
);
|
||||||
|
// don't throw — we want to continue processing next appointments
|
||||||
|
}
|
||||||
|
} // end try/catch/finally per appointment
|
||||||
|
} // end for appointments
|
||||||
|
|
||||||
|
// return summary
|
||||||
|
return res.json({ date, count: results.length, results });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[check-all-eligibilities] error", err);
|
||||||
|
return res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: err?.message ?? "Internal server error" });
|
||||||
|
} finally {
|
||||||
|
// Final cleanup attempt for any remaining paths we couldn't delete earlier
|
||||||
|
try {
|
||||||
|
if (remainingCleanupPaths.size > 0) {
|
||||||
|
for (const p of remainingCleanupPaths) {
|
||||||
|
try {
|
||||||
|
await emptyFolderContainingFile(p);
|
||||||
|
} catch (finalCleanupErr: any) {
|
||||||
|
console.error(`[final cleanup] failed for ${p}`, finalCleanupErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (outerFinalErr: any) {
|
||||||
|
console.error(
|
||||||
|
"[check-all-eligibilities final cleanup] unexpected error",
|
||||||
|
outerFinalErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export interface IStorage {
|
|||||||
startTime: string,
|
startTime: string,
|
||||||
excludeId: number
|
excludeId: number
|
||||||
): Promise<Appointment | undefined>;
|
): Promise<Appointment | undefined>;
|
||||||
|
getAppointmentsByDateForUser(dateStr: string, userId: number): Promise<Appointment[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appointmentsStorage: IStorage = {
|
export const appointmentsStorage: IStorage = {
|
||||||
@@ -195,4 +196,31 @@ export const appointmentsStorage: IStorage = {
|
|||||||
})) ?? undefined
|
})) ?? undefined
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getAppointmentsByDateForUser
|
||||||
|
* dateStr expected as "YYYY-MM-DD" (same string your frontend sends)
|
||||||
|
* returns appointments for that date (local midnight-to-midnight) filtered by userId
|
||||||
|
*/
|
||||||
|
async getAppointmentsByDateForUser(dateStr: string, userId: number): Promise<Appointment[]> {
|
||||||
|
// defensive parsing — if invalid, throw so caller can handle
|
||||||
|
const start = new Date(dateStr);
|
||||||
|
if (Number.isNaN(start.getTime())) {
|
||||||
|
throw new Error(`Invalid date string passed to getAppointmentsByDateForUser: ${dateStr}`);
|
||||||
|
}
|
||||||
|
// create exclusive end (next day midnight)
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + 1);
|
||||||
|
|
||||||
|
return db.appointment.findMany({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
date: {
|
||||||
|
gte: start,
|
||||||
|
lt: end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: { startTime: "asc" },
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
StickyNote,
|
StickyNote,
|
||||||
Shield,
|
Shield,
|
||||||
FileCheck,
|
FileCheck,
|
||||||
|
LoaderCircleIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
@@ -44,6 +45,12 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { Label } from "@/components/ui/label";
|
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
|
// Define types for scheduling
|
||||||
interface TimeSlot {
|
interface TimeSlot {
|
||||||
@@ -93,6 +100,14 @@ export default function AppointmentsPage() {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
appointmentId?: number;
|
appointmentId?: number;
|
||||||
}>({ open: false });
|
}>({ 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();
|
const [, setLocation] = useLocation();
|
||||||
|
|
||||||
@@ -688,8 +703,166 @@ export default function AppointmentsPage() {
|
|||||||
console.log(`Opening clinic notes for appointment: ${appointmentId}`);
|
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 (
|
return (
|
||||||
<div className="">
|
<div>
|
||||||
|
<SeleniumTaskBanner
|
||||||
|
status={batchTask.status}
|
||||||
|
message={batchTask.message}
|
||||||
|
show={batchTask.show}
|
||||||
|
onClear={() => dispatch(clearTaskStatus())}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="container mx-auto">
|
<div className="container mx-auto">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -700,17 +873,36 @@ export default function AppointmentsPage() {
|
|||||||
View and manage the dental practice schedule
|
View and manage the dental practice schedule
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<div className="flex justify-between gap-2">
|
||||||
setEditingAppointment(undefined);
|
<Button
|
||||||
setIsAddModalOpen(true);
|
onClick={() => {
|
||||||
}}
|
setEditingAppointment(undefined);
|
||||||
className="gap-1"
|
setIsAddModalOpen(true);
|
||||||
disabled={isLoading}
|
}}
|
||||||
>
|
disabled={isLoading}
|
||||||
<Plus className="h-4 w-4" />
|
>
|
||||||
New Appointment
|
<Plus className="h-4 w-4" />
|
||||||
</Button>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Context Menu */}
|
{/* Context Menu */}
|
||||||
|
|||||||
@@ -28,6 +28,5 @@ const seleniumClaimSubmitTaskSlice = createSlice({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ Make sure you're exporting from the renamed slice
|
|
||||||
export const { setTaskStatus, clearTaskStatus } = seleniumClaimSubmitTaskSlice.actions;
|
export const { setTaskStatus, clearTaskStatus } = seleniumClaimSubmitTaskSlice.actions;
|
||||||
export default seleniumClaimSubmitTaskSlice.reducer;
|
export default seleniumClaimSubmitTaskSlice.reducer;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
export type TaskStatus = "idle" | "pending" | "success" | "error";
|
||||||
|
|
||||||
|
export interface SeleniumTaskState {
|
||||||
|
status: TaskStatus;
|
||||||
|
message: string;
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SeleniumTaskState = {
|
||||||
|
status: "idle",
|
||||||
|
message: "",
|
||||||
|
show: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const seleniumEligibilityBatchCheckTaskSlice = createSlice({
|
||||||
|
name: "seleniumEligibilityBatchCheckTask",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setTaskStatus: (
|
||||||
|
state: SeleniumTaskState,
|
||||||
|
action: PayloadAction<Partial<SeleniumTaskState>>
|
||||||
|
) => {
|
||||||
|
return { ...state, ...action.payload, show: true };
|
||||||
|
},
|
||||||
|
clearTaskStatus: () => initialState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setTaskStatus, clearTaskStatus } = seleniumEligibilityBatchCheckTaskSlice.actions;
|
||||||
|
export default seleniumEligibilityBatchCheckTaskSlice.reducer;
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import seleniumClaimSubmitTaskReducer from "./slices/seleniumClaimSubmitTaskSlice";
|
import seleniumClaimSubmitTaskReducer from "./slices/seleniumClaimSubmitTaskSlice";
|
||||||
import seleniumEligibilityCheckTaskReducer from "./slices/seleniumEligibilityCheckTaskSlice";
|
import seleniumEligibilityCheckTaskReducer from "./slices/seleniumEligibilityCheckTaskSlice";
|
||||||
|
import seleniumEligibilityBatchCheckTaskReducer from "./slices/seleniumEligibilityBatchCheckTaskSlice";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
seleniumClaimSubmitTask: seleniumClaimSubmitTaskReducer,
|
seleniumClaimSubmitTask: seleniumClaimSubmitTaskReducer,
|
||||||
seleniumEligibilityCheckTask: seleniumEligibilityCheckTaskReducer,
|
seleniumEligibilityCheckTask: seleniumEligibilityCheckTaskReducer,
|
||||||
|
seleniumEligibilityBatchCheckTask: seleniumEligibilityBatchCheckTaskReducer,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user