From 24a42f2ae6339c6266793b7af8721c6af484e5f0 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sat, 1 Aug 2026 21:13:35 +0000 Subject: [PATCH] feat(CC-061): add Document Library page (C0-C4 classification, OCR status, expiry alerts) --- src/app/dashboard/documents/page.tsx | 211 +++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 src/app/dashboard/documents/page.tsx diff --git a/src/app/dashboard/documents/page.tsx b/src/app/dashboard/documents/page.tsx new file mode 100644 index 0000000..caf9d47 --- /dev/null +++ b/src/app/dashboard/documents/page.tsx @@ -0,0 +1,211 @@ +'use client'; + +import Link from 'next/link'; +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Document { + id: string; + tenantId: string; + organizationId: string | null; + classification: 'c0' | 'c1' | 'c2' | 'c3' | 'c4'; + paperlessId: string | null; + filename: string; + checksum: string | null; + expiresAt: string | null; + retentionPolicy: string | null; + ocrStatus: string | null; + version: number; + createdAt: string; + updatedAt: string; +} + +const CLASS_META: Record = { + c0: { label: 'C0 Public', desc: 'Date publice, nicio restricție', cls: 'bg-sky-500/10 text-sky-700 dark:text-sky-300' }, + c1: { label: 'C1 Intern', desc: 'Date interne, distribuție limitată', cls: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-300' }, + c2: { label: 'C2 Confidențial', desc: 'Date confidențiale, acces controlat', cls: 'bg-amber-500/10 text-amber-700 dark:text-amber-300' }, + c3: { label: 'C3 Restricționat', desc: 'Date sensibile, autorizare necesară', cls: 'bg-orange-500/10 text-orange-700 dark:text-orange-300' }, + c4: { label: 'C4 Secret', desc: 'Date critice, acces maxim restricționat', cls: 'bg-signal-danger/10 text-signal-danger' }, +}; + +const OCR_META: Record = { + pending: { label: 'OCR în așteptare', cls: 'text-signal-warn' }, + done: { label: 'OCR complet', cls: 'text-signal-ok' }, + failed: { label: 'OCR eșuat', cls: 'text-signal-danger' }, + not_needed:{ label: 'Fără OCR', cls: 'text-ink-faint' }, +}; + +function fileIcon(filename: string): string { + const ext = filename.split('.').pop()?.toLowerCase() ?? ''; + const map: Record = { + pdf: '📄', jpg: '🖼️', jpeg: '🖼️', png: '🖼️', doc: '📝', docx: '📝', + xls: '📊', xlsx: '📊', csv: '📊', zip: '📦', xml: '🔖', + }; + return map[ext] ?? '📎'; +} + +export default function DocumentLibraryPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [filterClass, setFilterClass] = useState(''); + const [search, setSearch] = useState(''); + + const { data: docs = [], isLoading } = useQuery({ + queryKey: ['documents', tenantId, filterClass], + queryFn: () => { + const qs = filterClass ? `?classification=${filterClass}&limit=200` : '?limit=200'; + return apiFetch(`/v1/documents${qs}`, { tenantId }); + }, + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + + const filtered = search + ? docs.filter((d) => d.filename.toLowerCase().includes(search.toLowerCase())) + : docs; + + const expiringSoon = docs.filter((d) => { + if (!d.expiresAt) return false; + const exp = new Date(d.expiresAt); + const in30 = new Date(Date.now() + 30 * 86400_000); + return exp < in30 && exp > new Date(); + }); + + return ( +
+
+
+

Bibliotecă Documente

+

{docs.length} documente înregistrate

+
+
+ + {/* Expiry alert */} + {expiringSoon.length > 0 && ( +
+ ⚠️ +
+

+ {expiringSoon.length} document{expiringSoon.length > 1 ? 'e expiră' : ' expiră'} în 30 zile +

+
    + {expiringSoon.slice(0, 3).map((d) => ( +
  • + {fileIcon(d.filename)} {d.filename} — expiră {new Date(d.expiresAt!).toLocaleDateString('ro-RO')} +
  • + ))} + {expiringSoon.length > 3 && ( +
  • și alte {expiringSoon.length - 3}…
  • + )} +
+
+
+ )} + + {/* Filters */} +
+ setSearch(e.target.value)} + placeholder="Caută după nume fișier…" + className="rounded-lg border bg-card px-3 py-1.5 text-sm text-ink placeholder:text-ink-faint focus:outline-none focus:ring-2 focus:ring-ring w-60" + /> +
+ {['', 'c0', 'c1', 'c2', 'c3', 'c4'].map((cls) => ( + + ))} +
+
+ + {/* Document grid */} + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

📂

+

+ {search ? 'Niciun document nu corespunde căutării.' : 'Niciun document înregistrat.'} +

+

+ Documentele sunt gestionate prin Paperless-ngx și indexate automat. +

+
+ ) : ( +
+ {filtered.map((doc) => { + const cls = CLASS_META[doc.classification]; + const ocr = doc.ocrStatus ? OCR_META[doc.ocrStatus] : null; + const isExpiring = doc.expiresAt && new Date(doc.expiresAt) < new Date(Date.now() + 30 * 86400_000); + return ( +
+ {fileIcon(doc.filename)} +
+
+

{doc.filename}

+ {doc.version > 1 && ( + v{doc.version} + )} + {isExpiring && ( + + Expiră {new Date(doc.expiresAt!).toLocaleDateString('ro-RO')} + + )} +
+
+ + {cls.label} + + {ocr && ( + {ocr.label} + )} + + {new Date(doc.createdAt).toLocaleDateString('ro-RO')} + + {doc.paperlessId && ( + + pls:{doc.paperlessId} + + )} +
+
+ {doc.retentionPolicy && ( + + Retenție: {doc.retentionPolicy} + + )} +
+ ); + })} +
+ )} + + {/* C0-C4 legend */} +
+

+ Clasificare date (C0–C4) +

+
+ {Object.entries(CLASS_META).map(([key, { label, desc, cls }]) => ( +
+

{label}

+

{desc}

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