diff --git a/src/app/dashboard/reports/financial/page.tsx b/src/app/dashboard/reports/financial/page.tsx new file mode 100644 index 0000000..46b3b7e --- /dev/null +++ b/src/app/dashboard/reports/financial/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import Link from 'next/link'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../../lib/api'; +import type { Transaction } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +const B = '/dashboard'; + +function fmtMoney(minorUnits: number, currency: string): string { + const major = minorUnits / 100; + return new Intl.NumberFormat('ro-RO', { style: 'currency', currency, minimumFractionDigits: 0 }).format(major); +} + +function MonthBar({ label, value, maxVal }: { label: string; value: number; maxVal: number }) { + const pct = maxVal > 0 ? Math.round((value / maxVal) * 100) : 0; + return ( +
+
+ + {value.toLocaleString('ro-RO')} + +
+
+
+ {label} +
+
+ ); +} + +export default function FinancialReportsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: transactions = [], isLoading } = useQuery({ + queryKey: ['rep-transactions', tenantId], + queryFn: () => apiFetch('/v1/transactions', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + + // Group by currency + const currencies = [...new Set(transactions.map((t) => t.currency))]; + + // For each currency: total income vs total expense (by type) + // type can be anything — we assume any positive amount is 'income' unless type contains 'expense'/'cost'/'plata'/'cheltuiala' + // More pragmatically: show totals by type + function getStats(currency: string) { + const txs = transactions.filter((t) => t.currency === currency); + const byType: Record = {}; + for (const t of txs) { + const key = t.type || 'necunoscut'; + byType[key] = (byType[key] || 0) + parseFloat(t.amountMinorUnits); + } + const total = txs.reduce((s, t) => s + parseFloat(t.amountMinorUnits), 0); + return { txs, byType, total }; + } + + // Monthly volume (last 12 months) for first currency + const primaryCurrency = currencies[0] ?? 'RON'; + const now = new Date(); + const months: { key: string; label: string; total: number }[] = []; + for (let i = 11; i >= 0; i--) { + const d = new Date(now.getFullYear(), now.getMonth() - i, 1); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; + const label = d.toLocaleDateString('ro-RO', { month: 'short' }); + const total = transactions + .filter((t) => t.currency === primaryCurrency && t.transactionDate.startsWith(key)) + .reduce((s, t) => s + parseFloat(t.amountMinorUnits), 0); + months.push({ key, label, total }); + } + const maxMonthly = Math.max(...months.map((m) => m.total), 1); + + // Evidence status + const evByStatus: Record = {}; + for (const t of transactions) { + evByStatus[t.evidenceStatus] = (evByStatus[t.evidenceStatus] || 0) + 1; + } + + return ( +
+
+

Raport Financiar

+

+ {transactions.length} tranzacții înregistrate +

+
+ + {isLoading ? ( +
Se încarcă…
+ ) : transactions.length === 0 ? ( +
+

Nicio tranzacție înregistrată

+ + Adaugă prima tranzacție → + +
+ ) : ( + <> + {/* Per-currency stats */} + {currencies.map((currency) => { + const { txs, byType, total } = getStats(currency); + const sortedTypes = Object.entries(byType).sort((a, b) => b[1] - a[1]); + return ( +
+
+

+ {currency} — {txs.length} tranzacții +

+ + {fmtMoney(total, currency)} + +
+
+ {sortedTypes.map(([type, amount]) => ( +
+ {type} + {fmtMoney(amount, currency)} +
+ ))} +
+
+ ); + })} + + {/* Monthly volume chart */} +
+

+ Volum lunar — {primaryCurrency} (ultimele 12 luni) +

+
+ {months.map((m) => ( + + ))} +
+
+ + {/* Evidence status */} +
+
+

Status dovezi

+ + Tranzacții → + +
+
+ {[ + { key: 'missing', label: 'Lipsesc', color: 'text-signal-danger' }, + { key: 'partial', label: 'Parțiale', color: 'text-signal-warn' }, + { key: 'complete', label: 'Complete', color: 'text-signal-ok' }, + { key: 'not_required', label: 'Neobligatoriu', color: 'text-ink-faint' }, + ].map(({ key, label, color }) => ( +
+

+ {evByStatus[key] ?? 0} +

+

{label}

+
+ ))} +
+
+ + )} +
+ ); +}