From 62ac5f270ecd06a4d64bc3ccf1de30533b858a65 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:44:18 +0000 Subject: [PATCH] feat(CC-076): add Data Compliance page (GDPR score + consent + doc classification + source health) --- src/app/dashboard/data/compliance/page.tsx | 241 +++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 src/app/dashboard/data/compliance/page.tsx diff --git a/src/app/dashboard/data/compliance/page.tsx b/src/app/dashboard/data/compliance/page.tsx new file mode 100644 index 0000000..c063bb5 --- /dev/null +++ b/src/app/dashboard/data/compliance/page.tsx @@ -0,0 +1,241 @@ +'use client'; + +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import Link from 'next/link'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface Contact { id: string; consentStatus: string; tags: string[]; } +interface Document { id: string; classification: string | null; ocrStatus: string | null; tags: string[]; } +interface DataSource { id: string; name: string; sourceType: string; status: string; lastSyncAt: string | null; } + +const CLS_GDPR: Record = { + C0: { label: 'Public', risk: 'nul', color: 'text-ink-faint' }, + C1: { label: 'Intern', risk: 'scăzut', color: 'text-signal-ok' }, + C2: { label: 'Confidențial', risk: 'mediu', color: 'text-warn' }, + C3: { label: 'Restricționat', risk: 'înalt', color: 'text-orange-500' }, + C4: { label: 'Secret', risk: 'critic', color: 'text-signal-danger' }, +}; + +export default function DataCompliancePage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + + const { data: contacts = [], isLoading: cL } = useQuery({ + queryKey: ['dc-contacts', tenantId], + queryFn: () => apiFetch('/v1/contacts?limit=1000', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: docs = [], isLoading: dL } = useQuery({ + queryKey: ['dc-docs', tenantId], + queryFn: () => apiFetch('/v1/documents?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + const { data: sources = [], isLoading: sL } = useQuery({ + queryKey: ['dc-sources', tenantId], + queryFn: () => apiFetch('/v1/data-sources', { tenantId }), + enabled: Boolean(tenantId), staleTime: 120_000, + }); + + const isLoading = cL || dL || sL; + + const stats = useMemo(() => { + // Consent breakdown + const consent: Record = {}; + for (const c of contacts) consent[c.consentStatus] = (consent[c.consentStatus] ?? 0) + 1; + + // Classification breakdown + const cls: Record = {}; + for (const d of docs) cls[d.classification ?? 'unclassified'] = (cls[d.classification ?? 'unclassified'] ?? 0) + 1; + + // Data sources by type + const srcTypes: Record = {}; + for (const s of sources) srcTypes[s.sourceType] = (srcTypes[s.sourceType] ?? 0) + 1; + const errorSources = sources.filter((s) => s.status === 'error'); + const staleSources = sources.filter((s) => s.lastSyncAt && (Date.now() - new Date(s.lastSyncAt).getTime()) > 7 * 86400_000); + + // GDPR score + const granted = consent['granted'] ?? 0; + const revoked = consent['revoked'] ?? 0; + const total = contacts.length; + const consentScore = total > 0 ? Math.round((granted / total) * 100) : 100; + + // Doc classification score (% classified C0-C4) + const classified = Object.entries(cls).filter(([k]) => k !== 'unclassified').reduce((s, [,v]) => s + v, 0); + const classScore = docs.length > 0 ? Math.round((classified / docs.length) * 100) : 100; + + // Source health score + const sourceScore = sources.length > 0 + ? Math.round(((sources.length - errorSources.length) / sources.length) * 100) + : 100; + + const overallScore = Math.round((consentScore + classScore + sourceScore) / 3); + + return { consent, cls, srcTypes, errorSources, staleSources, consentScore, classScore, sourceScore, overallScore, granted, revoked, total, classified }; + }, [contacts, docs, sources]); + + const scoreCls = (s: number) => s >= 80 ? 'text-signal-ok' : s >= 60 ? 'text-warn' : 'text-signal-danger'; + + return ( +
+
+

Conformitate Date

+

+ GDPR + clasificare date + sănătate surse — platformă de date CEO OS. +

+
+ + {/* Overall score */} +
+
+

{stats.overallScore}

+

scor conformitate

+
+
+ {[ + { label: 'Consimțământ GDPR', score: stats.consentScore, href: '/dashboard/privacy/consents' }, + { label: 'Clasificare documente', score: stats.classScore, href: '/dashboard/privacy/classification' }, + { label: 'Sănătate surse de date', score: stats.sourceScore, href: '/dashboard/data/sources' }, + ].map((s) => ( +
+
+ {s.label} + {s.score}% +
+
+
= 80 ? 'bg-signal-ok' : s.score >= 60 ? 'bg-warn' : 'bg-signal-danger'}`} + style={{ width: `${s.score}%` }} /> +
+
+ ))} +
+
+ + {isLoading ? ( +
Se încarcă…
+ ) : ( +
+ {/* Consent */} +
+
+

Consimțământ GDPR

+ Detalii → +
+
+ {Object.entries(stats.consent).map(([status, count]) => { + const pct = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0; + return ( +
+
+ {status} + {count} ({pct}%) +
+
+
+
+
+ ); + })} +
+ {stats.revoked > 0 && ( +

+ ⚠ {stats.revoked} persoane cu GDPR revocat necesită acțiune. +

+ )} +
+ + {/* Document classification */} +
+
+

Clasificare documente

+ Detalii → +
+
+ {Object.entries(stats.cls).map(([cls, count]) => { + const pct = docs.length > 0 ? Math.round((count / docs.length) * 100) : 0; + const cfg = CLS_GDPR[cls]; + return ( +
+
+ {cls} {cfg ? `(${cfg.label})` : ''} + {count} +
+
+
+
+
+ ); + })} +
+ {stats.classified < docs.length && ( +

+ ⚠ {docs.length - stats.classified} documente neclasificate. +

+ )} +
+ + {/* Data sources health */} +
+
+

Surse de date

+ Detalii → +
+
+
+ Total surse + {sources.length} +
+
+ Active + {sources.filter(s => s.status === 'active').length} +
+ {stats.errorSources.length > 0 && ( +
+ Erori + {stats.errorSources.length} +
+ )} + {stats.staleSources.length > 0 && ( +
+ Stale (>7z) + {stats.staleSources.length} +
+ )} + {Object.entries(stats.srcTypes).map(([type, count]) => ( +
+ {type}{count} +
+ ))} +
+
+
+ )} + + {/* Action items */} +
+

Acțiuni recomandate

+
+ {[ + stats.revoked > 0 && { href: '/dashboard/privacy/consents', severity: 'CRITICAL', text: `${stats.revoked} contacte cu GDPR revocat — șterge sau anonimizează datele.` }, + stats.errorSources.length > 0 && { href: '/dashboard/data/sources', severity: 'HIGH', text: `${stats.errorSources.length} surse de date în eroare — verifică conexiunile.` }, + stats.staleSources.length > 0 && { href: '/dashboard/data/sources', severity: 'MEDIUM', text: `${stats.staleSources.length} surse fără sincronizare >7 zile.` }, + (stats.classified < docs.length) && { href: '/dashboard/privacy/classification', severity: 'MEDIUM', text: `${docs.length - stats.classified} documente neclasificate GDPR.` }, + ].filter(Boolean).map((action: any) => ( + + + {action.severity} + +

{action.text}

+ + ))} + {stats.revoked === 0 && stats.errorSources.length === 0 && stats.classified >= docs.length && ( +

✅ Nicio acțiune urgentă necesară.

+ )} +
+
+
+ ); +}