feat(CC-084): add Trust Passport page (selective local export, GDPR-compliant)

This commit is contained in:
admin-valentin 2026-08-02 17:31:32 +00:00
parent d9686fb70d
commit 89dc2cae72

View file

@ -0,0 +1,152 @@
'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 Observation { id: string; metric: string; value: string; source: string | null; confidence: number | null; observedAt: string | null; createdAt: string; }
interface Goal { id: string; title: string; status: string; }
interface Contact { id: string; consentStatus: string | null; }
const SECTIONS = [
{ id: 'credentials', label: 'Credențiale & Certificări', icon: '🎓' },
{ id: 'professional', label: 'Reputație Profesională', icon: '💼' },
{ id: 'goals', label: 'Obiective finalizate', icon: '🎯' },
{ id: 'skills', label: 'Competențe cheie', icon: '🛠️' },
];
const CRED_METRICS = ['certificare', 'certification', 'certificate', 'diploma', 'curs', 'course', 'badge'];
const SKILL_METRICS = ['skill', 'competenta', 'abilitate', 'limbaj', 'framework', 'tool'];
export default function TrustPassportPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const [selected, setSelected] = useState<Record<string, boolean>>({ credentials: true, professional: true, goals: false, skills: true });
const [generated, setGenerated] = useState(false);
const { data: observations = [] } = useQuery({ queryKey: ['pp-obs', tenantId], queryFn: () => apiFetch<Observation[]>('/v1/observations', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const { data: goals = [] } = useQuery({ queryKey: ['pp-goals', tenantId], queryFn: () => apiFetch<Goal[]>('/v1/goals?limit=200', { tenantId }), enabled: Boolean(tenantId), staleTime: 120_000 });
const preview = useMemo(() => {
const creds = observations.filter((o) => CRED_METRICS.some((k) => o.metric.toLowerCase().includes(k)));
const skills = observations.filter((o) => SKILL_METRICS.some((k) => o.metric.toLowerCase().includes(k)));
const completedGoals = goals.filter((g) => g.status === 'completed');
return { creds, skills, completedGoals };
}, [observations, goals]);
function generatePassport() {
const lines: string[] = [];
lines.push('=== TRUST PASSPORT ===');
lines.push(`Generat: ${new Date().toLocaleDateString('ro-RO', { dateStyle: 'full' })}`);
lines.push('Titular: CEO OS Platform');
lines.push('');
lines.push('IMPORTANT: Aceste date sunt auto-raportate. Partajarea este voluntară.');
lines.push('');
if (selected.credentials && preview.creds.length > 0) {
lines.push('--- CREDENȚIALE & CERTIFICĂRI ---');
for (const c of preview.creds.slice(0, 20)) {
lines.push(`${c.value} (${c.source ?? 'necunoscut'}) — ${c.metric}`);
}
lines.push('');
}
if (selected.skills && preview.skills.length > 0) {
lines.push('--- COMPETENȚE ---');
for (const s of preview.skills.slice(0, 20)) {
lines.push(`${s.value} (${s.metric})`);
}
lines.push('');
}
if (selected.goals && preview.completedGoals.length > 0) {
lines.push('--- OBIECTIVE FINALIZATE ---');
for (const g of preview.completedGoals.slice(0, 15)) {
lines.push(`${g.title}`);
}
lines.push('');
}
lines.push('---');
lines.push('Generat de CEO OS — date sub controlul exclusiv al titularului.');
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `trust-passport-${new Date().toISOString().slice(0,10)}.txt`;
a.click();
URL.revokeObjectURL(url);
setGenerated(true);
}
return (
<div className="max-w-3xl space-y-6 p-6">
<div>
<nav className="text-xs text-ink-faint mb-1">
<Link href="/dashboard/trust" className="hover:underline">Trust Dashboard</Link> / Trust Passport
</nav>
<h1 className="font-display text-2xl font-semibold text-ink">Trust Passport</h1>
<p className="text-sm text-ink-faint mt-1">Selectează ce partajezi. Tu controlezi datele.</p>
</div>
<div className="card p-4 bg-primary/5 border-primary/20 space-y-1">
<p className="text-xs font-semibold text-ink">Principii de partajare</p>
<ul className="text-[10px] text-ink-faint space-y-0.5 list-disc list-inside">
<li>Datele se exportă local nimic nu se trimite automat nicăieri</li>
<li>Tu decizi cu cine partajezi fișierul generat</li>
<li>Orice destinatar poate vedea datele sunt auto-raportate</li>
<li>Pașaportul nu are valoare legală sau contractuală</li>
</ul>
</div>
{/* Section selector */}
<div className="card p-5 space-y-3">
<p className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Ce incluzi în pașaport</p>
{SECTIONS.map((s) => (
<label key={s.id} className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={selected[s.id] ?? false}
onChange={(e) => setSelected((p) => ({ ...p, [s.id]: e.target.checked }))}
className="h-4 w-4 rounded border-border text-primary focus:ring-ring" />
<span className="text-lg">{s.icon}</span>
<div>
<p className="text-sm font-medium text-ink">{s.label}</p>
<p className="text-[10px] text-ink-faint">
{s.id === 'credentials' && `${preview.creds.length} credențiale`}
{s.id === 'professional' && 'Rate finalizare obiective/taskuri'}
{s.id === 'goals' && `${preview.completedGoals.length} obiective finalizate`}
{s.id === 'skills' && `${preview.skills.length} competențe`}
</p>
</div>
</label>
))}
</div>
{/* Preview counts */}
<div className="grid grid-cols-3 gap-3">
<div className="card p-3 text-center">
<p className="text-xl font-bold text-ink">{selected.credentials ? preview.creds.length : 0}</p>
<p className="text-[10px] text-ink-faint">credențiale</p>
</div>
<div className="card p-3 text-center">
<p className="text-xl font-bold text-ink">{selected.skills ? preview.skills.length : 0}</p>
<p className="text-[10px] text-ink-faint">competențe</p>
</div>
<div className="card p-3 text-center">
<p className="text-xl font-bold text-ink">{selected.goals ? preview.completedGoals.length : 0}</p>
<p className="text-[10px] text-ink-faint">obiective</p>
</div>
</div>
<button onClick={generatePassport}
className="w-full rounded-lg bg-primary py-3 text-sm font-semibold text-white hover:bg-primary/90">
Generează și descarcă Trust Passport (.txt)
</button>
{generated && (
<div className="card p-3 bg-signal-ok/10 border-signal-ok/30 text-center">
<p className="text-sm text-signal-ok font-medium">Pașaport generat și descărcat.</p>
<p className="text-[10px] text-ink-faint mt-1">Partajează fișierul manual cu cine dorești.</p>
</div>
)}
</div>
);
}