Files
nis2-agile/public/js/dashboard.js
T
DevEnv nis2-agileandClaude Opus 4.8 b0fc190704 [FEAT] UI V3 — foundation (v3.css + topnav-v3.js + Inter/Lucide self-host) + dashboard pilota fedele al mockup
Fase 0 della migrazione UI V3 (sidebar V2 → top-nav navy/oro, font Inter, icone Lucide).
- public/css/v3.css: design system portato fedelmente dal mockup index.html
  (token :root, chrome appbar+mega-menu, card/stat/calendario/anag/doc/fab, @media ≤900/≤560),
  + aggiunte minime V3+ per stati loading/empty/error, mobile nav toggle e touch≥44px (AGID/WCAG).
- public/vendor/inter/: Inter self-host (variable woff2 latin+latin-ext) + inter.css + README (NO Google CDN).
- public/vendor/lucide/lucide.min.js: Lucide UMD self-host (NO unpkg CDN).
- public/js/topnav-v3.js: appbar + mega-menu data-driven dalla mappa IA (§6, 9 gruppi),
  aria-current sulla voce attiva, riusa gli helper di common.js (utente/logout/versione/org-switcher,
  branding, impersonate) e i 3 FAB reali (ARIA + bug-reporter + notifiche). window.loadSidebar=loadTopnavV3.
- public/dashboard.html: convertita al V3 fedele al mockup, cablata sui DATI REALI via api.js
  (overview/compliance-score/upcoming-deadlines/recent-activity/organizations-current);
  monitor giornaliero/settimanale alimentati dagli eventi reali; documenti istituzionali = stato vuoto onesto.
- public/js/dashboard.js (nuovo): wiring dati reali + render calendario/scadenze/attività/anagrafica;
  stati loading/empty/error; zero dati finti del mockup.

node --check OK su topnav-v3.js e dashboard.js. Solo UI: nessun PHP/DB/api.js toccato.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:59:08 +02:00

