diff --git a/src/app/dashboard/accountant-pack/page.tsx b/src/app/dashboard/accountant-pack/page.tsx new file mode 100644 index 0000000..296ff70 --- /dev/null +++ b/src/app/dashboard/accountant-pack/page.tsx @@ -0,0 +1,239 @@ +'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'; + +interface Organization { id: string; name: string; } + +const B = '/dashboard'; + +function fmtMoney(minorUnits: number, currency: string) { + return new Intl.NumberFormat('ro-RO', { + style: 'currency', currency, minimumFractionDigits: 0, + }).format(minorUnits / 100); +} + +function downloadJson(data: unknown, filename: string) { + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); + setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1000); +} + +function downloadCsv(rows: string[][], filename: string) { + const csv = rows.map((r) => r.map((c) => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n'); + const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; a.download = filename; + document.body.appendChild(a); a.click(); + setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 1000); +} + +export default function AccountantPackPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const now = new Date(); + const [fromDate, setFromDate] = useState( + `${now.getFullYear()}-01-01` + ); + const [toDate, setToDate] = useState( + `${now.getFullYear()}-12-31` + ); + + const { data: txs = [], isLoading: loadingTxs } = useQuery({ + queryKey: ['accountant-txs', tenantId], + queryFn: () => apiFetch('/v1/transactions', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + const { data: orgs = [] } = useQuery({ + queryKey: ['orgs', tenantId], + queryFn: () => apiFetch('/v1/organizations', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 300_000, + }); + + const orgMap = Object.fromEntries(orgs.map((o) => [o.id, o.name])); + + // Filter by date range + const inRange = txs.filter((t) => { + const d = t.transactionDate.slice(0, 10); + return d >= fromDate && d <= toDate; + }); + + const missing = inRange.filter((t) => t.evidenceStatus === 'missing'); + const partial = inRange.filter((t) => t.evidenceStatus === 'partial'); + const complete = inRange.filter((t) => t.evidenceStatus === 'complete'); + const notReq = inRange.filter((t) => t.evidenceStatus === 'not_required'); + + const totalAmount = (list: Transaction[], currency: string) => + list.filter((t) => t.currency === currency) + .reduce((s, t) => s + parseFloat(t.amountMinorUnits), 0); + + const currencies = [...new Set(inRange.map((t) => t.currency))]; + + const handleExportCsv = () => { + const headers = ['Data', 'Tip', 'Suma', 'Moneda', 'Organizatie', 'Status dovada', 'Sursa']; + const rows = inRange.map((t) => [ + t.transactionDate.slice(0, 10), + t.type, + (parseFloat(t.amountMinorUnits) / 100).toFixed(2), + t.currency, + orgMap[t.organizationId] ?? t.organizationId, + t.evidenceStatus, + t.source ?? '', + ]); + const ts = new Date().toISOString().slice(0, 10); + downloadCsv([headers, ...rows], `pachet-contabil-${ts}.csv`); + }; + + const handleExportJson = () => { + const ts = new Date().toISOString().slice(0, 10); + downloadJson({ + exportedAt: new Date().toISOString(), + period: { from: fromDate, to: toDate }, + summary: { + total: inRange.length, + missing: missing.length, + partial: partial.length, + complete: complete.length, + notRequired: notReq.length, + }, + transactions: inRange.map((t) => ({ + ...t, + organizationName: orgMap[t.organizationId] ?? null, + amountMajorUnits: parseFloat(t.amountMinorUnits) / 100, + })), + }, `pachet-contabil-${ts}.json`); + }; + + return ( +
+
+
+

Pachet Contabil

+

+ Export tranzacții cu status dovezi — pentru contabilitate și audit +

+
+
+ + +
+
+ + {/* Period selector */} +
+
+ + setFromDate(e.target.value)} + className="rounded-lg border bg-card px-3 py-1.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+
+ + setToDate(e.target.value)} + className="rounded-lg border bg-card px-3 py-1.5 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" + /> +
+

+ {inRange.length} tranzacții în perioadă +

+
+ + {/* Evidence status summary */} +
+ {[ + { label: 'Lipsesc dovezi', count: missing.length, cls: 'text-signal-danger', bg: 'bg-signal-danger/5 border-signal-danger/20' }, + { label: 'Dovezi parțiale', count: partial.length, cls: 'text-signal-warn', bg: 'bg-signal-warn/5 border-signal-warn/20' }, + { label: 'Dovezi complete', count: complete.length, cls: 'text-signal-ok', bg: 'bg-signal-ok/5 border-signal-ok/20' }, + { label: 'Neobligatoriu', count: notReq.length, cls: 'text-ink-faint', bg: 'bg-muted/30 border-border/50' }, + ].map(({ label, count, cls, bg }) => ( +
+

{count}

+

{label}

+
+ ))} +
+ + {/* Totals by currency */} + {currencies.length > 0 && ( +
+

Totaluri per monedă

+
+ {currencies.map((cur) => ( +
+
+ {cur} +
+ lipsă: {fmtMoney(totalAmount(missing, cur), cur)} + parțial: {fmtMoney(totalAmount(partial, cur), cur)} +
+
+ {fmtMoney(totalAmount(inRange, cur), cur)} +
+ ))} +
+
+ )} + + {/* Missing evidence list */} + {missing.length > 0 && ( +
+
+

+ ⚠ Tranzacții fără dovezi ({missing.length}) +

+ + Administrează → + +
+
+ {missing.slice(0, 20).map((t) => ( +
+
+
+ {t.type} + + {orgMap[t.organizationId] ?? '—'} + +
+

{t.transactionDate.slice(0, 10)}

+
+ + {fmtMoney(parseFloat(t.amountMinorUnits), t.currency)} + +
+ ))} + {missing.length > 20 && ( +

+ și alte {missing.length - 20} tranzacții… +

+ )} +
+
+ )} + + {isLoading && ( +
Se încarcă…
+ )} +
+ ); +}