feat(cc-049): Finance — transaction summary with evidence breakdown

This commit is contained in:
admin-valentin 2026-07-31 19:26:44 +00:00
parent 2c3408e968
commit 7ac5e98be9

View file

@ -0,0 +1,195 @@
'use client';
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type Transaction } from '../../../lib/api';
import { useSession } from '../../../components/session-provider';
function fmt(minorUnits: string, currency: string): string {
return new Intl.NumberFormat('ro-RO', { style: 'currency', currency }).format(
Number(minorUnits) / 100,
);
}
export default function FinancePage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: transactions = [], isLoading } = useQuery({
queryKey: ['transactions-finance', tenantId],
queryFn: () => apiFetch<Transaction[]>('/v1/transactions', { tenantId }),
enabled: Boolean(tenantId),
});
const byCurrency: Record<string, number> = {};
for (const t of transactions)
byCurrency[t.currency] = (byCurrency[t.currency] ?? 0) + Number(t.amountMinorUnits);
const byEvidence = { missing: 0, partial: 0, complete: 0 };
for (const t of transactions)
byEvidence[t.evidenceStatus as keyof typeof byEvidence] =
(byEvidence[t.evidenceStatus as keyof typeof byEvidence] ?? 0) + 1;
const byType: Record<string, number> = {};
for (const t of transactions) byType[t.type] = (byType[t.type] ?? 0) + 1;
const topTypes = Object.entries(byType)
.sort((a, b) => b[1] - a[1])
.slice(0, 8);
const evidenceBar: Record<string, string> = {
complete: 'bg-signal-ok',
partial: 'bg-signal-warn',
missing: 'bg-signal-danger',
};
const evidenceBadge: Record<string, string> = {
complete: 'bg-signal-ok text-white',
partial: 'bg-signal-warn text-white',
missing: 'bg-signal-danger text-white',
};
return (
<div className="max-w-5xl space-y-6">
<div className="flex items-start justify-between">
<div>
<h1 className="font-display text-2xl font-semibold text-ink">Finanțe</h1>
<p className="mt-1 text-sm text-ink-faint">
Sumar tranzacții și status evidențe contabile.
</p>
</div>
<Link
href="/dashboard/transactions"
className="rounded-lg bg-bronze-deep px-4 py-2 text-sm font-medium text-white hover:opacity-90"
>
Gestionează tranzacțiile
</Link>
</div>
{isLoading ? (
<p className="text-sm text-ink-faint">Se încarcă</p>
) : transactions.length === 0 ? (
<div className="card p-10 text-center">
<p className="text-sm font-medium text-ink">Nicio tranzacție înregistrată</p>
<p className="mt-1 text-xs text-ink-faint">Adaugă tranzacții din modulul Tranzacții.</p>
<Link
href="/dashboard/transactions"
className="mt-4 inline-block rounded-lg bg-bronze-deep px-4 py-2 text-sm font-medium text-white"
>
Deschide Tranzacții
</Link>
</div>
) : (
<>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">
Total tranzacții
</p>
<p className="mt-1 font-display text-3xl font-semibold text-ink">
{transactions.length}
</p>
</div>
{Object.entries(byCurrency).map(([cur, total]) => (
<div key={cur} className="card p-5">
<p className="text-xs font-medium uppercase tracking-wider text-ink-faint">
Total {cur}
</p>
<p className="mt-1 font-mono text-xl font-semibold text-ink">
{fmt(String(total), cur)}
</p>
</div>
))}
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<section className="card p-5">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Status dovezi contabile
</h2>
<div className="space-y-3">
{(['complete', 'partial', 'missing'] as const).map((status) => {
const count = byEvidence[status];
const pct =
transactions.length > 0 ? (count / transactions.length) * 100 : 0;
return (
<div key={status}>
<div className="mb-1 flex items-center justify-between">
<span className="text-sm capitalize text-ink">{status}</span>
<span className="font-mono text-xs text-ink-faint">{count}</span>
</div>
<div className="h-2 overflow-hidden rounded-full bg-paper-sunken">
<div
className={`h-full rounded-full ${evidenceBar[status]}`}
style={{ width: `${pct}%` }}
/>
</div>
</div>
);
})}
</div>
</section>
<section className="card p-5">
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Tipuri tranzacție
</h2>
{topTypes.length === 0 ? (
<p className="text-sm text-ink-faint"></p>
) : (
<ul className="space-y-2">
{topTypes.map(([type, count]) => (
<li key={type} className="flex items-center justify-between">
<span className="capitalize text-sm text-ink">{type}</span>
<span className="rounded-full bg-paper-sunken px-2 py-0.5 font-mono text-xs text-ink-faint">
{count}
</span>
</li>
))}
</ul>
)}
</section>
</div>
<section className="card overflow-hidden p-5">
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
Tranzacții recente
</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-paper-sunken text-left">
<th className="pb-2 pr-4 text-xs font-medium text-ink-faint">Data</th>
<th className="pb-2 pr-4 text-xs font-medium text-ink-faint">Tip</th>
<th className="pb-2 pr-4 text-right text-xs font-medium text-ink-faint">
Sumă
</th>
<th className="pb-2 text-xs font-medium text-ink-faint">Dovezi</th>
</tr>
</thead>
<tbody>
{transactions.slice(0, 10).map((t) => (
<tr key={t.id} className="border-b border-paper-sunken last:border-0">
<td className="py-2 pr-4 font-mono text-xs text-ink-faint">
{new Date(t.transactionDate).toLocaleDateString('ro-RO')}
</td>
<td className="py-2 pr-4 capitalize text-ink">{t.type}</td>
<td className="py-2 pr-4 text-right font-mono text-xs text-ink">
{fmt(t.amountMinorUnits, t.currency)}
</td>
<td className="py-2">
<span
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${evidenceBadge[t.evidenceStatus]}`}
>
{t.evidenceStatus}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</>
)}
</div>
);
}