From 68697c15b001a2a2bf11b44fe8b9ae7452d9d589 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:44:20 +0000 Subject: [PATCH] feat(CC-076): add Education & Development page (certifications + courses + study hours) --- src/app/dashboard/education/page.tsx | 219 +++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 src/app/dashboard/education/page.tsx diff --git a/src/app/dashboard/education/page.tsx b/src/app/dashboard/education/page.tsx new file mode 100644 index 0000000..33a16c4 --- /dev/null +++ b/src/app/dashboard/education/page.tsx @@ -0,0 +1,219 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Observation { + id: string; subjectType: string; subjectId: string; metric: string; + value: string; unit: string | null; source: string; + confidence: number | null; observedAt: string | null; createdAt: string; +} + +const EDU_TYPES = ['education', 'course', 'certification', 'skill', 'learning']; + +const CERT_ISSUERS_ICONS: Record = { + harvard: '🎓', columbia: '🎓', nyif: '🏦', ibm: '💻', google: '🔵', + coursera: '📚', udemy: '📖', microsoft: '🪟', aws: '☁️', default: '🏆', +}; + +function issuerIcon(source: string) { + const lower = source.toLowerCase(); + for (const [key, icon] of Object.entries(CERT_ISSUERS_ICONS)) { + if (key !== 'default' && lower.includes(key)) return icon; + } + return CERT_ISSUERS_ICONS.default; +} + +const QUICK_METRICS = [ + { metric: 'curs completat', unit: '', icon: '📚', subjectId: 'learning', placeholder: 'Numele cursului' }, + { metric: 'certificare obtinuta', unit: '', icon: '🏆', subjectId: 'certification', placeholder: 'Certif. + emitent' }, + { metric: 'ore studiu', unit: 'h', icon: '⏱️', subjectId: 'daily', placeholder: '2.5' }, + { metric: 'competenta dobandita', unit: '/10', icon: '⚡', subjectId: 'skill', placeholder: '7' }, +]; + +export default function EducationPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [tab, setTab] = useState<'overview' | 'log'>('overview'); + const [quickVals, setQuickVals] = useState>({}); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ metric: '', value: '', unit: '', source: '', subjectId: 'education', subjectType: 'education' }); + + const { data: rawObs = [], isLoading } = useQuery({ + queryKey: ['education-obs', tenantId], + queryFn: () => apiFetch('/v1/observations', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, + }); + + const eduObs = rawObs.filter((o) => EDU_TYPES.includes(o.subjectType) || EDU_TYPES.some(t => (o.metric ?? '').includes(t))); + + const certs = eduObs.filter((o) => o.metric.includes('certificare') || o.metric.includes('certification') || o.subjectId === 'certification'); + const courses = eduObs.filter((o) => o.metric.includes('curs') || o.metric.includes('course')); + const studyHours = eduObs.filter((o) => o.metric.includes('ore') || o.metric.includes('studiu') || o.unit === 'h'); + const totalHours = studyHours.reduce((s, o) => s + (parseFloat(o.value) || 0), 0); + + const logMut = useMutation({ + mutationFn: ({ metric, value, unit, source, subjectId, subjectType }: { metric: string; value: string; unit: string; source: string; subjectId: string; subjectType: string }) => + apiFetch('/v1/observations', { + tenantId, method: 'POST', + body: { subjectType, subjectId, metric, value, unit, source, confidence: '0.95' }, + }), + onSuccess: (_, vars) => { + qc.invalidateQueries({ queryKey: ['education-obs', tenantId] }); + setQuickVals((prev) => ({ ...prev, [vars.metric]: '' })); + setForm({ metric: '', value: '', unit: '', source: '', subjectId: 'education', subjectType: 'education' }); + setShowCreate(false); + }, + }); + + return ( +
+
+
+

Educație & Dezvoltare

+

+ {isLoading ? 'Se încarcă…' : `${certs.length} certificări · ${courses.length} cursuri · ${totalHours.toFixed(0)}h studiu total`} +

+
+ +
+ + {/* Tabs */} +
+ + +
+ + {/* Quick log */} +
+

Înregistrare rapidă

+
+ {QUICK_METRICS.map((qm) => ( +
+

{qm.icon} {qm.metric}

+
+ setQuickVals({ ...quickVals, [qm.metric]: e.target.value })} + className="flex-1 min-w-0 rounded border bg-card px-2 py-1 text-xs text-ink focus:outline-none focus:ring-1 focus:ring-ring" /> + +
+
+ ))} +
+
+ + {/* Detail form */} + {showCreate && ( +
+

Înregistrare detaliată

+
+ setForm({ ...form, metric: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, value: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, unit: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, source: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+ )} + + {isLoading ? ( +
Se încarcă…
+ ) : tab === 'overview' ? ( +
+ {/* Stats */} +
+ {[ + { label: 'Certificări', value: certs.length, icon: '🏆' }, + { label: 'Cursuri finalizate', value: courses.length, icon: '📚' }, + { label: 'Ore studiu', value: `${totalHours.toFixed(0)}h`, icon: '⏱️' }, + ].map((s) => ( +
+

{s.icon}

+

{s.value}

+

{s.label}

+
+ ))} +
+ + {/* Certifications */} + {certs.length > 0 && ( +
+

Certificări & Diplome

+
+ {certs.map((c) => ( +
+ {issuerIcon(c.source)} +
+

{c.value}

+

{c.source} · {new Date(c.observedAt ?? c.createdAt).toLocaleDateString('ro-RO', { year: 'numeric', month: 'short' })}

+
+ {c.confidence != null && ( + {Math.round(c.confidence * 100)}% + )} +
+ ))} +
+
+ )} + + {eduObs.length === 0 && ( +
+

🎓

+

Nicio activitate educațională înregistrată.

+

Adaugă cursuri, certificări și ore de studiu cu butonul de mai sus.

+
+ )} +
+ ) : ( + /* Log tab */ +
+ {eduObs.length === 0 ? ( +

Niciun jurnal.

+ ) : eduObs.slice(0, 50).map((o) => ( +
+ {issuerIcon(o.source)} +
+

{o.metric}

+

{o.value}{o.unit ? ` ${o.unit}` : ''} · {o.source}

+
+ + {new Date(o.observedAt ?? o.createdAt).toLocaleDateString('ro-RO')} + +
+ ))} +
+ )} +
+ ); +}