diff --git a/src/app/dashboard/cash-flow/page.tsx b/src/app/dashboard/cash-flow/page.tsx new file mode 100644 index 0000000..bb8e174 --- /dev/null +++ b/src/app/dashboard/cash-flow/page.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import type { Transaction } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +type MonthKey = string; // 'YYYY-MM' + +interface MonthBucket { + income: number; + expense: number; + net: number; +} + +function monthKey(dateStr: string): MonthKey { + return dateStr.slice(0, 7); +} + +function fmtMonth(key: MonthKey): string { + const [y, m] = key.split('-'); + return new Date(Number(y), Number(m) - 1).toLocaleDateString('ro-RO', { month: 'short', year: 'numeric' }); +} + +function fmtAmount(minor: number, currency: string): string { + return new Intl.NumberFormat('ro-RO', { style: 'currency', currency, maximumFractionDigits: 0 }).format(minor / 100); +} + +export default function CashFlowPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: transactions = [], isLoading } = useQuery({ + queryKey: ['transactions-cashflow', tenantId], + queryFn: () => apiFetch('/v1/transactions', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + + // Group by currency then by month + const currencies = [...new Set(transactions.map((t) => t.currency))].sort(); + + function buildMonthlyFlow(currency: string) { + const byMonth = new Map(); + for (const t of transactions) { + if (t.currency !== currency) continue; + const key = monthKey(t.transactionDate); + const bucket = byMonth.get(key) ?? { income: 0, expense: 0, net: 0 }; + const amt = Number(t.amountMinorUnits); + if (t.type === 'income' || t.type === 'revenue') { + bucket.income += amt; + } else { + bucket.expense += amt; + } + bucket.net = bucket.income - bucket.expense; + byMonth.set(key, bucket); + } + return [...byMonth.entries()].sort((a, b) => a[0].localeCompare(b[0])); + } + + // Running totals per currency + const totals = currencies.map((cur) => { + const months = buildMonthlyFlow(cur); + const totalIncome = months.reduce((s, [, b]) => s + b.income, 0); + const totalExpense = months.reduce((s, [, b]) => s + b.expense, 0); + return { currency: cur, totalIncome, totalExpense, net: totalIncome - totalExpense, months }; + }); + + const maxAbsNet = Math.max(...totals.flatMap((t) => t.months.map(([, b]) => Math.abs(b.net))), 1); + + return ( +
+
+

Cash Flow

+

+ Flux financiar lunar pe valute —{' '} + {isLoading ? '…' : `${transactions.length} tranzacții`} +

+
+ + {isLoading ? ( +

Se încarcă…

+ ) : transactions.length === 0 ? ( +
+

Nicio tranzacție înregistrată

+

Adaugă tranzacții pentru a vedea fluxul de numerar.

+
+ ) : ( + <> + {/* Summary cards */} +
+ {totals.map(({ currency, totalIncome, totalExpense, net }) => ( +
+

{currency}

+
+
+ Venituri + {fmtAmount(totalIncome, currency)} +
+
+ Cheltuieli + {fmtAmount(totalExpense, currency)} +
+
+ Net + = 0 ? 'text-signal-ok' : 'text-signal-danger'}`}> + {fmtAmount(net, currency)} + +
+
+
+ ))} +
+ + {/* Monthly breakdown per currency */} + {totals.map(({ currency, months }) => ( +
+
+

Lunar · {currency}

+
+
+ + + + + + + + + + + + {months.map(([key, bucket]) => { + const barPct = (Math.abs(bucket.net) / maxAbsNet) * 100; + const barColor = bucket.net >= 0 ? 'bg-signal-ok' : 'bg-signal-danger'; + return ( + + + + + + + + ); + })} + +
LunăVenituriCheltuieliNetBalanță
{fmtMonth(key)} + {fmtAmount(bucket.income, currency)} + + {fmtAmount(bucket.expense, currency)} + = 0 ? 'text-signal-ok' : 'text-signal-danger'}`}> + {fmtAmount(bucket.net, currency)} + +
+
+
+
+
+
+ ))} + + )} +
+ ); +}