136 lines
6.6 KiB
TypeScript
136 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { apiFetch } from '../../../../lib/api';
|
|
import { useSession } from '../../../../components/session-provider';
|
|
|
|
interface Document {
|
|
id: string; filename: string; classification: string;
|
|
ocrStatus: string | null; expiresAt: string | null; createdAt: string;
|
|
}
|
|
|
|
const CLASS_META: Record<string, {
|
|
label: string; desc: string; cls: string; gdpr: string;
|
|
}> = {
|
|
C0: { label: 'C0 — Public', desc: 'Informații publice, fără restricții', cls: 'text-sky-500 bg-sky-500/10 border-sky-500/20', gdpr: 'Fără restricții GDPR' },
|
|
C1: { label: 'C1 — Intern', desc: 'Uz intern, distribuire controlată', cls: 'text-signal-ok bg-signal-ok/10 border-signal-ok/20', gdpr: 'Art. 5 — Integritate' },
|
|
C2: { label: 'C2 — Confidențial', desc: 'Date comerciale sensibile, parteneri', cls: 'text-signal-warn bg-signal-warn/10 border-signal-warn/20', gdpr: 'Art. 5 — Confidențialitate' },
|
|
C3: { label: 'C3 — Restricționat',desc: 'Date personale, financiare, juridice', cls: 'text-orange-500 bg-orange-500/10 border-orange-500/20', gdpr: 'Art. 9 — Date speciale' },
|
|
C4: { label: 'C4 — Secret', desc: 'NDA, secrete comerciale, date biometrice sau medicale', cls: 'text-signal-danger bg-signal-danger/10 border-signal-danger/20', gdpr: 'Art. 9 + DPA obligatoriu' },
|
|
};
|
|
|
|
export default function ClassificationPage() {
|
|
const { activeTenant } = useSession();
|
|
const tenantId = activeTenant?.tenantId ?? '';
|
|
|
|
const { data: docs = [], isLoading } = useQuery({
|
|
queryKey: ['docs-class', tenantId],
|
|
queryFn: () => apiFetch<Document[]>('/v1/documents?limit=500', { tenantId }),
|
|
enabled: Boolean(tenantId),
|
|
staleTime: 120_000,
|
|
});
|
|
|
|
const total = docs.length;
|
|
const byClass: Record<string, Document[]> = {};
|
|
for (const d of docs) {
|
|
const k = d.classification ?? 'C1';
|
|
byClass[k] = [...(byClass[k] ?? []), d];
|
|
}
|
|
|
|
const highRisk = (byClass['C3'] ?? []).length + (byClass['C4'] ?? []).length;
|
|
const ocrPending = docs.filter((d) => d.ocrStatus === 'pending').length;
|
|
const withExpiry = docs.filter((d) => d.expiresAt).length;
|
|
|
|
return (
|
|
<div className="max-w-4xl space-y-6 p-6">
|
|
<div>
|
|
<h1 className="font-display text-2xl font-semibold text-ink">Clasificare Date</h1>
|
|
<p className="text-sm text-ink-faint mt-1">
|
|
{isLoading ? 'Se încarcă…' : `${total} documente · ${highRisk} cu risc ridicat (C3/C4)`}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick stats */}
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<div className="card p-4 text-center">
|
|
<p className="font-display text-3xl font-bold text-ink">{total}</p>
|
|
<p className="text-xs text-ink-faint mt-1">Total documente</p>
|
|
</div>
|
|
<div className={`card p-4 text-center ${highRisk > 0 ? 'bg-signal-danger/5' : ''}`}>
|
|
<p className={`font-display text-3xl font-bold ${highRisk > 0 ? 'text-signal-danger' : 'text-ink'}`}>{highRisk}</p>
|
|
<p className="text-xs text-ink-faint mt-1">C3/C4 (risc ridicat)</p>
|
|
</div>
|
|
<div className="card p-4 text-center">
|
|
<p className="font-display text-3xl font-bold text-ink">{withExpiry}</p>
|
|
<p className="text-xs text-ink-faint mt-1">Cu dată expirare</p>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="card p-8 text-center text-sm text-ink-faint">Se încarcă…</div>
|
|
) : total === 0 ? (
|
|
<div className="card p-12 text-center space-y-2">
|
|
<p className="text-3xl">📁</p>
|
|
<p className="text-sm text-ink-faint">Niciun document în bibliotecă.</p>
|
|
<Link href="/dashboard/documents" className="text-xs text-bronze-deep hover:underline">Accesează biblioteca →</Link>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{(['C4','C3','C2','C1','C0'] as const).map((cls) => {
|
|
const items = byClass[cls] ?? [];
|
|
const meta = CLASS_META[cls];
|
|
const pct = total > 0 ? Math.round(items.length / total * 100) : 0;
|
|
if (items.length === 0) return null;
|
|
return (
|
|
<div key={cls} className={`card p-5 border ${meta.cls.split(' ').find(c => c.startsWith('border-')) ?? 'border-border'}`}>
|
|
<div className="flex items-start justify-between gap-4 mb-3">
|
|
<div>
|
|
<h2 className={`text-sm font-semibold ${meta.cls.split(' ')[0]}`}>{meta.label}</h2>
|
|
<p className="text-xs text-ink-faint mt-0.5">{meta.desc}</p>
|
|
</div>
|
|
<div className="text-right shrink-0">
|
|
<p className={`font-display text-2xl font-bold ${meta.cls.split(' ')[0]}`}>{items.length}</p>
|
|
<p className="text-[10px] text-ink-faint">{pct}% din total</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress bar */}
|
|
<div className="h-1.5 rounded-full bg-muted/50 mb-3">
|
|
<div className={`h-1.5 rounded-full ${meta.cls.split(' ').find(c => c.startsWith('bg-')) ?? 'bg-primary'}`}
|
|
style={{ width: `${pct}%` }} />
|
|
</div>
|
|
|
|
{/* GDPR note */}
|
|
<p className="text-[10px] text-ink-faint mb-3">⚖ {meta.gdpr}</p>
|
|
|
|
{/* Recent docs sample */}
|
|
<div className="divide-y divide-border/30">
|
|
{items.slice(0, 5).map((d) => (
|
|
<div key={d.id} className="flex items-center gap-2 py-1.5">
|
|
<span className="text-sm">📄</span>
|
|
<span className="text-xs text-ink truncate flex-1">{d.filename}</span>
|
|
{d.expiresAt && <span className="text-[10px] text-signal-warn shrink-0">exp: {d.expiresAt.slice(0, 10)}</span>}
|
|
</div>
|
|
))}
|
|
{items.length > 5 && (
|
|
<p className="text-[10px] text-ink-faint pt-1.5">și alte {items.length - 5} documente…</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{ocrPending > 0 && (
|
|
<div className="rounded-lg border border-signal-warn/20 bg-signal-warn/5 p-4">
|
|
<p className="text-xs text-signal-warn">
|
|
⚠ <strong>{ocrPending} documente</strong> au OCR pendente — clasificarea poate fi incompletă.{' '}
|
|
<Link href="/dashboard/documents/ocr" className="underline">Revizuiește OCR →</Link>
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|