diff --git a/apps/Backend/src/routes/export-payments-reports.ts b/apps/Backend/src/routes/export-payments-reports.ts new file mode 100644 index 0000000..17c855e --- /dev/null +++ b/apps/Backend/src/routes/export-payments-reports.ts @@ -0,0 +1,99 @@ +import { Router } from "express"; +import type { Request, Response } from "express"; +import { storage } from "../storage"; +const router = Router(); + +/** + * GET /api/reports/export + * query: + * - type = patients_with_balance | collections_by_doctor + * - from, to = optional ISO date strings (YYYY-MM-DD) + * - staffId = required for collections_by_doctor + * - format = csv (we expect csv; if missing default to csv) + */ +function escapeCsvCell(v: any) { + if (v === null || v === undefined) return ""; + const s = String(v).replace(/\r?\n/g, " "); + if (s.includes('"') || s.includes(",") || s.includes("\n")) { + return `"${s.replace(/"/g, '""')}"`; + } + return s; +} + +router.get("/export", async (req: Request, res: Response): Promise => { + try { + const type = String(req.query.type || ""); + const from = req.query.from ? new Date(String(req.query.from)) : undefined; + const to = req.query.to ? new Date(String(req.query.to)) : undefined; + const staffId = req.query.staffId ? Number(req.query.staffId) : undefined; + const format = String(req.query.format || "csv").toLowerCase(); + + if (format !== "csv") { + return res.status(400).json({ message: "Only CSV export is supported" }); + } + + let patientsSummary: any[] = []; + + if (type === "patients_with_balance") { + patientsSummary = await storage.fetchAllPatientsWithBalances(from, to); + } else if (type === "collections_by_doctor") { + if (!staffId || !Number.isFinite(staffId) || staffId <= 0) { + return res.status(400).json({ message: "Missing or invalid staffId for collections_by_doctor" }); + } + patientsSummary = await storage.fetchAllPatientsForDoctor(staffId, from, to); + } else { + return res.status(400).json({ message: "Unsupported report type" }); + } + + const patientsWithFinancials = await storage.buildExportRowsForPatients(patientsSummary, 5000); + + // Build CSV - flattened rows + // columns: patientId, patientName, currentBalance, type, date, procedureCode, billed, paid, adjusted, totalDue, status + const header = [ + "patientId", + "patientName", + "currentBalance", + "type", + "date", + "procedureCode", + "billed", + "paid", + "adjusted", + "totalDue", + "status", + ]; + + const lines = [header.join(",")]; + + for (const p of patientsWithFinancials) { + const name = `${p.firstName ?? ""} ${p.lastName ?? ""}`.trim(); + for (const fr of p.financialRows) { + lines.push( + [ + escapeCsvCell(p.patientId), + escapeCsvCell(name), + (Number(p.currentBalance ?? 0)).toFixed(2), + escapeCsvCell(fr.type), + escapeCsvCell(fr.date), + escapeCsvCell(fr.procedureCode), + (Number(fr.billed ?? 0)).toFixed(2), + (Number(fr.paid ?? 0)).toFixed(2), + (Number(fr.adjusted ?? 0)).toFixed(2), + (Number(fr.totalDue ?? 0)).toFixed(2), + escapeCsvCell(fr.status), + ].join(",") + ); + } + } + + const fname = `report-${type}-${new Date().toISOString().slice(0, 10)}.csv`; + res.setHeader("Content-Type", "text/csv; charset=utf-8"); + res.setHeader("Content-Disposition", `attachment; filename="${fname}"`); + return res.send(lines.join("\n")); + } catch (err: any) { + console.error("[/api/reports/export] error:", err?.message ?? err, err?.stack); + return res.status(500).json({ message: "Export error" }); + } +}); + +export default router; diff --git a/apps/Backend/src/routes/index.ts b/apps/Backend/src/routes/index.ts index 43be9e3..3b0ecbf 100644 --- a/apps/Backend/src/routes/index.ts +++ b/apps/Backend/src/routes/index.ts @@ -14,6 +14,7 @@ import notificationsRoutes from "./notifications"; import paymentOcrRoutes from "./paymentOcrExtraction"; import cloudStorageRoutes from "./cloud-storage"; import paymentsReportsRoutes from "./payments-reports"; +import exportPaymentsReportsRoutes from "./export-payments-reports"; const router = Router(); @@ -32,5 +33,6 @@ router.use("/notifications", notificationsRoutes); router.use("/payment-ocr", paymentOcrRoutes); router.use("/cloud-storage", cloudStorageRoutes); router.use("/payments-reports", paymentsReportsRoutes); +router.use("/export-payments-reports", exportPaymentsReportsRoutes); export default router; diff --git a/apps/Backend/src/storage/export-payments-reports-storage.ts b/apps/Backend/src/storage/export-payments-reports-storage.ts new file mode 100644 index 0000000..ea6ad53 --- /dev/null +++ b/apps/Backend/src/storage/export-payments-reports-storage.ts @@ -0,0 +1,142 @@ +import { storage } from "../storage"; +import { getPatientFinancialRowsFn } from "./patients-storage"; + +type PatientSummaryRow = { + patientId: number; + firstName: string | null; + lastName: string | null; + currentBalance: number; +}; + +/** + * Page through storage.getPatientsWithBalances to return the full list (not paginated). + * Uses the same filters (from/to) as the existing queries. + */ +export async function fetchAllPatientsWithBalances( + from?: Date | null, + to?: Date | null, + pageSize = 500 +): Promise { + const all: PatientSummaryRow[] = []; + let cursor: string | null = null; + while (true) { + const page = await storage.getPatientsWithBalances( + pageSize, + cursor, + from, + to + ); + if (!page) break; + if (Array.isArray(page.balances) && page.balances.length) { + for (const b of page.balances) { + all.push({ + patientId: Number(b.patientId), + firstName: b.firstName ?? null, + lastName: b.lastName ?? null, + currentBalance: Number(b.currentBalance ?? 0), + }); + } + } + if (!page.hasMore || !page.nextCursor) break; + cursor = page.nextCursor; + } + return all; +} + +/** + * Page through storage.getBalancesAndSummaryByDoctor to return full patient list for the staff. + */ +export async function fetchAllPatientsForDoctor( + staffId: number, + from?: Date | null, + to?: Date | null, + pageSize = 500 +): Promise { + const all: PatientSummaryRow[] = []; + let cursor: string | null = null; + while (true) { + const page = await storage.getBalancesAndSummaryByDoctor( + staffId, + pageSize, + cursor, + from, + to + ); + if (!page) break; + if (Array.isArray(page.balances) && page.balances.length) { + for (const b of page.balances) { + all.push({ + patientId: Number(b.patientId), + firstName: b.firstName ?? null, + lastName: b.lastName ?? null, + currentBalance: Number(b.currentBalance ?? 0), + }); + } + } + if (!page.hasMore || !page.nextCursor) break; + cursor = page.nextCursor; + } + return all; +} + +/** + * For each patient, call the existing function to fetch full financial rows. + * This uses your existing getPatientFinancialRowsFn which returns { rows, totalCount }. + * + * The function returns an array of: + * { patientId, firstName, lastName, currentBalance, financialRows: Array<{ type, date, procedureCode, billed, paid, adjusted, totalDue, status }> } + */ +export async function buildExportRowsForPatients( + patients: PatientSummaryRow[], + perPatientLimit = 5000 +) { + const out: Array = []; + + for (const p of patients) { + const patientId = Number(p.patientId); + const { rows } = await getPatientFinancialRowsFn( + patientId, + perPatientLimit, + 0 + ); // returns rows array similarly to your earlier code + + const frs = rows.flatMap((r: any) => { + const svc = r.service_lines ?? []; + if (svc.length > 0) { + return svc.map((sl: any) => ({ + type: r.type, + date: r.date ? new Date(r.date).toLocaleDateString() : "", + procedureCode: String(sl.procedureCode ?? "-"), + billed: Number(sl.totalBilled ?? 0), + paid: Number(sl.totalPaid ?? 0), + adjusted: Number(sl.totalAdjusted ?? 0), + totalDue: Number(sl.totalDue ?? 0), + status: sl.status ?? r.status ?? "", + })); + } else { + return [ + { + type: r.type, + date: r.date ? new Date(r.date).toLocaleDateString() : "", + procedureCode: "-", + billed: Number(r.total_billed ?? r.totalBilled ?? 0), + paid: Number(r.total_paid ?? r.totalPaid ?? 0), + adjusted: Number(r.total_adjusted ?? r.totalAdjusted ?? 0), + totalDue: Number(r.total_due ?? r.totalDue ?? 0), + status: r.status ?? "", + }, + ]; + } + }); + + out.push({ + patientId, + firstName: p.firstName, + lastName: p.lastName, + currentBalance: Number(p.currentBalance ?? 0), + financialRows: frs, + }); + } + + return out; +} diff --git a/apps/Backend/src/storage/index.ts b/apps/Backend/src/storage/index.ts index 1886301..5b5674a 100644 --- a/apps/Backend/src/storage/index.ts +++ b/apps/Backend/src/storage/index.ts @@ -12,7 +12,7 @@ import { databaseBackupStorage } from './database-backup-storage'; import { notificationsStorage } from './notifications-storage'; import { cloudStorageStorage } from './cloudStorage-storage'; import { paymentsReportsStorage } from './payments-reports-storage'; - +import * as exportPaymentsReportsStorage from "./export-payments-reports-storage"; export const storage = { @@ -28,6 +28,7 @@ export const storage = { ...notificationsStorage, ...cloudStorageStorage, ...paymentsReportsStorage, + ...exportPaymentsReportsStorage, }; diff --git a/apps/Backend/src/storage/payments-reports-storage.ts b/apps/Backend/src/storage/payments-reports-storage.ts index 2ba2c41..e1a9530 100644 --- a/apps/Backend/src/storage/payments-reports-storage.ts +++ b/apps/Backend/src/storage/payments-reports-storage.ts @@ -68,7 +68,6 @@ function isoStartOfNextDayLiteral(d?: Date | null): string | null { function encodeCursor(obj: { staffId?: number; lastPaymentDate: string | null; - lastPatientCreatedAt: string; // ISO string lastPatientId: number; }) { return Buffer.from(JSON.stringify(obj)).toString("base64"); @@ -77,7 +76,6 @@ function encodeCursor(obj: { function decodeCursor(token?: string | null): { staffId?: number; // optional because older cursors might not include it lastPaymentDate: string | null; - lastPatientCreatedAt: string; lastPatientId: number; } | null { if (!token) return null; @@ -86,7 +84,6 @@ function decodeCursor(token?: string | null): { if ( typeof parsed === "object" && "lastPaymentDate" in parsed && - "lastPatientCreatedAt" in parsed && "lastPatientId" in parsed ) { return { @@ -96,7 +93,6 @@ function decodeCursor(token?: string | null): { (parsed as any).lastPaymentDate === null ? null : String((parsed as any).lastPaymentDate), - lastPatientCreatedAt: String((parsed as any).lastPatientCreatedAt), lastPatientId: Number((parsed as any).lastPatientId), }; } @@ -331,7 +327,6 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { current_balance: string; last_payment_date: Date | null; last_appointment_date: Date | null; - patient_created_at: Date | null; // we select patient.createdAt for cursor tie-breaker }; const safeLimit = Math.max(1, Math.min(200, Number(limit) || 25)); @@ -376,7 +371,6 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { const lp = cursor.lastPaymentDate ? `'${cursor.lastPaymentDate}'` : "NULL"; - const lc = `'${cursor.lastPatientCreatedAt}'`; const id = Number(cursor.lastPatientId); // We handle NULL last_payment_date ordering: since we use "NULLS LAST" in ORDER BY, @@ -385,19 +379,12 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { // This predicate tries to cover both cases. keysetPredicate = ` AND ( - -- case: both sides have non-null last_payment_date (pm.last_payment_date IS NOT NULL AND ${lp} IS NOT NULL AND ( - pm.last_payment_date < ${lp} - OR (pm.last_payment_date = ${lp} AND p."createdAt" < ${lc}) - OR (pm.last_payment_date = ${lp} AND p."createdAt" = ${lc} AND p.id < ${id}) + pm.last_payment_date < ${lp} + OR (pm.last_payment_date = ${lp} AND p.id < ${id}) )) - -- case: cursor lastPaymentDate IS NULL -> we need rows with last_payment_date IS NULL but earlier createdAt/id - OR (pm.last_payment_date IS NULL AND ${lp} IS NULL AND ( - p."createdAt" < ${lc} - OR (p."createdAt" = ${lc} AND p.id < ${id}) - )) - -- case: cursor had non-null lastPaymentDate but pm.last_payment_date IS NULL: - -- since NULLS LAST, pm.last_payment_date IS NULL are after non-null dates, so they are NOT < cursor -> excluded + OR (pm.last_payment_date IS NULL AND ${lp} IS NOT NULL) + OR (pm.last_payment_date IS NULL AND ${lp} IS NULL AND p.id < ${id}) ) `; } @@ -412,8 +399,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { COALESCE(pm.total_adjusted,0)::numeric(12,2) AS total_adjusted, (COALESCE(pm.total_charges,0) - COALESCE(pm.total_paid,0) - COALESCE(pm.total_adjusted,0))::numeric(12,2) AS current_balance, pm.last_payment_date, - apt.last_appointment_date, - p."createdAt" AS patient_created_at + apt.last_appointment_date FROM "Patient" p LEFT JOIN ${pmSubquery} ON pm.patient_id = p.id LEFT JOIN ( @@ -424,7 +410,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { WHERE (COALESCE(pm.total_charges,0) - COALESCE(pm.total_paid,0) - COALESCE(pm.total_adjusted,0)) > 0 `; - const orderBy = `ORDER BY pm.last_payment_date DESC NULLS LAST, p."createdAt" DESC, p.id DESC`; + const orderBy = `ORDER BY pm.last_payment_date DESC NULLS LAST, p.id DESC`; const limitClause = `LIMIT ${safeLimit}`; const query = ` @@ -453,14 +439,9 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { ? new Date(last.last_payment_date).toISOString() : null; - const lastPatientCreatedAtIso = last.patient_created_at - ? new Date(last.patient_created_at).toISOString() - : new Date().toISOString(); - if (rows.length === safeLimit) { nextCursor = encodeCursor({ lastPaymentDate: lastPaymentDateIso, - lastPatientCreatedAt: lastPatientCreatedAtIso, lastPatientId: Number(last.patient_id), }); } else { @@ -487,9 +468,6 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { lastAppointmentDate: r.last_appointment_date ? new Date(r.last_appointment_date).toISOString() : null, - patientCreatedAt: r.patient_created_at - ? new Date(r.patient_created_at).toISOString() - : null, })); // totalCount: count of patients with positive balance within same payment date filter @@ -536,15 +514,13 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { const safeLimit = Math.max(1, Math.min(200, Number(limit) || 25)); const decoded = decodeCursor(cursorToken); - // Accept older cursors that didn't include staffId; if cursor has staffId and it doesn't match, ignore. + // Do NOT accept cursors without staffId — they may belong to another listing. const effectiveCursor = decoded && typeof decoded.staffId === "number" && decoded.staffId === Number(staffId) ? decoded - : decoded && typeof decoded.staffId === "undefined" - ? decoded - : null; + : null; const hasFrom = from !== undefined && from !== null; const hasTo = to !== undefined && to !== null; @@ -565,27 +541,38 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { // Keyset predicate must use columns present in the 'patients' CTE rows (alias p). // We'll compare p.last_payment_date, p.patient_created_at and p.id + let pageKeysetPredicate = ""; if (effectiveCursor) { const lp = effectiveCursor.lastPaymentDate ? `'${effectiveCursor.lastPaymentDate}'` : "NULL"; - const lc = `'${effectiveCursor.lastPatientCreatedAt}'`; const id = Number(effectiveCursor.lastPatientId); pageKeysetPredicate = `AND ( - (p.last_payment_date IS NOT NULL AND ${lp} IS NOT NULL AND ( - p.last_payment_date < ${lp} - OR (p.last_payment_date = ${lp} AND p.patient_created_at < ${lc}) - OR (p.last_payment_date = ${lp} AND p.patient_created_at = ${lc} AND p.id < ${id}) - )) - OR (p.last_payment_date IS NULL AND ${lp} IS NULL AND ( - p.patient_created_at < ${lc} - OR (p.patient_created_at = ${lc} AND p.id < ${id}) - )) - )`; + ( ${lp} IS NOT NULL AND ( + (p.last_payment_date IS NOT NULL AND p.last_payment_date < ${lp}) + OR (p.last_payment_date IS NOT NULL AND p.last_payment_date = ${lp} AND p.id < ${id}) + ) + ) + OR + ( ${lp} IS NULL AND ( + p.last_payment_date IS NOT NULL + OR (p.last_payment_date IS NULL AND p.id < ${id}) + ) + ) + )`; } + console.debug( + "[getBalancesAndSummaryByDoctor] decodedCursor:", + effectiveCursor + ); + console.debug( + "[getBalancesAndSummaryByDoctor] pageKeysetPredicate:", + pageKeysetPredicate + ); + // When a time window is provided, we want the patient rows to be restricted to patients who have // payments in that window (and those payments must be linked to claims with this staffId). // When no time-window provided, we want all patients who have appointments with this staff. @@ -637,8 +624,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { COALESCE(pa.total_adjusted, 0)::numeric(14,2) AS total_adjusted, (COALESCE(pa.total_charges,0) - COALESCE(pa.total_paid,0) - COALESCE(pa.total_adjusted,0))::numeric(14,2) AS current_balance, pa.last_payment_date, - la.last_appointment_date, - p."createdAt" AS patient_created_at + la.last_appointment_date FROM "Patient" p INNER JOIN staff_patients sp ON sp.patient_id = p.id ${paymentsJoinForPatients} @@ -657,12 +643,11 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { p.total_adjusted::text AS "totalAdjusted", p.current_balance::text AS "currentBalance", p.last_payment_date AS "lastPaymentDate", - p.last_appointment_date AS "lastAppointmentDate", - p.patient_created_at AS "patientCreatedAt" + p.last_appointment_date AS "lastAppointmentDate" FROM patients p WHERE 1=1 ${pageKeysetPredicate} - ORDER BY p.last_payment_date DESC NULLS LAST, p.patient_created_at DESC, p.id DESC + ORDER BY p.last_payment_date DESC NULLS LAST, p.id DESC LIMIT ${safeLimit} ) t) AS balances_json, @@ -728,9 +713,6 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { lastAppointmentDate: r.lastAppointmentDate ? new Date(r.lastAppointmentDate).toISOString() : null, - patientCreatedAt: r.patientCreatedAt - ? new Date(r.patientCreatedAt).toISOString() - : null, })); const hasMore = balances.length === safeLimit; @@ -741,8 +723,6 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { nextCursor = encodeCursor({ staffId: Number(staffId), lastPaymentDate: last.lastPaymentDate, - lastPatientCreatedAt: - last.patientCreatedAt ?? new Date().toISOString(), lastPatientId: Number(last.patientId), }); } diff --git a/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx b/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx index 30ada92..66ef9a6 100644 --- a/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx +++ b/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx @@ -12,6 +12,7 @@ import { SelectValue, } from "@/components/ui/select"; import { DoctorBalancesAndSummary } from "@repo/db/types"; +import ExportReportButton from "./export-button"; type StaffOption = { id: number; name: string }; @@ -259,6 +260,15 @@ export default function CollectionsByDoctorReport({ onNext={handleNext} hasPrev={cursorIndex > 0} hasNext={hasMore} + headerRight={ + + } /> )} diff --git a/apps/Frontend/src/components/reports/export-button.tsx b/apps/Frontend/src/components/reports/export-button.tsx new file mode 100644 index 0000000..92ce86b --- /dev/null +++ b/apps/Frontend/src/components/reports/export-button.tsx @@ -0,0 +1,71 @@ +import React, { useState } from "react"; +import { apiRequest } from "@/lib/queryClient"; + +export default function ExportReportButton({ + reportType, + from, + to, + staffId, + className, + labelCsv = "Download CSV", +}: { + reportType: string; // e.g. "collections_by_doctor" or "patients_with_balance" + from?: string; + to?: string; + staffId?: number | string | null; + className?: string; + labelCsv?: string; +}) { + const [loading, setLoading] = useState(false); + + async function downloadCsv() { + setLoading(true); + try { + const params = new URLSearchParams(); + params.set("type", reportType); + if (from) params.set("from", from); + if (to) params.set("to", to); + if (staffId) params.set("staffId", String(staffId)); + params.set("format", "csv"); // server expects format=csv + + const url = `/api/export-payments-reports/export?${params.toString()}`; + + // Use apiRequest for consistent auth headers/cookies + const res = await apiRequest("GET", url); + if (!res.ok) { + const body = await res.text().catch(() => "Export failed"); + throw new Error(body || "Export failed"); + } + + const blob = await res.blob(); + const href = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = href; + const safeFrom = from || "all"; + const safeTo = to || "all"; + a.download = `${reportType}_${safeFrom}_${safeTo}.csv`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(href); + } catch (err: any) { + console.error("Export CSV failed", err); + alert("Export failed: " + (err?.message ?? "unknown error")); + } finally { + setLoading(false); + } + } + + return ( +
+ +
+ ); +} diff --git a/apps/Frontend/src/components/reports/patients-balances-list.tsx b/apps/Frontend/src/components/reports/patients-balances-list.tsx index 669c283..b914689 100644 --- a/apps/Frontend/src/components/reports/patients-balances-list.tsx +++ b/apps/Frontend/src/components/reports/patients-balances-list.tsx @@ -23,6 +23,7 @@ export default function PatientsBalancesList({ onNext, hasPrev, hasNext, + headerRight, // optional UI node to render in header }: { rows: GenericRow[]; reportType?: string | null; @@ -37,6 +38,7 @@ export default function PatientsBalancesList({ onNext: () => void; hasPrev: boolean; hasNext: boolean; + headerRight?: React.ReactNode; }) { const fmt = (v: number) => new Intl.NumberFormat("en-US", { @@ -66,6 +68,9 @@ export default function PatientsBalancesList({

{reportTypeTitle(reportType)}

+ + {/* headerRight rendered here (if provided) */} +
{headerRight ?? null}
diff --git a/apps/Frontend/src/components/reports/patients-with-balance-report.tsx b/apps/Frontend/src/components/reports/patients-with-balance-report.tsx index 796cc82..d2215fe 100644 --- a/apps/Frontend/src/components/reports/patients-with-balance-report.tsx +++ b/apps/Frontend/src/components/reports/patients-with-balance-report.tsx @@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@/lib/queryClient"; import type { PatientBalanceRow } from "@repo/db/types"; import PatientsBalancesList from "./patients-balances-list"; +import ExportReportButton from "./export-button"; type Resp = { balances: PatientBalanceRow[]; @@ -117,6 +118,14 @@ export default function PatientsWithBalanceReport({ onNext={handleNext} hasPrev={cursorIndex > 0} hasNext={hasMore} + headerRight={ + + } />
diff --git a/apps/Frontend/src/pages/reports-page.tsx b/apps/Frontend/src/pages/reports-page.tsx index c8795b2..f66b129 100644 --- a/apps/Frontend/src/pages/reports-page.tsx +++ b/apps/Frontend/src/pages/reports-page.tsx @@ -1,11 +1,9 @@ import React, { useState } from "react"; -import { Download } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import ReportConfig from "@/components/reports/report-config"; import PatientsWithBalanceReport from "@/components/reports/patients-with-balance-report"; import CollectionsByDoctorReport from "@/components/reports/collections-by-doctor-report"; import SummaryCards from "@/components/reports/summary-cards"; -import { Button } from "@/components/ui/button"; type ReportType = | "patients_with_balance" @@ -32,17 +30,6 @@ export default function ReportPage() { const [selectedReportType, setSelectedReportType] = useState( "patients_with_balance" ); - const [isGenerating, setIsGenerating] = useState(false); - - const generateReport = async () => { - setIsGenerating(true); - try { - // placeholder: implement export per-report endpoint - await new Promise((r) => setTimeout(r, 900)); - } finally { - setIsGenerating(false); - } - }; if (!user) { return ( @@ -62,16 +49,6 @@ export default function ReportPage() { Generate comprehensive financial reports for your practice

- - {/* Export Button (Top Right) */} -
diff --git a/packages/db/types/payments-reports-types.ts b/packages/db/types/payments-reports-types.ts index baf957b..13d961c 100644 --- a/packages/db/types/payments-reports-types.ts +++ b/packages/db/types/payments-reports-types.ts @@ -8,7 +8,6 @@ export interface PatientBalanceRow { currentBalance: number; lastPaymentDate: string | null; lastAppointmentDate: string | null; - patientCreatedAt?: string | null; } export interface GetPatientBalancesResult {