Files
DentalManagement09/apps/Frontend/src/components/reports/patients-with-balance-report.tsx
T
GiteadandClaude Sonnet 4.6 7360b1930b feat: add provider column, commission tracking, and report provider filter
- Claims & Payments: save npiProviderId when submitting MH claim; sync between claim and payment on update
- Claims table: add Provider column showing rendering provider name
- Payments table: add Provider column + purple Commissioned badge on status
- Claim edit modal: add Rendering Provider dropdown (defaults to Mary Scannell)
- Payment edit modal: add Rendering Provider dropdown + Commissioned metadata display
- Reports page: add Provider filter dropdown (dynamic from NPI providers settings)
- Reports page: remove Collections by Doctor report type and Select Doctor dropdown
- Commission section: new section in reports page with date range + provider filter, shows eligible paid claims/payments per provider, multi-select checkboxes, Pay Commission modal with print + save, marks payments as commissioned so they are excluded from future cycles
- DB: add CommissionBatch and CommissionBatchItem tables; backfill Payment.npiProviderId from linked claims
- Backend: PATCH /api/payments/:id/provider syncs to linked claim; PUT /api/claims/:id syncs to linked payment; new /api/commissions routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:51:39 -04:00

138 lines
4.0 KiB
TypeScript
Executable File

import React, { useCallback, useEffect, useState } from "react";
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[];
totalCount?: number; // optional
nextCursor?: string | null;
hasMore?: boolean;
};
export default function PatientsWithBalanceReport({
startDate,
endDate,
npiProviderId,
}: {
startDate: string;
endDate: string;
npiProviderId?: number | null;
}) {
const balancesPerPage = 10;
const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);
const [cursorIndex, setCursorIndex] = useState(0);
const currentCursor = cursorStack[cursorIndex] ?? null;
const pageIndex = cursorIndex + 1; // 1-based for UI
const { data, isLoading, isError, refetch } = useQuery<Resp, Error>({
queryKey: [
"/api/payments-reports/patient-balances",
currentCursor,
balancesPerPage,
startDate,
endDate,
npiProviderId ?? "all",
],
queryFn: async () => {
const params = new URLSearchParams();
params.set("limit", String(balancesPerPage));
if (currentCursor) params.set("cursor", currentCursor);
if (startDate) params.set("from", startDate);
if (endDate) params.set("to", endDate);
if (npiProviderId) params.set("npiProviderId", String(npiProviderId));
const res = await apiRequest(
"GET",
`/api/payments-reports/patients-with-balances?${params.toString()}`
);
if (!res.ok) {
const body = await res
.json()
.catch(() => ({ message: "Failed to load patient balances" }));
throw new Error(body.message || "Failed to load patient balances");
}
return res.json();
},
enabled: true,
});
const balances = data?.balances ?? [];
const totalCount = data?.totalCount ?? undefined;
const nextCursor = data?.nextCursor ?? null;
const hasMore = data?.hasMore ?? false;
useEffect(() => {
setCursorStack([null]);
setCursorIndex(0);
refetch();
}, [startDate, endDate, npiProviderId]);
const handleNext = useCallback(() => {
const idx = cursorIndex;
const isLastKnown = idx === cursorStack.length - 1;
if (isLastKnown) {
if (nextCursor) {
setCursorStack((s) => [...s, nextCursor]);
setCursorIndex((i) => i + 1);
}
} else {
setCursorIndex((i) => i + 1);
}
}, [cursorIndex, cursorStack.length, nextCursor]);
const handlePrev = useCallback(() => {
setCursorIndex((i) => Math.max(0, i - 1));
}, []);
const normalized = balances.map((b) => {
const currentBalance = Number(b.currentBalance ?? 0);
const totalCharges = Number(b.totalCharges ?? 0);
const totalPayments =
b.totalPayments != null
? Number(b.totalPayments)
: Number(totalCharges - currentBalance);
return {
id: b.patientId,
name: `${b.firstName ?? "Unknown"} ${b.lastName ?? ""}`.trim(),
currentBalance,
totalCharges,
totalPayments,
};
});
return (
<div>
<div className="mb-4">
<PatientsBalancesList
rows={normalized}
reportType="patients_with_balance"
loading={isLoading}
error={
isError
? "Failed to load patient balances for the selected date range."
: false
}
emptyMessage="No patient balances for the selected date range."
pageIndex={pageIndex}
perPage={balancesPerPage}
total={totalCount}
onPrev={handlePrev}
onNext={handleNext}
hasPrev={cursorIndex > 0}
hasNext={hasMore}
headerRight={
<ExportReportButton
reportType="patients_with_balance"
from={startDate}
to={endDate}
className="mr-2"
/>
}
/>
</div>
</div>
);
}