diff --git a/src/app/dashboard/trust/credentials/page.tsx b/src/app/dashboard/trust/credentials/page.tsx new file mode 100644 index 0000000..10f8976 --- /dev/null +++ b/src/app/dashboard/trust/credentials/page.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } 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; unit: string | null; + subjectType: string; source: string | null; confidence: number | null; + createdAt: string; observedAt: string | null; +} + +const CRED_METRICS = ['certificare', 'certification', 'certificate', 'diploma', 'curs', 'course', 'badge', 'acreditare']; +const ISSUER_ICONS: Record = { + harvard: '🎓', columbia: '🎓', ibm: '🔵', google: '🔴', microsoft: '🟦', + coursera: '📘', udemy: '📙', linkedin: '💼', aws: '☁️', nyif: '💰', default: '📜', +}; + +function issuerIcon(source: string | null): string { + if (!source) return ISSUER_ICONS.default; + const s = source.toLowerCase(); + for (const [k, v] of Object.entries(ISSUER_ICONS)) { + if (s.includes(k)) return v; + } + return ISSUER_ICONS.default; +} + +export default function TrustCredentialsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ metric: 'certificare', value: '', source: '', unit: '', confidence: '1' }); + + const { data: observations = [], isLoading } = useQuery({ + queryKey: ['trust-creds', tenantId], + queryFn: () => apiFetch('/v1/observations', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const credentials = useMemo(() => + observations + .filter((o) => CRED_METRICS.some((k) => o.metric.toLowerCase().includes(k))) + .sort((a, b) => new Date(b.observedAt ?? b.createdAt).getTime() - new Date(a.observedAt ?? a.createdAt).getTime()), + [observations]); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/observations', { tenantId, method: 'POST', body: { + ...form, confidence: parseFloat(form.confidence), + subjectType: 'certification', observedAt: new Date().toISOString(), + }}), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['trust-creds', tenantId] }); setShowAdd(false); setForm({ metric: 'certificare', value: '', source: '', unit: '', confidence: '1' }); }, + }); + + const byIssuer = useMemo(() => { + const map: Record = {}; + for (const c of credentials) { + const key = c.source ?? 'Necunoscut'; + map[key] = [...(map[key] ?? []), c]; + } + return Object.entries(map).sort((a, b) => b[1].length - a[1].length); + }, [credentials]); + + return ( +
+
+
+ +

Credențiale & Certificări

+

+ {isLoading ? 'Se încarcă…' : `${credentials.length} credențiale logate din ${byIssuer.length} emitenti`} +

+
+ +
+ + {showAdd && ( +
+

Credențial nou

+
+ setForm((p) => ({ ...p, value: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm((p) => ({ ...p, source: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + +
+ + setForm((p) => ({ ...p, confidence: e.target.value }))} + className="w-full" /> +
+
+
+ + +
+
+ )} + + {/* Stats */} +
+
+

{credentials.length}

+

total credențiale

+
+
+

{byIssuer.length}

+

emitenți unici

+
+
+

+ {credentials.length > 0 ? Math.round(credentials.reduce((s, c) => s + (c.confidence ?? 1), 0) / credentials.length * 100) : 0}% +

+

confidență medie

+
+
+ + {isLoading ? ( +
Se încarcă…
+ ) : credentials.length === 0 ? ( +
+

🎓

+

Nicio credențială. Adaugă certificările tale.

+

Credențialele se pot adăuga și din Education & Development.

+
+ ) : ( +
+ {byIssuer.map(([issuer, creds]) => ( +
+
+ {issuerIcon(issuer)} +

{issuer}

+ ({creds.length}) +
+
+ {creds.map((c) => ( +
+

{c.value}

+
+ {c.metric} + {new Date(c.observedAt ?? c.createdAt).toLocaleDateString('ro-RO', { month: 'short', year: 'numeric' })} +
+ {c.confidence !== null && ( +
+
+
+ )} +
+ ))} +
+
+ ))} +
+ )} +
+ ); +}