From 7c3e79e4b7ed138ae254a41126688ee85f82c7d3 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:53:52 +0000 Subject: [PATCH] feat(CC-078): add Export Control page (audit log filtered to export events + GDPR notes) --- .../dashboard/data/export-control/page.tsx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/app/dashboard/data/export-control/page.tsx diff --git a/src/app/dashboard/data/export-control/page.tsx b/src/app/dashboard/data/export-control/page.tsx new file mode 100644 index 0000000..aaf9a6a --- /dev/null +++ b/src/app/dashboard/data/export-control/page.tsx @@ -0,0 +1,128 @@ +'use client'; + +import { useMemo, useState } 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 AuditEntry { + id: string; action: string; entityType: string | null; entityId: string | null; + userId: string | null; userEmail: string | null; details: Record | null; + createdAt: string; +} + +const EXPORT_ACTIONS = ['export', 'download', 'bulk_export', 'csv', 'pdf', 'xlsx', 'extract', 'backup']; + +function isExportEvent(e: AuditEntry) { + const action = e.action.toLowerCase(); + return EXPORT_ACTIONS.some((k) => action.includes(k)); +} + +function groupByDay(entries: AuditEntry[]) { + const map: Record = {}; + for (const e of entries) { + const d = e.createdAt.slice(0, 10); + map[d] = (map[d] ?? 0) + 1; + } + return Object.entries(map).sort((a, b) => a[0].localeCompare(b[0])); +} + +export default function ExportControlPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [filter, setFilter] = useState<'all' | 'exports'>('all'); + + const { data: entries = [], isLoading } = useQuery({ + queryKey: ['audit-export', tenantId], + queryFn: () => apiFetch('/v1/audit-log?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const exportEvents = useMemo(() => entries.filter(isExportEvent), [entries]); + const shown = filter === 'exports' ? exportEvents : entries; + const byDay = useMemo(() => groupByDay(exportEvents), [exportEvents]); + const maxDay = Math.max(...byDay.map(([, c]) => c), 1); + + return ( +
+
+
+

Control Export Date

+

+ {isLoading ? 'Se încarcă…' : `${exportEvents.length} evenimente export din ${entries.length} intrări audit`} +

+
+ Audit complet → +
+ + {/* Activity chart */} + {byDay.length > 0 && ( +
+

Activitate export (ultimele zile)

+
+ {byDay.slice(-30).map(([day, count]) => ( +
+
+
+ ))} +
+

Ultimele {Math.min(30, byDay.length)} zile cu activitate

+
+ )} + + {/* GDPR info */} +
+

Cerințe GDPR art. 30 & 33

+
+

• Exporturile de date personale (C2/C3) trebuie documentate

+

• Destinatarul exportului trebuie înregistrat când conțin date personale

+

• Incidentele de securitate ce implică exporturi neautorizate se raportează în 72h

+
+ + Politici de retenție → + +
+ + {/* Filter */} +
+ {([['all', `Toate acțiunile (${entries.length})`], ['exports', `Exporturi (${exportEvents.length})`]] as const).map(([key, label]) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : shown.length === 0 ? ( +
+

📦

+

Nicio activitate de export înregistrată.

+
+ ) : ( +
+ {shown.slice(0, 100).map((e) => ( +
+ +
+

{e.action}

+
+ {e.userEmail && {e.userEmail}} + {e.entityType && {e.entityType}} +
+
+ + {new Date(e.createdAt).toLocaleString('ro-RO', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })} + +
+ ))} + {shown.length > 100 && ( +

și alte {shown.length - 100} intrări

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