Files
nis2-agile/public/js/dashboard.js
T
DevEnv nis2-agileandClaude Opus 4.8 896663d24c [FEAT] Dashboard: card Documenti istituzionali funzionante + UI (ticket #499)
- loadDocs: 5 voci standard sempre presenti + voci custom + 'Aggiungi voce'
- modale add/edit con descrizione OBBLIGATORIA (richiesta punto 1), link doc opzionale
- delete voci custom; controlli visibili a org_admin/compliance_manager/super_admin
- CSS .doc-* in v3.css; cache-buster dashboard.js/v3.css su dashboard.html; version 1.25.1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:15:26 +02:00

423 lines
27 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 — /api/institutional-docs (ticket #499) ──
// Le 5 voci standard sono SEMPRE presenti (Codice Etico, Mission, Vision,
// Statuto, Piano Sanzionatorio); ogni documento richiede una descrizione.
function canEditDocs() {
var r = (localStorage.getItem('nis2_user_role') || '').toLowerCase();
return r === 'org_admin' || r === 'compliance_manager' || r === 'super_admin';
}
function docRow(item, stdLabel) {
var filled = item && item.description && String(item.description).trim() !== '';
var title = esc((item && item.title) || stdLabel || 'Documento');
var editBtn = canEditDocs()
? '<button class="btn-ghost btn-sm doc-edit" type="button"><i data-lucide="pencil" class="ic"></i></button>' : '';
var delBtn = (canEditDocs() && item && item.id && Number(item.is_standard) !== 1)
? '<button class="btn-ghost btn-sm doc-del" type="button"><i data-lucide="trash-2" class="ic"></i></button>' : '';
var body = filled
? '<span class="doc-desc">' + esc(item.description) + '</span>'
: '<span class="doc-desc doc-empty-desc">Nessuna descrizione — ' + (canEditDocs() ? 'compila la voce.' : 'da compilare.') + '</span>';
var fileLink = (item && item.file_url)
? ' <a class="doc-file" href="' + esc(item.file_url) + '" target="_blank" rel="noopener">documento</a>' : '';
return '<div class="doc-item" data-id="' + esc((item && item.id) || '') +
'" data-key="' + esc((item && item.doc_key) || (stdLabel ? '' : '')) + '">' +
'<div class="doc-main"><b>' + title + fileLink + '</b>' + body + '</div>' +
'<div class="doc-actions">' + editBtn + delBtn + '</div></div>';
}
function stdKeyOf(label, standards) {
for (var i = 0; i < standards.length; i++) if (standards[i].label === label) return standards[i].key;
return null;
}
function loadDocs() {
var card = document.getElementById('doc-card'); if (!card) return;
setLoading('doc-card');
api.request('GET', '/institutional-docs/list').then(function (res) {
var data = (res && res.data) || res || {};
var docs = data.docs || [];
var standards = data.standards || [];
var byKey = {}; var customs = [];
docs.forEach(function (d) {
if (d.doc_key) byKey[d.doc_key] = d; else customs.push(d);
});
var html = '';
// 5 voci standard sempre presenti
standards.forEach(function (s) {
var item = byKey[s.key] || { doc_key: s.key, title: s.label, is_standard: 1 };
html += docRow(item, s.label);
});
// voci custom
customs.forEach(function (d) { html += docRow(d, null); });
if (canEditDocs()) {
html += '<button class="btn-ghost doc-add" type="button" style="margin-top:8px">' +
'<i data-lucide="plus" class="ic"></i>Aggiungi voce</button>';
}
card.innerHTML = '<div class="doc-list">' + html + '</div>';
refreshIcons();
wireDocEvents(card, standards);
}).catch(function (e) {
setError('doc-card', (e && e.message) || 'Riprova tra poco.');
});
}
function docModal(opts, standards) {
var isStd = !!opts.doc_key;
var titleField = isStd
? '<input type="text" id="doc-f-title" class="form-control" value="' + esc(opts.title || '') + '" readonly>'
: '<input type="text" id="doc-f-title" class="form-control" value="' + esc(opts.title || '') + '" maxlength="200" placeholder="Titolo del documento">';
var body =
'<div class="form-group"><label for="doc-f-title">Titolo</label>' + titleField + '</div>' +
'<div class="form-group"><label for="doc-f-desc">Descrizione <span style="color:#c00">*</span></label>' +
'<textarea id="doc-f-desc" class="form-control" rows="4" maxlength="2000" required placeholder="Descrizione del documento (obbligatoria)">' + esc(opts.description || '') + '</textarea></div>' +
'<div class="form-group"><label for="doc-f-file">Link al documento (opzionale)</label>' +
'<input type="url" id="doc-f-file" class="form-control" value="' + esc(opts.file_url || '') + '" placeholder="https://…"></div>';
var footer = '<button class="btn-ghost" type="button" onclick="closeModal()">Annulla</button>' +
'<button class="btn-primary" type="button" id="doc-f-save">Salva</button>';
showModal(opts.id ? 'Modifica documento' : (isStd ? 'Compila: ' + (opts.title || '') : 'Aggiungi documento'), body, { footer: footer });
var saveBtn = document.getElementById('doc-f-save');
saveBtn.addEventListener('click', function () {
var title = document.getElementById('doc-f-title').value.trim();
var desc = document.getElementById('doc-f-desc').value.trim();
var file = document.getElementById('doc-f-file').value.trim();
if (!title) { showNotification('Il titolo è obbligatorio', 'error'); return; }
if (!desc) { showNotification('La descrizione è obbligatoria', 'error'); return; }
saveBtn.disabled = true;
api.request('POST', '/institutional-docs/save', {
id: opts.id || undefined, doc_key: opts.doc_key || undefined,
title: title, description: desc, file_url: file || undefined
}).then(function () {
closeModal(); showNotification('Documento salvato', 'success'); loadDocs();
}).catch(function (e) {
saveBtn.disabled = false;
showNotification((e && e.message) || 'Salvataggio non riuscito', 'error');
});
});
}
function wireDocEvents(card, standards) {
card.querySelectorAll('.doc-edit').forEach(function (btn) {
btn.addEventListener('click', function () {
var el = btn.closest('.doc-item');
docModal({
id: el.getAttribute('data-id') || null,
doc_key: el.getAttribute('data-key') || null,
title: el.querySelector('b') ? el.querySelector('b').childNodes[0].textContent.trim() : '',
description: el.querySelector('.doc-empty-desc') ? '' : (el.querySelector('.doc-desc') ? el.querySelector('.doc-desc').textContent : ''),
file_url: el.querySelector('.doc-file') ? el.querySelector('.doc-file').getAttribute('href') : ''
}, standards);
});
});
card.querySelectorAll('.doc-del').forEach(function (btn) {
btn.addEventListener('click', function () {
var el = btn.closest('.doc-item'); var id = el.getAttribute('data-id');
if (!id || !confirm('Eliminare questa voce?')) return;
api.request('DELETE', '/institutional-docs/' + id).then(function () {
showNotification('Documento eliminato', 'success'); loadDocs();
}).catch(function (e) { showNotification((e && e.message) || 'Eliminazione non riuscita', 'error'); });
});
});
var addBtn = card.querySelector('.doc-add');
if (addBtn) addBtn.addEventListener('click', function () { docModal({}, standards); });
}
// ── 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;
})();