diff --git a/src/app/dashboard/reports/accountant/page.tsx b/src/app/dashboard/reports/accountant/page.tsx new file mode 100644 index 0000000..83f33d0 --- /dev/null +++ b/src/app/dashboard/reports/accountant/page.tsx @@ -0,0 +1,206 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +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) { + return new Intl.NumberFormat('ro-RO', { + style: 'currency', currency, minimumFractionDigits: 0, + }).format(minorUnits / 100); +} + +export default function AccountantReportsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const now = new Date(); + const [year, setYear] = useState(now.getFullYear()); + + const { data: txs = [], isLoading } = useQuery({ + queryKey: ['accountant-rep', tenantId], + queryFn: () => apiFetch('/v1/transactions', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + + const yearTxs = txs.filter((t) => t.transactionDate.startsWith(String(year))); + const currencies = [...new Set(yearTxs.map((t) => t.currency))]; + + // Monthly breakdown for primary currency + const primaryCurrency = currencies[0] ?? 'RON'; + + // By month: totals and evidence gap count + const months = Array.from({ length: 12 }, (_, i) => { + const m = String(i + 1).padStart(2, '0'); + const prefix = `${year}-${m}`; + const monthTxs = yearTxs.filter((t) => t.currency === primaryCurrency && t.transactionDate.startsWith(prefix)); + const total = monthTxs.reduce((s, t) => s + parseFloat(t.amountMinorUnits), 0); + const gaps = monthTxs.filter((t) => t.evidenceStatus === 'missing' || t.evidenceStatus === 'partial').length; + const label = new Date(year, i, 1).toLocaleDateString('ro-RO', { month: 'short' }); + return { m, label, total, gaps, count: monthTxs.length }; + }); + + const maxTotal = Math.max(...months.map((m) => m.total), 1); + + // Evidence by month summary + const evidenceByStatus = { + missing: yearTxs.filter((t) => t.evidenceStatus === 'missing').length, + partial: yearTxs.filter((t) => t.evidenceStatus === 'partial').length, + complete: yearTxs.filter((t) => t.evidenceStatus === 'complete').length, + not_required: yearTxs.filter((t) => t.evidenceStatus === 'not_required').length, + }; + const evidenceGapRate = yearTxs.length > 0 + ? Math.round(((evidenceByStatus.missing + evidenceByStatus.partial) / yearTxs.length) * 100) + : 0; + + // By type breakdown + const byType: Record = {}; + for (const t of yearTxs) { + byType[t.type] = (byType[t.type] || 0) + parseFloat(t.amountMinorUnits); + } + const sortedTypes = Object.entries(byType).sort((a, b) => b[1] - a[1]).slice(0, 8); + + const years = [...new Set(txs.map((t) => parseInt(t.transactionDate.slice(0, 4))))].sort((a, b) => b - a); + + return ( +
+
+
+

Raport Contabil

+

+ {yearTxs.length} tranzacții în {year} +

+
+
+ + +
+
+ + {isLoading ? ( +
Se încarcă…
+ ) : ( + <> + {/* Evidence health */} +
+ {[ + { label: 'Lipsesc', count: evidenceByStatus.missing, cls: 'text-signal-danger' }, + { label: 'Parțiale', count: evidenceByStatus.partial, cls: 'text-signal-warn' }, + { label: 'Complete', count: evidenceByStatus.complete, cls: 'text-signal-ok' }, + { label: 'Rata gap-uri', count: `${evidenceGapRate}%`, cls: evidenceGapRate > 20 ? 'text-signal-danger' : 'text-ink' }, + ].map(({ label, count, cls }) => ( +
+

{count}

+

{label}

+
+ ))} +
+ + {/* Monthly chart */} +
+

+ Volum lunar — {primaryCurrency} +

+
+ {months.map((m) => { + const pct = maxTotal > 0 ? (m.total / maxTotal) * 100 : 0; + return ( +
+
+
0 ? 'bg-signal-warn/70' : 'bg-primary/70'}`} + style={{ height: `${pct}%`, marginTop: `${100 - pct}%` }} + /> +
+ {m.label} + {m.gaps > 0 && ( + {m.gaps}⚠ + )} +
+ ); + })} +
+

+ + Portocaliu = luni cu dovezi lipsă/parțiale +

+
+ + {/* By type */} + {sortedTypes.length > 0 && ( +
+

Totaluri per tip

+
+ {sortedTypes.map(([type, amount]) => ( +
+ {type} + + {fmtMoney(amount, primaryCurrency)} + +
+ ))} +
+
+ )} + + {/* Monthly table */} +
+ + + + + + + + + + + {months.filter((m) => m.count > 0).map((m) => ( + + + + + + + ))} + {months.filter((m) => m.count > 0).length === 0 && ( + + + + )} + +
LunăTranzacțiiTotal ({primaryCurrency})Gap-uri dovezi
{m.label} {year}{m.count}{fmtMoney(m.total, primaryCurrency)} + {m.gaps > 0 ? ( + {m.gaps} + ) : ( + + )} +
+ Nicio tranzacție în {year} +
+
+ +
+ + Descarcă pachetul complet → + +
+ + )} +
+ ); +}