feat: Download Filled Claim Form; BCBS AI claim returns to schedule date

- "Download Claim PDF for Column" -> "Download Filled Claim Form": ZIP of the
  ADA forms the app filled (e.g. BCBS) for appointments on the selected
  schedule date, all columns; Selenium-submitted claims are not included
  (GET /api/claims/filled-forms replaces /batch-pdf)
- After a BCBS claim from "Claim for column with AI", go back to the
  schedule even when the queue is finished
- Schedule remembers the selected date for the tab, so returning from the
  claims page keeps the working date instead of jumping to today

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 10:39:01 -04:00
co-authored by Claude Opus 5.5
parent f55e8c114b
commit c3efdc7f8e
3 changed files with 74 additions and 83 deletions
+25 -35
View File
@@ -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<Date>(startOfToday());
// Remember the working date for this tab, so returning from the claims page keeps it
const [selectedDate, setSelectedDate] = useState<Date>(() => {
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<Set<number>>(new Set());
const [isSendingReschedule, setIsSendingReschedule] = useState(false);
const [rescheduleAiFollowUp, setRescheduleAiFollowUp] = useState(true);
const [selectedDownloadPdfColumns, setSelectedDownloadPdfColumns] = useState<Set<number>>(new Set());
const [isDownloadingClaimPdfs, setIsDownloadingClaimPdfs] = useState(false);
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
const [editingLabelStaffId, setEditingLabelStaffId] = useState<string | null>(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() {
</div>
</div>
{/* Download Claim PDF for Column section */}
{/* Download Filled Claim Form: ADA forms (e.g. BCBS) for the selected date */}
<div className="flex items-center gap-2 border rounded-md px-3 py-2 bg-white shadow-sm">
<Button
onClick={() => handleDownloadClaimPdfs()}
disabled={isLoading || isDownloadingClaimPdfs || selectedDownloadPdfColumns.size === 0}
disabled={isLoading || isDownloadingClaimPdfs}
size="sm"
>
{isDownloadingClaimPdfs ? (
@@ -2098,26 +2104,10 @@ export default function AppointmentsPage() {
) : (
<>
<Download className="h-4 w-4 mr-1" />
Download Claim PDF for Column
Download Filled Claim Form
</>
)}
</Button>
{staffMembers.map((staff, index) => (
<label
key={staff.id}
className="flex items-center gap-1 cursor-pointer select-none"
>
<input
type="checkbox"
className="w-4 h-4 rounded border-gray-400 accent-teal-600"
checked={selectedDownloadPdfColumns.has(Number(staff.id))}
onChange={() => toggleDownloadPdfColumn(Number(staff.id))}
/>
<span className="text-sm font-medium">
{columnLabels[String(staff.id)] ?? String.fromCharCode(65 + index)}
</span>
</label>
))}
</div>
</div>
</div>
+7
View File
@@ -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");