312 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ═══════════════════════════════════════════════════════════════════════════
* dashboard.js — Dashboard V3 (cablata sui DATI REALI via api.js).
* ───────────────────────────────────────────────────────────────────────────
* Markup/classi FEDELI al mockup "docs/mockup Ui/index.html" (.daystrip/.weekstrip/
* .stat/.sca/.act/.anag/.doc), ma ZERO dati finti: tutto da
* /dashboard/overview · /dashboard/compliance-score · /dashboard/upcoming-deadlines
* /dashboard/recent-activity · /organizations/current
* Stati loading/empty/error gestiti. NESSUN endpoint nuovo.
* I monitor giornaliero/settimanale usano gli EVENTI reali (scadenze + attività);
* "Documenti istituzionali" non ha endpoint → stato vuoto onesto (no invenzione).
* ═══════════════════════════════════════════════════════════════════════════ */
(function () {
'use strict';
// ── Date helpers (porting dal mockup, ma TODAY = data reale) ────────────
var TODAY = new Date(); TODAY.setHours(0, 0, 0, 0);
var MO = ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'];
var WDa = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
var WDi = ['L', 'M', 'M', 'G', 'V', 'S', 'D'];
var DAY = 86400000;
function mi(d) { return (d.getDay() + 6) % 7; }
function addD(d, n) { return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n); }
function iso(d) { return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0'); }
function monday(d) { return addD(d, -mi(d)); }
function same(a, b) { return iso(a) === iso(b); }
function weekLabel(m) { var s = addD(m, 6); return m.getMonth() === s.getMonth() ? (m.getDate() + ' – ' + s.getDate() + ' ' + MO[s.getMonth()]) : (m.getDate() + ' ' + MO[m.getMonth()] + ' – ' + s.getDate() + ' ' + MO[s.getMonth()]); }
function esc(s) { return (typeof escapeHtml === 'function') ? escapeHtml(s) : String(s == null ? '' : s); }
// ── EVENTI reali: dizionario iso-date → [{t, c, time}] ──────────────────
// Popolato da scadenze (api.getUpcomingDeadlines) + attività (recent-activity).
var EVENTS = {};
function pushEvent(dateVal, title, color, time) {
if (!dateVal) return;
var d = new Date(dateVal);
if (isNaN(d.getTime())) return;
var k = iso(d);
(EVENTS[k] = EVENTS[k] || []).push({ t: title || 'Evento', c: color || 'blue', time: time || '' });
}
function evOf(d) { return EVENTS[iso(d)] || []; }
function dots(list) { return list.slice(0, 4).map(function (e) { return '<i class="ev ' + e.c + '"></i>'; }).join(''); }
// ── Popover (porting fedele dal mockup) ─────────────────────────────────
var POP = document.createElement('div'); POP.className = 'cal-pop'; document.body.appendChild(POP);
var popTimer;
function showPop(anchor, html) {
clearTimeout(popTimer);
POP.innerHTML = html; POP.style.display = 'block';
var r = anchor.getBoundingClientRect();
var pw = POP.offsetWidth, ph = POP.offsetHeight;
var left = r.left + r.width / 2 - pw / 2;
left = Math.max(10, Math.min(left, window.innerWidth - pw - 10));
var top = r.top - ph - 10; if (top < 10) top = r.bottom + 10;
POP.style.left = left + 'px'; POP.style.top = top + 'px';
requestAnimationFrame(function () { POP.classList.add('show'); });
}
function hidePop() { POP.classList.remove('show'); popTimer = setTimeout(function () { POP.style.display = 'none'; }, 150); }
function dayPop(d) {
var list = evOf(d);
var head = WDa[mi(d)] + ' ' + d.getDate() + ' ' + MO[d.getMonth()];
var body = list.length
? list.map(function (e) { return '<div class="pe"><span class="ev ' + e.c + '"></span><div>' + (e.time ? '<span class="tt">' + esc(e.time) + '</span>' : '') + '<b>' + esc(e.t) + '</b></div></div>'; }).join('')
: '<div class="empty">Nessun appuntamento.</div>';
return '<h4>' + head + '</h4>' + body;
}
function weekPop(m) {
var items = [];
for (var i = 0; i < 7; i++) { var d = addD(m, i); evOf(d).forEach(function (e) { items.push({ d: d, e: e }); }); }
var body = items.length
? items.slice(0, 6).map(function (o) { return '<div class="pe"><span class="ev ' + o.e.c + '"></span><div><span class="tt">' + WDa[mi(o.d)] + ' ' + o.d.getDate() + '</span><b>' + esc(o.e.t) + '</b></div></div>'; }).join('')
: '<div class="empty">Nessun appuntamento.</div>';
if (items.length > 6) body += '<div class="empty" style="margin-top:6px">+' + (items.length - 6) + ' altri…</div>';
return '<h4>' + weekLabel(m) + '</h4>' + body;
}
function openCal() { window.location.href = 'calendario.html'; }
var dayOffset = 0, weekOffset = 0;
function renderDaily() {
var center = addD(TODAY, dayOffset);
var wrap = document.getElementById('daystrip'); if (!wrap) return; wrap.innerHTML = '';
for (var i = -6; i <= 6; i++) {
(function (d) {
var cell = document.createElement('div');
cell.className = 'daycell' + (mi(d) >= 5 ? ' weekend' : '') + (same(d, TODAY) ? ' today' : '');
cell.innerHTML = '<div class="wd">' + WDa[mi(d)] + '</div><div class="dn">' + d.getDate() + '</div><div class="mo">' + MO[d.getMonth()] + '</div><div class="dots">' + dots(evOf(d)) + '</div>';
cell.addEventListener('mouseenter', function () { showPop(cell, dayPop(d)); });
cell.addEventListener('mouseleave', hidePop);
cell.addEventListener('click', openCal);
wrap.appendChild(cell);
})(addD(center, i));
}
}
function renderWeekly() {
var curMon = monday(TODAY);
var centerMon = addD(curMon, weekOffset * 7);
var wrap = document.getElementById('weekstrip'); if (!wrap) return; wrap.innerHTML = '';
for (var w = -1; w <= 1; w++) {
(function (m) {
var isCur = same(m, curMon);
var diff = Math.round((m - curMon) / (7 * DAY));
var tag = diff === 0 ? 'Settimana in corso' : diff === -1 ? 'Settimana precedente' : diff === 1 ? 'Settimana successiva' : diff < 0 ? (-diff) + ' settimane fa' : 'Tra ' + diff + ' settimane';
var block = document.createElement('div');
block.className = 'weekblock' + (isCur ? ' current' : '');
var minis = '';
for (var i = 0; i < 7; i++) { var d = addD(m, i); minis += '<div class="mini' + (mi(d) >= 5 ? ' weekend' : '') + (same(d, TODAY) ? ' today' : '') + '"><div class="mi-wd">' + WDi[mi(d)] + '</div><div class="mi-dn">' + d.getDate() + '</div><div class="mi-ev">' + dots(evOf(d)) + '</div></div>'; }
block.innerHTML = '<div class="wl"><b>' + weekLabel(m) + '</b><span>' + tag + '</span></div><div class="week7">' + minis + '</div>';
block.addEventListener('mouseenter', function () { showPop(block, weekPop(m)); });
block.addEventListener('mouseleave', hidePop);
block.addEventListener('click', openCal);
wrap.appendChild(block);
})(addD(centerMon, w * 7));
}
}
function wireCalControls() {
var map = [['d-prev', function () { dayOffset--; renderDaily(); }], ['d-next', function () { dayOffset++; renderDaily(); }],
['d-today', function () { dayOffset = 0; renderDaily(); }], ['w-prev', function () { weekOffset--; renderWeekly(); }],
['w-next', function () { weekOffset++; renderWeekly(); }], ['w-today', function () { weekOffset = 0; renderWeekly(); }]];
map.forEach(function (p) { var el = document.getElementById(p[0]); if (el) el.onclick = p[1]; });
}
// ── Stato cella generico (loading/empty/error) ──────────────────────────
function setLoading(id) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>'; }
function setEmpty(id, title, sub, icon) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-empty"><i data-lucide="' + (icon || 'inbox') + '" class="ic"></i><b>' + esc(title) + '</b><span>' + esc(sub || '') + '</span></div>'; refreshIcons(); }
function setError(id, msg) { var el = document.getElementById(id); if (el) el.innerHTML = '<div class="v3-error"><i data-lucide="alert-triangle" class="ic"></i><b>Errore di caricamento</b><span>' + esc(msg || 'Riprova tra poco.') + '</span></div>'; refreshIcons(); }
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
// ── Mappa categoria → colore badge (coerente col mockup) ────────────────
function catColor(cat) {
var c = String(cat || '').toLowerCase();
if (/incid/.test(c)) return 'red';
if (/formaz|training/.test(c)) return 'green';
if (/govern|riesame|board|direzion/.test(c)) return 'blue';
if (/control|rischi|risk|audit/.test(c)) return 'amber';
return 'gray';
}
// ── 1) Stat cards (3) — dati reali da overview + compliance-score ───────
async function loadStats() {
var elGap = document.getElementById('stat-gap');
var elTrain = document.getElementById('stat-training');
var elRisk = document.getElementById('stat-risk');
try {
var ov = await api.getDashboardOverview();
var d = (ov && ov.success && ov.data) ? ov.data : {};
// Formazione: % completata (overview.training {total, completed})
var tTot = d.training ? (parseInt(d.training.total) || 0) : 0;
var tDone = d.training ? (parseInt(d.training.completed) || 0) : 0;
var tPct = tTot > 0 ? Math.round((tDone / tTot) * 100) : 0;
if (elTrain) {
elTrain.querySelector('.val').textContent = tTot > 0 ? (tPct + '%') : '0%';
elTrain.querySelector('.sub').textContent = tTot > 0 ? (tDone + ' di ' + tTot + ' completati') : 'Nessun corso assegnato';
}
// Rischi valutati: total rischi (overview.risks.total) + % implementazione misure
var riskTotal = d.risks ? (parseInt(d.risks.total) || 0) : 0;
// compliance-score per la percentuale "valutati/implementati"
var pct = 0;
try {
var sc = await api.getComplianceScore();
if (sc && sc.success && sc.data) {
pct = (sc.data.avg_implementation && sc.data.avg_implementation > 0) ? Math.round(sc.data.avg_implementation) : Math.round(sc.data.score || 0);
}
} catch (e) {}
if (elRisk) {
elRisk.querySelector('.val').textContent = pct + '%';
elRisk.querySelector('.sub').textContent = riskTotal > 0 ? ('su ' + riskTotal + ' del registro rischi') : 'Registro rischi vuoto';
var barI = elRisk.querySelector('.bar > i'); if (barI) barI.style.width = Math.max(0, Math.min(100, pct)) + '%';
}
// GAP analysis: numero assessment in corso (overview non sempre lo espone → usa list)
if (elGap) { fillGapCard(elGap); }
} catch (e) {
if (elGap) { elGap.querySelector('.val').textContent = '—'; elGap.querySelector('.sub').textContent = 'Dati non disponibili'; }
if (elTrain) { elTrain.querySelector('.val').textContent = '—'; elTrain.querySelector('.sub').textContent = 'Dati non disponibili'; }
if (elRisk) { elRisk.querySelector('.val').textContent = '—'; elRisk.querySelector('.sub').textContent = 'Dati non disponibili'; }
}
}
async function fillGapCard(elGap) {
try {
var r = await api.listAssessments();
var list = (r && r.success && Array.isArray(r.data)) ? r.data : (r && Array.isArray(r.data && r.data.items) ? r.data.items : []);
if (!Array.isArray(list)) list = [];
var inProgress = list.filter(function (a) { var s = String(a.status || '').toLowerCase(); return s && s !== 'completed' && s !== 'completato'; }).length;
elGap.querySelector('.val').textContent = list.length ? String(inProgress) : '0';
elGap.querySelector('.sub').textContent = list.length ? ('su ' + list.length + ' assessment') : 'Nessun assessment';
} catch (e) {
elGap.querySelector('.val').textContent = '—';
elGap.querySelector('.sub').textContent = 'Dati non disponibili';
}
}
// ── 2) Scadenze (prossimi 10 giorni) — dati reali ───────────────────────
async function loadDeadlines() {
setLoading('sca-list');
try {
var r = await api.getUpcomingDeadlines();
var list = (r && r.success && Array.isArray(r.data)) ? r.data : [];
// alimenta i monitor calendario con eventi reali
list.forEach(function (dl) {
var when = dl.due_date || dl.date || dl.deadline;
pushEvent(when, dl.title || dl.description || 'Scadenza', catColor(dl.category || dl.type), '');
});
renderDaily(); renderWeekly();
var hint = document.getElementById('sca-hint');
if (hint) hint.textContent = list.length + (list.length === 1 ? ' in agenda' : ' in agenda');
if (!list.length) { setEmpty('sca-list', 'Nessuna scadenza imminente', 'Le prossime scadenze appariranno qui.', 'calendar-check'); return; }
var html = list.slice(0, 10).map(function (dl) {
var d = new Date(dl.due_date || dl.date || dl.deadline);
var day = isNaN(d.getTime()) ? '—' : d.getDate();
var mon = isNaN(d.getTime()) ? '' : MO[d.getMonth()].charAt(0).toUpperCase() + MO[d.getMonth()].slice(1);
var cat = dl.category || dl.type || '';
var who = dl.owner || dl.assignee || dl.responsible || '';
return '<div class="sca"><div class="dpill"><b>' + day + '</b><span>' + esc(mon) + '</span></div>' +
'<div class="body"><b>' + esc(dl.title || dl.description || 'Scadenza') + '</b>' +
'<div class="meta">' + (cat ? '<span class="badge-t ' + catColor(cat) + '">' + esc(cat) + '</span>' : '') +
(who ? '<span class="who">' + esc(who) + '</span>' : '') + '</div></div></div>';
}).join('');
document.getElementById('sca-list').innerHTML = html;
} catch (e) {
setError('sca-list');
}
}
// ── 3) Attività aperte — dati reali da recent-activity ──────────────────
async function loadActivities() {
setLoading('act-scroll');
try {
var r = await api.getRecentActivity();
var list = (r && r.success && Array.isArray(r.data)) ? r.data : [];
// alimenta i monitor con eventi reali (data attività)
list.forEach(function (a) {
var when = a.created_at || a.timestamp || a.date;
pushEvent(when, a.description || a.message || a.action || 'Attività', catColor(a.category || a.type || a.entity_type), '');
});
renderDaily(); renderWeekly();
if (!list.length) { setEmpty('act-scroll', 'Nessuna attività recente', 'Le attività appariranno qui.', 'activity'); return; }
var html = list.slice(0, 30).map(function (a) {
var title = a.description || a.message || a.action || 'Attività';
var who = a.user_name || a.actor || a.user || '';
var when = a.created_at || a.timestamp || a.date;
var ago = (typeof timeAgo === 'function' && when) ? timeAgo(when) : '';
var st = a.status || a.entity_type || a.type || '';
var stColor = catColor(st);
return '<div class="act"><div class="body"><b>' + esc(title) + '</b>' +
'<div class="meta">' + (who ? '<span class="who">' + esc(who) + '</span>' : '') +
(ago ? '<span class="who">· ' + esc(ago) + '</span>' : '') + '</div></div>' +
(st ? '<span class="badge-t ' + stColor + '">' + esc(st) + '</span>' : '') + '</div>';
}).join('');
document.getElementById('act-scroll').innerHTML = html;
} catch (e) {
setError('act-scroll');
}
}
// ── 4) Anagrafica aziendale — dati reali da organizations/current ───────
var ENTITY_LABEL = { essential: 'Soggetto essenziale', important: 'Soggetto importante', not_applicable: 'Fuori ambito', voluntary: 'Adesione volontaria' };
function initials(name) {
return String(name || '').trim().split(/\s+/).map(function (w) { return w[0]; }).filter(Boolean).slice(0, 2).join('').toUpperCase() || '—';
}
async function loadAnag() {
var card = document.getElementById('anag-card'); if (!card) return;
try {
var r = await api.getCurrentOrg();
if (!r || !r.success || !r.data) { setEmpty('anag-card', 'Nessuna organizzazione', 'Completa l’onboarding per visualizzare l’anagrafica.', 'building-2'); return; }
var o = r.data;
var nis = ENTITY_LABEL[o.entity_type] || (o.entity_type ? esc(o.entity_type) : 'Da classificare');
var fields = [
['Settore', o.sector || o.nis2_sector || '—'],
['Partita IVA', o.vat_number || '—'],
['Sede legale', [o.city, o.country].filter(Boolean).join(', ') || o.legal_address || '—'],
['Dipendenti', (o.employee_count != null && o.employee_count !== '') ? o.employee_count : '—'],
['Codice fiscale', o.fiscal_code || '—'],
['Classificazione NIS2', nis]
];
var grid = fields.map(function (f) { return '<div class="f"><span>' + esc(f[0]) + '</span><b>' + esc(f[1]) + '</b></div>'; }).join('');
card.innerHTML =
'<div class="anag"><div class="anag-logo">' + esc(initials(o.name)) + '</div>' +
'<div class="info"><div class="nm"><h3>' + esc(o.name || 'Organizzazione') + '</h3>' +
'<span class="badge-nis">' + nis + '</span></div>' +
'<div class="anag-grid">' + grid + '</div></div></div>';
} catch (e) {
setError('anag-card');
}
}
// ── 5) Documenti istituzionali — nessun endpoint reale → stato vuoto onesto
function loadDocs() {
var card = document.getElementById('doc-card'); if (!card) return;
setEmpty('doc-card', 'Documenti istituzionali', 'Gestisci Mission, Vision ed Etica nelle Impostazioni.', 'file-text');
}
// ── Init pubblico (chiamato dall'init pagina dopo checkAuth/loadSidebar) ─
function initDashboard() {
wireCalControls();
renderDaily(); renderWeekly(); // render iniziale (eventi reali arriveranno e ri-renderizzano)
loadStats();
loadDeadlines();
loadActivities();
loadAnag();
loadDocs();
}
window.initDashboard = initDashboard;
})();