feat(cc-049): Finance — transaction summary with evidence breakdown
This commit is contained in:
parent
2c3408e968
commit
7ac5e98be9
1 changed files with 195 additions and 0 deletions
195
src/app/dashboard/finance/page.tsx
Normal file
195
src/app/dashboard/finance/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue