diff --git a/src/app/dashboard/evidence/page.tsx b/src/app/dashboard/evidence/page.tsx new file mode 100644 index 0000000..3558b82 --- /dev/null +++ b/src/app/dashboard/evidence/page.tsx @@ -0,0 +1,177 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Transaction { + id: string; description: string | null; amountMinorUnits: string; currency: string; + transactionDate: string; counterparty: string | null; evidenceStatus: string; + evidenceUrl: string | null; category: string | null; +} +interface Document { id: string; filename: string; classification: string; createdAt: string; } + +const EV_META: Record = { + missing: { label: 'Lipsă', cls: 'text-signal-danger bg-signal-danger/10', icon: '❌' }, + partial: { label: 'Parțial', cls: 'text-signal-warn bg-signal-warn/10', icon: '⚠' }, + complete: { label: 'Complet', cls: 'text-signal-ok bg-signal-ok/10', icon: '✓' }, + not_required: { label: 'N/A', cls: 'text-ink-faint bg-muted', icon: '—' }, +}; + +function fmtMoney(minorUnits: string, currency: string) { + try { return new Intl.NumberFormat('ro-RO', { style: 'currency', currency, maximumFractionDigits: 2 }).format(parseFloat(minorUnits) / 100); } + catch { return `${currency} ${(parseFloat(minorUnits) / 100).toFixed(2)}`; } +} + +export default function EvidencePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [filterStatus, setFilterStatus] = useState('missing'); + const [linkTarget, setLinkTarget] = useState(null); + const [evidenceUrl, setEvidenceUrl] = useState(''); + + const { data: transactions = [], isLoading: txLoading } = useQuery({ + queryKey: ['transactions-ev', tenantId, filterStatus], + queryFn: () => apiFetch(`/v1/transactions?evidenceStatus=${filterStatus}&limit=100`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 30_000, + }); + + const { data: docs = [] } = useQuery({ + queryKey: ['docs-ev', tenantId], + queryFn: () => apiFetch('/v1/documents?limit=50', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, evidenceStatus, evidenceUrl }: { id: string; evidenceStatus: string; evidenceUrl?: string }) => + apiFetch(`/v1/transactions/${id}`, { tenantId, method: 'PATCH', body: { evidenceStatus, ...(evidenceUrl ? { evidenceUrl } : {}) } }), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['transactions-ev', tenantId] }); setLinkTarget(null); setEvidenceUrl(''); }, + }); + + const { data: allTx = [] } = useQuery({ + queryKey: ['transactions-stats', tenantId], + queryFn: () => apiFetch('/v1/transactions?limit=500', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 120_000, + }); + + const stats: Record = {}; + for (const t of allTx) stats[t.evidenceStatus] = (stats[t.evidenceStatus] || 0) + 1; + + const totalGap = (stats.missing || 0) + (stats.partial || 0); + const totalTx = allTx.length; + const coveragePct = totalTx > 0 ? Math.round(((stats.complete || 0) + (stats.not_required || 0)) / totalTx * 100) : 100; + + return ( +
+
+

Centru de Dovezi

+

+ Acoperire: = 80 ? 'text-signal-ok font-medium' : 'text-signal-warn font-medium'}>{coveragePct}% + {totalGap > 0 && ` · ${totalGap} tranzacții fără dovezi complete`} +

+
+ + {/* Stats */} +
+ {Object.entries(EV_META).map(([status, m]) => ( + + ))} +
+ +
+ {/* Transaction list */} +
+

+ {EV_META[filterStatus]?.label} ({transactions.length}) +

+ {txLoading ? ( +
Se încarcă…
+ ) : transactions.length === 0 ? ( +
+

+

Nicio tranzacție cu status "{EV_META[filterStatus]?.label}"

+
+ ) : ( +
+ {transactions.map((tx) => ( +
+
+
+

{tx.description ?? tx.counterparty ?? '—'}

+
+ {fmtMoney(tx.amountMinorUnits, tx.currency)} + {tx.transactionDate} + {tx.category && {tx.category}} +
+
+
+ {filterStatus !== 'complete' && filterStatus !== 'not_required' && ( + <> + {linkTarget === tx.id ? ( +
+ setEvidenceUrl(e.target.value)} + className="rounded border px-2 py-1 text-xs bg-card text-ink w-40 focus:outline-none focus:ring-1 focus:ring-ring" /> + + +
+ ) : ( + + )} + {filterStatus === 'missing' && ( + + )} + + )} +
+
+ {tx.evidenceUrl && ( + + 🔗 {tx.evidenceUrl} + + )} +
+ ))} +
+ )} +
+ + {/* Recent documents panel */} +
+

Documente recente

+
+ {docs.slice(0, 15).map((d) => ( +
+

{d.filename}

+

{d.classification}

+
+ ))} + {docs.length === 0 && ( +

Niciun document în bibliotecă

+ )} +
+
+
+
+ ); +}