diff --git a/distribution_fe/app/(dashboard)/payments/analytics/page.tsx b/distribution_fe/app/(dashboard)/payments/analytics/page.tsx new file mode 100644 index 0000000..9ea4884 --- /dev/null +++ b/distribution_fe/app/(dashboard)/payments/analytics/page.tsx @@ -0,0 +1,266 @@ +'use client' + +import { useEffect, useState } from 'react' +import { Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, XAxis, YAxis } from 'recharts' + +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart' +import { Input } from '@/components/ui/input' +import { Skeleton } from '@/components/ui/skeleton' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { paymentReports, getApiError } from '@/lib/api/index' +import type { PaymentAnalyticsResponse } from '@/lib/api/reports' + +import { daysFromToday, fmtCompact, fmtMoney, fmtNum, fmtShortDay } from '@/app/(dashboard)/network/warehouses/_components/dashboard/format' +import { EmptyNote, Metric, Section } from '@/app/(dashboard)/network/warehouses/_components/dashboard/shared' + +const PRESETS = [ + { label: '7 days', days: -6 }, + { label: '30 days', days: -29 }, + { label: '90 days', days: -89 }, + { label: '1 year', days: -364 }, +] + +const flowConfig = { + cash: { label: 'Cash', color: 'var(--chart-1)' }, + cheque: { label: 'Cheque', color: 'var(--chart-2)' }, + credit: { label: 'Credit', color: 'var(--chart-3)' }, +} satisfies ChartConfig + +const METHOD_COLOR: Record = { + Cash: 'var(--chart-1)', + Cheque: 'var(--chart-2)', + Credit: 'var(--chart-3)', +} + +const methodConfig = { + Cash: { label: 'Cash', color: METHOD_COLOR.Cash }, + Cheque: { label: 'Cheque', color: METHOD_COLOR.Cheque }, + Credit: { label: 'Credit', color: METHOD_COLOR.Credit }, +} satisfies ChartConfig + +const balanceConfig = { balance: { label: 'Outstanding', color: 'var(--chart-4)' } } satisfies ChartConfig + +const periodLabel = (p: string) => (p.length === 10 ? fmtShortDay(p) : p) + +export default function PaymentAnalyticsPage() { + const [from, setFrom] = useState(() => daysFromToday(-29)) + const [to, setTo] = useState(() => daysFromToday(0)) + const [data, setData] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!from || !to) return + let cancelled = false + setLoading(true) + setError(null) + paymentReports.analytics({ from, to }) + .then(res => { if (!cancelled) setData(res) }) + .catch(err => { if (!cancelled) setError(getApiError(err)) }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, [from, to]) + + const s = data?.summary + const hasFlow = !!data && data.byMethod.some(m => m.amount > 0) + + return ( +
+
+
+

Payment Analytics

+

Money flow by method, collections and outstanding receivables.

+
+
+ {PRESETS.map(p => ( + + ))} + setFrom(e.target.value)} className="w-40" aria-label="From date" /> + setTo(e.target.value)} className="w-40" aria-label="To date" /> +
+
+ + {error && ( +
{error}
+ )} + + {!error && !data && loading && ( +
+ + +
+ )} + + {data && s && ( +
+
+ + + + 0 ? 'warning' : undefined} /> + 0 ? 'danger' : undefined} /> + +
+ +
+
+
+ {!hasFlow ? ( + No payments in this period. + ) : ( + + + + + + periodLabel(String(l))} formatter={(v) => fmtMoney(Number(v))} />} /> + + + + + + )} +
+
+ +
+ {!hasFlow ? ( + No payments in this period. + ) : ( +
+ + + fmtMoney(Number(v))} />} /> + + {data.byMethod.map(m => )} + + + +
    + {data.byMethod.map(m => ( +
  • + + + {m.method} · {fmtNum(m.count)} + + {fmtMoney(m.amount)} ({m.sharePct}%) +
  • + ))} +
+
+ )} +
+
+ +
+
+ {s.outstanding === 0 ? ( + Nothing outstanding. + ) : ( + + + + + + fmtMoney(Number(v))} />} /> + + + + )} +
+ +
+ {data.topOutstanding.length === 0 ? ( + No outstanding balances. + ) : ( + + + + Store + Invoices + Outstanding + Overdue + + + + {data.topOutstanding.map(r => ( + + {r.storeName} + {fmtNum(r.invoices)} + {fmtMoney(r.balance)} + + {r.overdue > 0 ? {fmtMoney(r.overdue)} : '—'} + + + ))} + +
+ )} +
+
+ +
+
+ {data.byAccount.length === 0 ? ( + No cash payments in this period. + ) : ( + + + + Account + Payments + Amount + + + + {data.byAccount.map(a => ( + + {a.accountName} + {fmtNum(a.count)} + {fmtMoney(a.amount)} + + ))} + +
+ )} +
+ +
+ {data.cheques.length === 0 ? ( + No cheque payments in this period. + ) : ( + + + + Status + Cheques + Amount + + + + {data.cheques.map(c => ( + + {c.status} + {fmtNum(c.count)} + {fmtMoney(c.amount)} + + ))} + +
+ )} +
+
+
+ )} +
+ ) +} diff --git a/distribution_fe/config/nav.ts b/distribution_fe/config/nav.ts index 98dcabf..54cc6ab 100644 --- a/distribution_fe/config/nav.ts +++ b/distribution_fe/config/nav.ts @@ -65,7 +65,6 @@ export const NAV_GROUPS: NavGroup[] = [ items: [ { title: 'Shop POs', href: '/sales', icon: ClipboardListIcon, description: 'Purchase orders raised by sales agents on behalf of stores', permission: 'orders.view' }, { title: 'Invoices', href: '/sales/orders', icon: DollarSquareIcon, description: 'Billing documents generated from confirmed Shop POs', permission: 'orders.view' }, - { title: 'Payments', href: '/sales/payments', icon: BankIcon, description: 'Cash and credit payments collected against invoices', permission: 'orders.view' }, ], }, { @@ -131,6 +130,7 @@ export const NAV_GROUPS: NavGroup[] = [ title: 'Accounts', icon: BankIcon, items: [ + { title: 'Analytics', href: '/payments/analytics', icon: BankIcon, description: 'Overview of bank accounts, cheque books and received cheques' }, { title: 'Bank Accounts', href: '/payments/bank-accounts', icon: BankIcon, description: 'Cash and bank accounts used for payments and deposits' }, { title: 'Cheque Books', href: '/payments/cheque-books', icon: ClipboardListIcon, description: 'Cheque books issued from the company’s own supply' }, { title: 'Received Cheques', href: '/payments/received-cheques', icon: DollarSquareIcon, description: 'Cheques received from customers, suppliers, or others' }, diff --git a/distribution_fe/lib/api/index.ts b/distribution_fe/lib/api/index.ts index 0c45d86..ed57b7e 100644 --- a/distribution_fe/lib/api/index.ts +++ b/distribution_fe/lib/api/index.ts @@ -8,7 +8,7 @@ export { drivers, compliance } from './drivers' export { purchaseOrders, grn, purchaseReturns } from './procurement' export { rides, loadingSheets, deliveryTasks } from './operations' export { stock } from './stock' -export { productReports, stockReports, dashboardReports } from './reports' +export { productReports, stockReports, dashboardReports, paymentReports } from './reports' export { shopPos, invoices, payments } from './sales' export { saleReturns, claims, supplierClaims } from './returns-claims' export { sysConfig, ITEM_TRACKING_KEYS, GRN_TAX_KEYS, grnTaxSettings, DEFAULT_GRN_TAX_SETTINGS, invoiceCreationMode, INVOICE_CREATION_MODE_KEY, DEFAULT_INVOICE_CREATION_MODE } from './sys-config' diff --git a/distribution_fe/lib/api/reports.ts b/distribution_fe/lib/api/reports.ts index a620d4e..098689d 100644 --- a/distribution_fe/lib/api/reports.ts +++ b/distribution_fe/lib/api/reports.ts @@ -206,3 +206,28 @@ export const dashboardReports = { inventory: (params?: { from?: string; to?: string; warehouseId?: string; categoryId?: string }) => api.get('/reports/dashboard/inventory', { params }).then(r => r.data), } + +export interface PaymentAnalyticsResponse { + from: string + to: string + summary: { + invoiced: number + collected: number + creditExtended: number + outstanding: number + overdue: number + collectionRatePct: number | null + paymentCount: number + } + byMethod: Array<{ method: 'Cash' | 'Cheque' | 'Credit'; count: number; amount: number; sharePct: number }> + trend: Array<{ period: string; cash: number; cheque: number; credit: number }> + aging: Array<{ bucket: string; invoices: number; balance: number }> + topOutstanding: Array<{ storeId: string; storeName: string; invoices: number; balance: number; overdue: number }> + byAccount: Array<{ accountId: string | null; accountName: string; count: number; amount: number }> + cheques: Array<{ status: string; count: number; amount: number }> +} + +export const paymentReports = { + analytics: (params?: { from?: string; to?: string; regionId?: string }) => + api.get('/reports/payments/analytics', { params }).then(r => r.data), +}