feat(CC-084): add Trust Passport page (selective local export, GDPR-compliant)
This commit is contained in:
parent
d9686fb70d
commit
89dc2cae72
1 changed files with 152 additions and 0 deletions
152
src/app/dashboard/trust/passport/page.tsx
Normal file
152
src/app/dashboard/trust/passport/page.tsx
Normal 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 că 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue