- 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>
423 lines
27 KiB
JavaScript
423 lines
27 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════════════════
|
||
* 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;
|
||
})();
|