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:
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user