feat(cc-051): Privacy Center overview page
This commit is contained in:
parent
12727faa30
commit
56faae7eab
1 changed files with 145 additions and 0 deletions
145
src/app/dashboard/privacy/page.tsx
Normal file
145
src/app/dashboard/privacy/page.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiFetch } from '../../../lib/api';
|
||||
import type { AuditLogEntry, AiRequest, Member } from '../../../lib/api';
|
||||
import { useSession } from '../../../components/session-provider';
|
||||
|
||||
const B = '/dashboard';
|
||||
|
||||
interface PrivacyModule {
|
||||
label: string;
|
||||
href: string;
|
||||
status: 'live' | 'planned' | 'a3';
|
||||
desc: string;
|
||||
}
|
||||
|
||||
const PRIVACY_MODULES: PrivacyModule[] = [
|
||||
{ label: 'Audit Log', href: `${B}/privacy/audit`, status: 'live', desc: 'Toate acțiunile înregistrate cu actor, timestamp și resursă.' },
|
||||
{ label: 'Activitate AI', href: `${B}/privacy/ai-activity`, status: 'live', desc: 'Context manifest hash per cerere AI — ce date au fost permise.' },
|
||||
{ label: 'Roluri & Permisiuni', href: `${B}/privacy/roles`, status: 'live', desc: 'RBAC owner/admin/member — cine are acces la ce.' },
|
||||
{ label: 'Consimțăminte', href: `${B}/privacy/consents`, status: 'planned', desc: 'Tabel consent_records existent, UI în construcție.' },
|
||||
{ label: 'Clasificare Date', href: `${B}/privacy/classification`, status: 'planned', desc: 'Clase C0–C4 definite în schema; aplicare sistematică în lucru.' },
|
||||
{ label: 'Export & Ștergere', href: `${B}/privacy/export`, status: 'planned', desc: 'Portabilitate GDPR + cerere de ștergere cu excepții juridice.' },
|
||||
{ label: 'Politici Retenție', href: `${B}/privacy/retention`, status: 'a3', desc: 'Politici per categorie de date, faza A3.' },
|
||||
{ label: 'Evenimente Securitate', href: `${B}/privacy/security`, status: 'a3', desc: 'Acces break-glass, incidente, alertă în timp real.' },
|
||||
];
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = { live: '● live', planned: 'în lucru', a3: 'a3' };
|
||||
const STATUS_CLASS: Record<string, string> = {
|
||||
live: 'text-signal-ok',
|
||||
planned: 'text-signal-warn',
|
||||
a3: 'text-ink-line',
|
||||
};
|
||||
|
||||
export default function PrivacyCenterPage() {
|
||||
const { activeTenant } = useSession();
|
||||
const tenantId = activeTenant?.tenantId ?? '';
|
||||
const opts = { enabled: Boolean(tenantId), staleTime: 60_000 };
|
||||
|
||||
const { data: auditEntries = [] } = useQuery({
|
||||
queryKey: ['privacy-audit', tenantId],
|
||||
queryFn: () => apiFetch<AuditLogEntry[]>('/v1/audit-log?limit=200', { tenantId }),
|
||||
...opts,
|
||||
});
|
||||
const { data: aiRequests = [] } = useQuery({
|
||||
queryKey: ['privacy-ai', tenantId],
|
||||
queryFn: () => apiFetch<AiRequest[]>('/v1/ai-requests?limit=200', { tenantId }),
|
||||
...opts,
|
||||
});
|
||||
const { data: members = [] } = useQuery({
|
||||
queryKey: ['privacy-members', tenantId],
|
||||
queryFn: () => apiFetch<Member[]>('/v1/tenants/current/members', { tenantId }),
|
||||
...opts,
|
||||
});
|
||||
|
||||
const liveModules = PRIVACY_MODULES.filter((m) => m.status === 'live').length;
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Privacy Center</h1>
|
||||
<p className="mt-1 text-sm text-ink-faint">
|
||||
Punct central pentru confidențialitate, transparență și control date.
|
||||
{liveModules}/{PRIVACY_MODULES.length} module active.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GDPR rights summary */}
|
||||
<div className="card p-5">
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
|
||||
Drepturile tale ca titular de date
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
{[
|
||||
{ right: 'Acces', desc: 'Datele tale sunt vizibile în platformă.', available: true },
|
||||
{ right: 'Rectificare', desc: 'Poți corecta orice date incorecte.', available: true },
|
||||
{ right: 'Ștergere', desc: 'Cerere de ștergere — în implementare.', available: false },
|
||||
{ right: 'Portabilitate', desc: 'Export date — în implementare.', available: false },
|
||||
{ right: 'Opoziție', desc: 'Opt-out procesare AI — parțial.', available: false },
|
||||
{ right: 'Restricție', desc: 'Limitare procesare — a3.', available: false },
|
||||
].map((r) => (
|
||||
<div key={r.right} className={`rounded-lg border p-3 ${r.available ? 'border-signal-ok/30 bg-signal-ok/5' : 'border-paper-sunken'}`}>
|
||||
<p className="text-xs font-semibold text-ink">{r.right}</p>
|
||||
<p className="mt-0.5 text-[11px] text-ink-faint">{r.desc}</p>
|
||||
<p className={`mt-1 text-[10px] font-semibold ${r.available ? 'text-signal-ok' : 'text-ink-line'}`}>
|
||||
{r.available ? '✓ disponibil' : '○ în lucru'}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Link href={`${B}/privacy/audit`} className="card p-5 hover:border-bronze-deep transition-colors">
|
||||
<p className="text-xs font-medium text-ink-faint">Înregistrări audit</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold text-ink">{auditEntries.length}</p>
|
||||
</Link>
|
||||
<Link href={`${B}/privacy/ai-activity`} className="card p-5 hover:border-bronze-deep transition-colors">
|
||||
<p className="text-xs font-medium text-ink-faint">Cereri AI loggate</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold text-ink">{aiRequests.length}</p>
|
||||
</Link>
|
||||
<Link href={`${B}/privacy/roles`} className="card p-5 hover:border-bronze-deep transition-colors">
|
||||
<p className="text-xs font-medium text-ink-faint">Membri workspace</p>
|
||||
<p className="mt-1 font-display text-2xl font-semibold text-ink">{members.length}</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Privacy modules */}
|
||||
<section>
|
||||
<h2 className="mb-3 text-xs font-semibold uppercase tracking-wider text-ink-faint">
|
||||
Module Privacy & Security
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{PRIVACY_MODULES.map((mod) => (
|
||||
<li key={mod.label}>
|
||||
{mod.status === 'live' ? (
|
||||
<Link href={mod.href} className="card flex items-center gap-4 px-4 py-3 hover:border-bronze-deep transition-colors">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-ink">{mod.label}</p>
|
||||
<p className="mt-0.5 text-xs text-ink-faint">{mod.desc}</p>
|
||||
</div>
|
||||
<span className={`shrink-0 text-[10px] font-semibold ${STATUS_CLASS[mod.status]}`}>
|
||||
{STATUS_LABEL[mod.status]}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="card flex items-center gap-4 px-4 py-3 opacity-60">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-ink">{mod.label}</p>
|
||||
<p className="mt-0.5 text-xs text-ink-faint">{mod.desc}</p>
|
||||
</div>
|
||||
<span className={`shrink-0 text-[10px] font-semibold ${STATUS_CLASS[mod.status]}`}>
|
||||
{STATUS_LABEL[mod.status]}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue