diff --git a/apps/Backend/src/routes/claims.ts b/apps/Backend/src/routes/claims.ts index 1753480d..48a7ce3f 100755 --- a/apps/Backend/src/routes/claims.ts +++ b/apps/Backend/src/routes/claims.ts @@ -1046,74 +1046,68 @@ router.post( } ); -// GET /api/claims/batch-pdf -// Query params: date=YYYY-MM-DD (required), staffIds=1,2 (required) -// Returns a ZIP archive of all INSURANCE_CLAIM PdfFile records for patients -// scheduled on that date in the given staff columns. +// GET /api/claims/filled-forms?date=YYYY-MM-DD +// ZIP of the claim forms the app fills itself (ADA paper forms, e.g. BCBS) for +// appointments on that schedule date. Claims submitted through Selenium are not included. router.get( - "/batch-pdf", + "/filled-forms", async (req: Request, res: Response): Promise => { const date = String(req.query.date ?? "").trim(); - const staffIdsRaw = String(req.query.staffIds ?? "").trim(); - if (!date) return res.status(400).json({ error: "Missing date query param" }); - if (!staffIdsRaw) return res.status(400).json({ error: "Missing staffIds query param" }); if (!req.user?.id) return res.status(401).json({ error: "Unauthorized" }); - const staffIdFilter = new Set( - staffIdsRaw.split(",").map(Number).filter((n) => Number.isFinite(n) && n > 0) - ); - try { - const allAppointments = await storage.getAppointmentsByDateForUser(date, req.user.id); - const appointments = allAppointments.filter((a) => staffIdFilter.has(Number(a.staffId))); + const appointments = await storage.getAppointmentsByDateForUser(date, req.user.id); + const appointmentIds = appointments.map((a) => a.id).filter((id): id is number => id != null); + + const { prisma: db } = await import("@repo/db/client"); + const claims = appointmentIds.length + ? await db.claim.findMany({ + where: { + appointmentId: { in: appointmentIds }, + userId: req.user.id, + status: { notIn: ["VOID", "CANCELLED"] }, + }, + select: { id: true, patientId: true }, + }) + : []; type PdfEntry = { filename: string; data: Buffer }; const pdfEntries: PdfEntry[] = []; - for (const apt of appointments) { - const patientId = apt.patientId; - if (!patientId) continue; + const claimIdsByPatient = new Map>(); + for (const c of claims) { + if (!claimIdsByPatient.has(c.patientId)) claimIdsByPatient.set(c.patientId, new Set()); + claimIdsByPatient.get(c.patientId)!.add(c.id); + } - // Claim PDFs from Selenium are stored in PdfGroup (titleKey=INSURANCE_CLAIM) → PdfFile (binary pdfData) - const group = await storage.findPdfGroupByPatientTitleKey(patientId, "INSURANCE_CLAIM"); - if (!group?.id) continue; - - const raw = await storage.getPdfFilesByGroupId(group.id); - const files = (Array.isArray(raw) ? raw : (raw as any).data ?? []) as Array<{ filename: string; pdfData: unknown }>; - - for (const f of files) { - if (!f.pdfData) continue; - // Prisma Bytes → always convert to Buffer to satisfy archiver - const buf = Buffer.isBuffer(f.pdfData) - ? f.pdfData - : Buffer.from(f.pdfData as any); - if (!buf.length) continue; - // Sanitize filename: strip path components, keep safe chars - const safeName = (f.filename || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf"; - pdfEntries.push({ filename: safeName, data: buf }); + for (const [patientId, claimIds] of claimIdsByPatient) { + // Filled forms are saved as ADA_Claim___.pdf in the patient's "Claims" folder; + // keep the newest file per claim + const newestByClaim = new Map(); + for (const cf of await storage.listPatientCategoryCloudFiles(patientId, "Claims")) { + const m = /^ADA_Claim_.*_(\d+)\.pdf$/.exec(cf.name ?? ""); + if (!m || cf.id == null) continue; + const claimId = Number(m[1]); + if (!claimIds.has(claimId)) continue; + const prev = newestByClaim.get(claimId); + if (!prev || cf.id > prev.id) newestByClaim.set(claimId, { id: cf.id, name: cf.name! }); } - - // New claim PDFs are saved to the patient's Cloud Storage "Claims" subfolder - const cloudFiles = await storage.listPatientCategoryCloudFiles(patientId, "Claims"); - for (const cf of cloudFiles) { - if (cf.id == null) continue; - const full = await storage.getFile(cf.id); + for (const { id, name } of newestByClaim.values()) { + const full = await storage.getFile(id); if (!full?.diskPath) continue; const abs = path.join(process.cwd(), full.diskPath); if (!fs.existsSync(abs)) continue; const buf = fs.readFileSync(abs); - if (!buf.length) continue; - const safeName = (cf.name || "claim.pdf").replace(/[/\\]/g, "_").trim() || "claim.pdf"; - pdfEntries.push({ filename: safeName, data: buf }); + if (buf.length) pdfEntries.push({ filename: name, data: buf }); } } if (pdfEntries.length === 0) { - return res.status(404).json({ error: "No claim PDFs found for the selected columns and date" }); + return res.status(404).json({ error: `No filled claim forms for ${date}` }); } - const zipFilename = `claims_${date}.zip`; + const zipFilename = `filled_claim_forms_${date}.zip`; res.setHeader("Content-Type", "application/zip"); res.setHeader("Content-Disposition", `attachment; filename="${zipFilename}"`); @@ -1123,7 +1117,7 @@ router.get( let archiveError: Error | null = null; archive.on("error", (err) => { archiveError = err; - console.error("[batch-pdf] archiver error:", err); + console.error("[filled-forms] archiver error:", err); if (!res.headersSent) { res.status(500).json({ error: `ZIP creation failed: ${err.message}` }); } @@ -1147,9 +1141,9 @@ router.get( await archive.finalize(); } } catch (err: any) { - console.error("[batch-pdf] error:", err); + console.error("[filled-forms] error:", err); if (!res.headersSent) { - return res.status(500).json({ error: err?.message ?? "Batch PDF download failed" }); + return res.status(500).json({ error: err?.message ?? "Filled claim form download failed" }); } } } diff --git a/apps/Frontend/src/pages/appointments-page.tsx b/apps/Frontend/src/pages/appointments-page.tsx index 1cdbce48..a167e746 100755 --- a/apps/Frontend/src/pages/appointments-page.tsx +++ b/apps/Frontend/src/pages/appointments-page.tsx @@ -151,6 +151,8 @@ function resolveAppointmentBadgeStatus(apt: ScheduledAppointment): PatientStatus const APPOINTMENT_CONTEXT_MENU_ID = "appointment-context-menu"; // 🔑 exported base key +const SCHEDULE_DATE_KEY = "schedule_selected_date"; + export const QK_APPOINTMENTS_BASE = ["appointments", "day"] as const; // helper (optional) – mirrors the query key structure export const qkAppointmentsDay = (date: string) => @@ -164,7 +166,19 @@ export default function AppointmentsPage() { const [editingAppointment, setEditingAppointment] = useState< Appointment | undefined >(undefined); - const [selectedDate, setSelectedDate] = useState(startOfToday()); + // Remember the working date for this tab, so returning from the claims page keeps it + const [selectedDate, setSelectedDate] = useState(() => { + try { + const saved = sessionStorage.getItem(SCHEDULE_DATE_KEY); + if (saved) return parseLocalDate(saved); + } catch {} + return startOfToday(); + }); + useEffect(() => { + try { + sessionStorage.setItem(SCHEDULE_DATE_KEY, formatLocalDate(selectedDate)); + } catch {} + }, [selectedDate]); const [location] = useLocation(); const [confirmDeleteState, setConfirmDeleteState] = useState<{ open: boolean; @@ -234,7 +248,6 @@ export default function AppointmentsPage() { const [selectedRescheduleColumns, setSelectedRescheduleColumns] = useState>(new Set()); const [isSendingReschedule, setIsSendingReschedule] = useState(false); const [rescheduleAiFollowUp, setRescheduleAiFollowUp] = useState(true); - const [selectedDownloadPdfColumns, setSelectedDownloadPdfColumns] = useState>(new Set()); const [isDownloadingClaimPdfs, setIsDownloadingClaimPdfs] = useState(false); const [columnLabels, setColumnLabels] = useState>({}); const [editingLabelStaffId, setEditingLabelStaffId] = useState(null); @@ -282,15 +295,6 @@ export default function AppointmentsPage() { }); }; - const toggleDownloadPdfColumn = (staffId: number) => { - setSelectedDownloadPdfColumns((prev) => { - const next = new Set(prev); - if (next.has(staffId)) next.delete(staffId); - else next.add(staffId); - return next; - }); - }; - const [, setLocation] = useLocation(); // Select Procedures modal state (opened inline, no navigation) @@ -1499,13 +1503,12 @@ export default function AppointmentsPage() { }; const handleDownloadClaimPdfs = async () => { - if (!user || selectedDownloadPdfColumns.size === 0) return; - const staffIdsParam = Array.from(selectedDownloadPdfColumns).join(","); + if (!user) return; setIsDownloadingClaimPdfs(true); try { const res = await apiRequest( "GET", - `/api/claims/batch-pdf?date=${formattedSelectedDate}&staffIds=${staffIdsParam}` + `/api/claims/filled-forms?date=${formattedSelectedDate}` ); if (!res.ok) { let errMsg = `Server error ${res.status}`; @@ -1517,12 +1520,12 @@ export default function AppointmentsPage() { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `claims_${formattedSelectedDate}.zip`; + a.download = `filled_claim_forms_${formattedSelectedDate}.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - toast({ title: "Download started", description: `Claim PDFs for ${formattedSelectedDate} saved to your Downloads folder.` }); + toast({ title: "Download started", description: `Filled claim forms for ${formattedSelectedDate} saved to your Downloads folder.` }); } catch (err: any) { toast({ title: "Download failed", description: err?.message ?? String(err), variant: "destructive" }); } finally { @@ -1692,6 +1695,9 @@ export default function AppointmentsPage() { serviceDate, autoSubmit: true, })); + // BCBS is a paper form filled and saved in the background — come straight back here + if (/^bcbs/i.test(siteKey)) sessionStorage.setItem("ai_claim_return_to_schedule", "1"); + else sessionStorage.removeItem("ai_claim_return_to_schedule"); setAiClaimModalOpen(false); setLocation(`/claims?appointmentId=${appointmentId}`); }; @@ -2083,11 +2089,11 @@ export default function AppointmentsPage() { - {/* Download Claim PDF for Column section */} + {/* Download Filled Claim Form: ADA forms (e.g. BCBS) for the selected date */}
- {staffMembers.map((staff, index) => ( - - ))}
diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index 373af050..c102f951 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -1176,6 +1176,13 @@ export default function ClaimsPage() { clearUrlParams(["newPatient", "appointmentId"]); + // A BCBS claim started from the schedule returns there even when the queue is done + if (sessionStorage.getItem("ai_claim_return_to_schedule")) { + sessionStorage.removeItem("ai_claim_return_to_schedule"); + setWouterLocation("/appointments"); + return; + } + // If an AI claim queue is pending resume, go straight back to appointments try { const raw = sessionStorage.getItem("ai_claim_queue");