diff --git a/apps/Backend/src/storage/payments-reports-storage.ts b/apps/Backend/src/storage/payments-reports-storage.ts index 0fff4ef..2ba2c41 100644 --- a/apps/Backend/src/storage/payments-reports-storage.ts +++ b/apps/Backend/src/storage/payments-reports-storage.ts @@ -1,37 +1,9 @@ import { prisma } from "@repo/db/client"; - -export interface PatientBalanceRow { - patientId: number; - firstName: string | null; - lastName: string | null; - totalCharges: number; - totalPayments: number; - totalAdjusted: number; - currentBalance: number; - lastPaymentDate: string | null; - lastAppointmentDate: string | null; - patientCreatedAt?: string | null; -} - -export interface GetPatientBalancesResult { - balances: PatientBalanceRow[]; - totalCount: number; - nextCursor: string | null; - hasMore: boolean; -} - -export interface DoctorBalancesAndSummary { - balances: PatientBalanceRow[]; - totalCount: number; - nextCursor: string | null; - hasMore: boolean; - summary: { - totalPatients: number; - totalOutstanding: number; - totalCollected: number; - patientsWithBalance: number; - }; -} +import { + DoctorBalancesAndSummary, + GetPatientBalancesResult, + PatientBalanceRow, +} from "../../../../packages/db/types/payments-reports-types"; export interface IPaymentsReportsStorage { // summary now returns an extra field patientsWithBalance @@ -75,11 +47,21 @@ export interface IPaymentsReportsStorage { ): Promise; } -/** Helper: format Date -> SQL literal 'YYYY-MM-DDTHH:mm:ss.sssZ' or null */ -function fmtDateLiteral(d?: Date | null): string | null { +/** Return ISO literal for inclusive start-of-day (UTC midnight) */ +function isoStartOfDayLiteral(d?: Date | null): string | null { if (!d) return null; - const iso = new Date(d).toISOString(); - return `'${iso}'`; + const dt = new Date(d); + dt.setUTCHours(0, 0, 0, 0); + return `'${dt.toISOString()}'`; +} + +/** Return ISO literal for exclusive next-day start (UTC midnight of the next day) */ +function isoStartOfNextDayLiteral(d?: Date | null): string | null { + if (!d) return null; + const dt = new Date(d); + dt.setUTCHours(0, 0, 0, 0); + dt.setUTCDate(dt.getUTCDate() + 1); + return `'${dt.toISOString()}'`; } /** Cursor helpers — base64(JSON) */ @@ -129,8 +111,10 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { try { const hasFrom = from !== undefined && from !== null; const hasTo = to !== undefined && to !== null; - const fromLit = fmtDateLiteral(from); - const toLit = fmtDateLiteral(to); + + // Use inclusive start-of-day for 'from' and exclusive start-of-next-day for 'to' + const fromStart = isoStartOfDayLiteral(from); // 'YYYY-MM-DDT00:00:00.000Z' + const toNextStart = isoStartOfNextDayLiteral(to); // 'YYYY-MM-DDT00:00:00.000Z' of next day // totalPatients: distinct patients who had payments in the date range let patientsCountSql = ""; @@ -139,7 +123,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SELECT COUNT(*)::int AS cnt FROM ( SELECT pay."patientId" AS patient_id FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} AND pay."createdAt" <= ${toLit} + WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) t `; @@ -148,7 +132,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SELECT COUNT(*)::int AS cnt FROM ( SELECT pay."patientId" AS patient_id FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} + WHERE pay."createdAt" >= ${fromStart} GROUP BY pay."patientId" ) t `; @@ -157,7 +141,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SELECT COUNT(*)::int AS cnt FROM ( SELECT pay."patientId" AS patient_id FROM "Payment" pay - WHERE pay."createdAt" <= ${toLit} + WHERE pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) t `; @@ -182,7 +166,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} AND pay."createdAt" <= ${toLit} + WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) pm `; @@ -197,7 +181,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} + WHERE pay."createdAt" >= ${fromStart} GROUP BY pay."patientId" ) pm `; @@ -212,7 +196,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" <= ${toLit} + WHERE pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) pm `; @@ -239,11 +223,11 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { // totalCollected: sum(totalPaid) in the range let collSql = ""; if (hasFrom && hasTo) { - collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromLit} AND "createdAt" <= ${toLit}`; + collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromStart} AND "createdAt" <= ${toNextStart}`; } else if (hasFrom) { - collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromLit}`; + collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromStart}`; } else if (hasTo) { - collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" <= ${toLit}`; + collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" <= ${toNextStart}`; } else { collSql = `SELECT COALESCE(SUM("totalPaid"),0)::numeric(14,2) AS collected FROM "Payment"`; } @@ -262,7 +246,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} AND pay."createdAt" <= ${toLit} + WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) t WHERE (COALESCE(t.total_charges,0) - COALESCE(t.total_paid,0) - COALESCE(t.total_adjusted,0)) > 0 @@ -275,7 +259,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" >= ${fromLit} + WHERE pay."createdAt" >= ${fromStart} GROUP BY pay."patientId" ) t WHERE (COALESCE(t.total_charges,0) - COALESCE(t.total_paid,0) - COALESCE(t.total_adjusted,0)) > 0 @@ -288,7 +272,7 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { SUM(pay."totalPaid")::numeric(14,2) AS total_paid, SUM(pay."totalAdjusted")::numeric(14,2) AS total_adjusted FROM "Payment" pay - WHERE pay."createdAt" <= ${toLit} + WHERE pay."createdAt" <= ${toNextStart} GROUP BY pay."patientId" ) t WHERE (COALESCE(t.total_charges,0) - COALESCE(t.total_paid,0) - COALESCE(t.total_adjusted,0)) > 0 @@ -355,17 +339,19 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { const hasFrom = from !== undefined && from !== null; const hasTo = to !== undefined && to !== null; - const fromLit = fmtDateLiteral(from); - const toLit = fmtDateLiteral(to); + + // Use inclusive start-of-day for 'from' and exclusive start-of-next-day for 'to' + const fromStart = isoStartOfDayLiteral(from); // 'YYYY-MM-DDT00:00:00.000Z' + const toNextStart = isoStartOfNextDayLiteral(to); // 'YYYY-MM-DDT00:00:00.000Z' of next day // Build payment subquery (aggregated payments by patient, filtered by createdAt if provided) const paymentWhereClause = hasFrom && hasTo - ? `WHERE pay."createdAt" >= ${fromLit} AND pay."createdAt" <= ${toLit}` + ? `WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}` : hasFrom - ? `WHERE pay."createdAt" >= ${fromLit}` + ? `WHERE pay."createdAt" >= ${fromStart}` : hasTo - ? `WHERE pay."createdAt" <= ${toLit}` + ? `WHERE pay."createdAt" <= ${toNextStart}` : ""; const pmSubquery = ` @@ -562,17 +548,19 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = { const hasFrom = from !== undefined && from !== null; const hasTo = to !== undefined && to !== null; - const fromLit = fmtDateLiteral(from); - const toLit = fmtDateLiteral(to); + + // Use inclusive start-of-day for 'from' and exclusive start-of-next-day for 'to' + const fromStart = isoStartOfDayLiteral(from); // 'YYYY-MM-DDT00:00:00.000Z' + const toNextStart = isoStartOfNextDayLiteral(to); // 'YYYY-MM-DDT00:00:00.000Z' of next day // Filter payments by createdAt (time window) when provided const paymentTimeFilter = hasFrom && hasTo - ? `AND pay."createdAt" >= ${fromLit} AND pay."createdAt" <= ${toLit}` + ? `AND pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}` : hasFrom - ? `AND pay."createdAt" >= ${fromLit}` + ? `AND pay."createdAt" >= ${fromStart}` : hasTo - ? `AND pay."createdAt" <= ${toLit}` + ? `AND pay."createdAt" <= ${toNextStart}` : ""; // Keyset predicate must use columns present in the 'patients' CTE rows (alias p). 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 d96f9c9..30ada92 100644 --- a/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx +++ b/apps/Frontend/src/components/reports/collections-by-doctor-report.tsx @@ -11,35 +11,10 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { DoctorBalancesAndSummary } from "@repo/db/types"; type StaffOption = { id: number; name: string }; -type BalanceRow = { - patientId: number; - firstName: string | null; - lastName: string | null; - totalCharges: number | string; - totalPaid: number | string; - totalAdjusted: number | string; - currentBalance: number | string; - lastPaymentDate: string | null; - lastAppointmentDate: string | null; - patientCreatedAt?: string | null; -}; - -type CollectionsResp = { - balances: BalanceRow[]; - totalCount?: number; - nextCursor?: string | null; - hasMore?: boolean; - summary?: { - totalPatients?: number; - totalOutstanding?: number; - totalCollected?: number; - patientsWithBalance?: number; - }; -}; - function fmtCurrency(v: number) { return new Intl.NumberFormat("en-US", { style: "currency", @@ -85,7 +60,7 @@ export default function CollectionsByDoctorReport({ isError: isErrorRows, refetch, isFetching, - } = useQuery({ + } = useQuery({ queryKey: [ "collections-by-doctor-rows", staffId, @@ -115,7 +90,6 @@ export default function CollectionsByDoctorReport({ return res.json(); }, enabled: Boolean(staffId), // only load when a doctor is selected - staleTime: 30_000, }); const balances = collectionData?.balances ?? []; @@ -152,7 +126,7 @@ export default function CollectionsByDoctorReport({ // Map server rows to GenericRow const genericRows: GenericRow[] = balances.map((r) => { const totalCharges = Number(r.totalCharges ?? 0); - const totalPayments = Number(r.totalPaid ?? 0); + const totalPayments = Number(r.totalPayments ?? 0); const currentBalance = Number(r.currentBalance ?? 0); const name = `${r.firstName ?? ""} ${r.lastName ?? ""}`.trim() || "Unknown"; @@ -169,7 +143,9 @@ export default function CollectionsByDoctorReport({
- +