Dopo il revamp V3, allineati i layer che descrivevano ancora la sidebar V2: - topnav-v3.js: aggiunte le pagine orfane in nav (Matrice RACI → Struttura interna, Scadenziario → Monitoraggio, Compliance Journey → Guida) + cablato data-i18n su gruppi e voci del mega-menu (traduzione IT/EN come in V2). - i18n.js: nuove chiavi nav V3 (gruppi mega-menu + connettori/whistleblowing/ normative/cross-analysis/kb/architettura/simulazione/integrazioni/workflow/ audit ACN/registra incidente). - AIService.php navigationMapBlock: riscritta sul menu IN ALTO V3 (9 gruppi + pulsante "Registra incidente"), sync con topnav-v3.js NAV_GROUPS; ARIA non parla più di "barra laterale". - help.js: corrette le 3 menzioni "sidebar" → barra/menu in alto. - KB nis2_kb: nuovo docs/kb/V3_UI_NAVIGAZIONE.md + scripts/ingest-v3-ui-doc.php (ingerito host-side: 3 chunk SYSTEM, rimossi i chunk V2; ARIA RAG ora guida sul top-nav). - Cache-buster ?v=20260627n sui 3 JS modificati su tutte le pagine. Deploy: JS/HTML statici live via bind-mount; php-fpm reload (host + nis2-app) per AIService. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
444 lines
26 KiB
HTML
444 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>Calendario · NIS2 Agile</title>
|
|
<!-- UI V3: Inter self-host + design system v3.css. Self-host only: no external font/script hosts. -->
|
|
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
|
|
<link rel="stylesheet" href="/css/v3.css?v=20260627">
|
|
<!-- PWA:start -->
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<meta name="theme-color" content="#0066CC">
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32.png">
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16.png">
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="NIS2 Agile">
|
|
<meta name="application-name" content="NIS2 Agile">
|
|
<script src="/js/pwa.js?v=20260614" defer></script>
|
|
<!-- PWA:end -->
|
|
<style>
|
|
/* ── Calendario V3 — shell app full-page (fonte: docs/mockup Ui/calendario.html).
|
|
Solo classi NON coperte da v3.css; token condivisi via v3.css. Il calendario reale
|
|
è un AGGREGATORE READ-ONLY (api.calendarEvents/calendarSummary): niente create/edit
|
|
eventi → dal mockup riusiamo .cal-app/.cal-side/.cal-main/.mv-grid/.view-switch/.legend,
|
|
wiring sui dati reali. Le due viste reali sono Griglia (mese) e Lista. */
|
|
.cal-app{display:grid;grid-template-columns:250px 1fr;gap:18px;align-items:start}
|
|
.cal-side{display:flex;flex-direction:column;gap:16px}
|
|
|
|
/* KPI sintetici (by_status) */
|
|
.cal-kpis{display:grid;gap:10px}
|
|
.cal-kpi{background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 14px;box-shadow:var(--shadow);display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
.cal-kpi .n{font-size:1.5rem;font-weight:800;line-height:1}
|
|
.cal-kpi .l{font-size:.74rem;text-transform:uppercase;letter-spacing:.04em;color:var(--muted)}
|
|
.cal-kpi.k-overdue .n{color:var(--c-red)} .cal-kpi.k-due_soon .n{color:#b45309}
|
|
.cal-kpi.k-upcoming .n{color:var(--c-blue)} .cal-kpi.k-done .n{color:#15803d}
|
|
|
|
/* legenda categorie + filtri */
|
|
.legend{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px;box-shadow:var(--shadow)}
|
|
.legend p{margin:0 0 9px;font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#8093A8}
|
|
.cal-chips{display:flex;flex-direction:column;gap:4px}
|
|
.cal-chip{display:inline-flex;align-items:center;gap:9px;font-size:12.5px;padding:5px 4px;border-radius:7px;cursor:pointer;user-select:none}
|
|
.cal-chip:hover{background:#F3F7FC}
|
|
.cal-chip input{margin:0;cursor:pointer;accent-color:var(--c-blue);flex:0 0 auto}
|
|
.cal-chip .dot{width:11px;height:11px;border-radius:3px;display:inline-block;flex:0 0 auto}
|
|
.cal-chip .lbl{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
|
|
/* main */
|
|
.cal-main{min-width:0}
|
|
.cal-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}
|
|
.tb-left{display:flex;align-items:center;gap:10px}
|
|
.tb-nav{display:flex;gap:2px}
|
|
.tb-title{font-size:19px;font-weight:800;text-transform:capitalize;letter-spacing:-.3px;margin-left:4px}
|
|
.view-switch{display:inline-flex;background:#E7EDF4;border-radius:9px;padding:3px}
|
|
.view-switch button{border:0;background:transparent;font:inherit;font-weight:600;font-size:12.5px;color:var(--muted);padding:6px 14px;border-radius:7px;cursor:pointer}
|
|
.view-switch button.on{background:#fff;color:var(--ink);box-shadow:0 1px 2px rgba(16,40,80,.12)}
|
|
|
|
/* vista mese (griglia) */
|
|
.mv{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
|
|
.mv-dow{display:grid;grid-template-columns:repeat(7,1fr);background:#F6F9FC;border-bottom:1px solid var(--line)}
|
|
.mv-dow div{padding:9px 8px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#7a8aa0}
|
|
.mv-grid{display:grid;grid-template-columns:repeat(7,1fr)}
|
|
.mc{min-height:108px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);padding:6px 7px;position:relative;overflow:hidden}
|
|
.mc:nth-child(7n){border-right:0}
|
|
.mc.has{cursor:pointer}
|
|
.mc.has:hover{background:#FAFCFE}
|
|
.mc .dnum{font-size:12.5px;font-weight:600;width:25px;height:25px;display:flex;align-items:center;justify-content:center;border-radius:50%;color:var(--ink)}
|
|
.mc.other{background:#FAFBFD}.mc.other .dnum{color:#AEBAC8}
|
|
.mc.today .dnum{background:var(--c-blue);color:#fff}
|
|
.mc .chip{display:flex;align-items:center;gap:6px;font-size:11px;font-weight:600;color:var(--ink);border-radius:5px;padding:2px 6px;margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
.mc .chip:hover{background:#F0F4F9}
|
|
.mc .chip .cdot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
|
|
.mc .more{font-size:10.5px;color:var(--muted);margin-top:3px;font-weight:600;cursor:pointer}
|
|
.mc-empty{grid-column:1 / -1;text-align:center;padding:30px 16px;color:var(--muted);font-size:13px}
|
|
|
|
/* vista lista */
|
|
.cal-list-card{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;box-shadow:var(--shadow)}
|
|
.cal-list{width:100%;border-collapse:collapse;font-size:13px}
|
|
.cal-list th,.cal-list td{text-align:left;padding:10px 14px;border-top:1px solid var(--line);vertical-align:middle}
|
|
.cal-list thead th{border-top:0;font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:#8093A8;font-weight:700;background:#F6F9FC}
|
|
.cal-list tr.row-overdue td{background:#FEF2F2}
|
|
.cal-list a.cal-link{color:var(--c-blue);font-weight:600}
|
|
.cal-list a.cal-link:hover{text-decoration:underline}
|
|
.type-tag{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;padding:3px 9px;border-radius:6px}
|
|
.type-tag .cdot{width:8px;height:8px;border-radius:50%}
|
|
.st-badge{display:inline-block;font-size:10px;font-weight:700;padding:3px 9px;border-radius:6px;white-space:nowrap;color:#fff;text-transform:uppercase;letter-spacing:.03em}
|
|
.st-badge.st-overdue{background:#dc2626}.st-badge.st-due_soon{background:#d97706}
|
|
.st-badge.st-upcoming{background:#2563eb}.st-badge.st-done{background:#16a34a}
|
|
.cal-empty{text-align:center;padding:30px 16px;color:var(--muted);font-size:13px}
|
|
.hidden{display:none !important}
|
|
|
|
/* popover giorno (modale) */
|
|
.cal-modal{position:fixed;inset:0;background:rgba(13,28,52,.45);display:none;align-items:flex-start;justify-content:center;z-index:90;padding:40px 16px;overflow:auto}
|
|
.cal-modal.open{display:flex}
|
|
.cal-modal-card{background:#fff;border-radius:14px;width:100%;max-width:520px;box-shadow:0 24px 60px rgba(11,30,60,.35)}
|
|
.cal-modal-head{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;border-bottom:1px solid var(--line)}
|
|
.cal-modal-head h3{margin:0;font-size:15px;font-weight:700}
|
|
.cal-modal-x{border:0;background:transparent;cursor:pointer;color:var(--muted);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;line-height:1}
|
|
.cal-modal-x:hover{background:#F0F4F9}
|
|
.cal-modal-body{padding:14px 18px}
|
|
.cal-pop-item{display:flex;gap:10px;align-items:flex-start;padding:9px 0;border-top:1px solid var(--line)}
|
|
.cal-pop-item:first-child{border-top:none}
|
|
.cal-pop-item .cdot{width:10px;height:10px;border-radius:50%;margin-top:4px;flex:0 0 auto}
|
|
|
|
@media (max-width:900px){
|
|
.cal-app{grid-template-columns:1fr}
|
|
.cal-side{flex-direction:row;flex-wrap:wrap}
|
|
.cal-kpis{grid-template-columns:repeat(2,1fr);flex:1;min-width:220px}
|
|
.legend{flex:1;min-width:220px}
|
|
.mc{min-height:84px}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
|
|
<div id="topnav"></div>
|
|
|
|
<main class="wrap">
|
|
<nav class="crumb" aria-label="breadcrumb">
|
|
<i data-lucide="home" class="ic"></i><a href="dashboard.html">Home</a>
|
|
<i data-lucide="chevron-right" class="ic"></i><span data-i18n="cal.title">Calendario</span>
|
|
</nav>
|
|
<div class="page-head">
|
|
<div>
|
|
<h1 data-i18n="cal.title">Calendario scadenze</h1>
|
|
<p class="sub">Vista temporale aggregata di scadenze, revisioni e controlli del sistema</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cal-app">
|
|
<!-- Sidebar: KPI + legenda/filtri categoria + stato -->
|
|
<aside class="cal-side">
|
|
<div class="cal-kpis" id="cal-summary" aria-live="polite"></div>
|
|
|
|
<div class="legend">
|
|
<p>Filtra per tipo</p>
|
|
<div class="cal-chips" id="cal-type-chips"></div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<p>Filtra per stato</p>
|
|
<div class="cal-chips" id="cal-status-chips"></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main: toolbar + viste -->
|
|
<section class="cal-main">
|
|
<div class="cal-toolbar">
|
|
<div class="tb-left">
|
|
<button class="ctrl-today" id="tb-today" onclick="calToday()"><i data-lucide="calendar-check" class="ic"></i>Oggi</button>
|
|
<div class="tb-nav">
|
|
<button class="ctrl-btn" id="tb-prev" title="Mese precedente" aria-label="Mese precedente" onclick="calPrevMonth()"><i data-lucide="chevron-left" class="ic"></i></button>
|
|
<button class="ctrl-btn" id="tb-next" title="Mese successivo" aria-label="Mese successivo" onclick="calNextMonth()"><i data-lucide="chevron-right" class="ic"></i></button>
|
|
</div>
|
|
<span class="tb-title" id="cal-month-label">—</span>
|
|
</div>
|
|
<div class="view-switch" role="tablist" aria-label="Vista calendario">
|
|
<button data-v="grid" class="on" id="view-grid" role="tab" aria-selected="true" aria-controls="pane-grid" onclick="calSetView('grid')">Griglia</button>
|
|
<button data-v="list" id="view-list" role="tab" aria-selected="false" aria-controls="pane-list" onclick="calSetView('list')">Lista</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vista griglia mensile -->
|
|
<section id="pane-grid" class="mv" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
|
|
<div class="mv-dow"><div>Lun</div><div>Mar</div><div>Mer</div><div>Gio</div><div>Ven</div><div>Sab</div><div>Dom</div></div>
|
|
<div class="mv-grid" id="mv-grid"></div>
|
|
</section>
|
|
|
|
<!-- Vista lista -->
|
|
<section id="pane-list" class="hidden" role="tabpanel" aria-labelledby="view-list" aria-live="polite">
|
|
<div class="cal-list-card" id="cal-list-wrap"></div>
|
|
</section>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Popover eventi di un giorno (modale) -->
|
|
<div class="cal-modal" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
|
|
<div class="cal-modal-card">
|
|
<div class="cal-modal-head">
|
|
<h3 id="cal-pop-title">Eventi</h3>
|
|
<button class="cal-modal-x" type="button" aria-label="Chiudi" onclick="calPopClose()">×</button>
|
|
</div>
|
|
<div class="cal-modal-body" id="cal-pop-body"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide self-host →
|
|
topnav-v3.js (chrome, ridefinisce loadSidebar) → i18n → help. La logica del
|
|
calendario (aggregatore read-only) resta inline e usa gli stessi id/funzioni. -->
|
|
<script src="/js/api.js?v=20260625d"></script>
|
|
<script src="/js/common.js?v=20260627v3"></script>
|
|
<script src="/vendor/lucide/lucide.min.js"></script>
|
|
<script src="/js/topnav-v3.js?v=20260627n"></script>
|
|
<script src="/js/i18n.js?v=20260627n"></script>
|
|
<script src="/js/help.js?v=20260627n"></script>
|
|
<script>
|
|
'use strict';
|
|
/*
|
|
* NIS2 Agile - Calendario unico delle scadenze (Modulo C) — UI V3
|
|
* Aggregatore READ-ONLY: griglia mensile + lista + filtri (tipo/stato) + deep-link.
|
|
* Dati reali via api.calendarEvents / api.calendarSummary. Dependency-free.
|
|
* Veste fedele a docs/mockup Ui/calendario.html (.cal-app/.mv-grid/.view-switch/.legend).
|
|
*/
|
|
var CAL = {
|
|
view: 'grid',
|
|
cursor: new Date(), // mese visualizzato (giorno 1)
|
|
events: [], // tutti gli eventi della finestra caricata
|
|
rangeFrom: null, rangeTo: null,
|
|
activeTypes: null, // null = tutti
|
|
activeStatuses: null, // null = tutti
|
|
};
|
|
|
|
var TYPE_META = {
|
|
incident_early_warning: { label: 'Incidente - Early Warning', color: '#dc2626' },
|
|
incident_notification: { label: 'Incidente - Notifica CSIRT', color: '#ea580c' },
|
|
incident_final_report: { label: 'Incidente - Report finale', color: '#f59e0b' },
|
|
policy_review: { label: 'Revisione policy', color: '#2563eb' },
|
|
risk_treatment: { label: 'Trattamento rischio', color: '#7c3aed' },
|
|
control_review: { label: 'Revisione controllo', color: '#0891b2' },
|
|
nc_target_close: { label: 'Chiusura non conformita', color: '#be123c' },
|
|
capa_action: { label: 'Azione correttiva', color: '#c026d3' },
|
|
training_due: { label: 'Formazione', color: '#16a34a' },
|
|
stakeholder_activity: { label: 'Attivita stakeholder', color: '#0d9488' },
|
|
review_schedule: { label: 'Scadenziario revisioni', color: '#4f46e5' },
|
|
internal_audit: { label: 'Audit interno', color: '#9333ea' },
|
|
management_review_decision: { label: 'Decisione riesame', color: '#0369a1' },
|
|
periodic_control: { label: 'Controllo periodico', color: '#65a30d' }
|
|
};
|
|
var STATUS_META = {
|
|
overdue: { label: 'In ritardo', color: '#dc2626' },
|
|
due_soon: { label: 'In scadenza', color: '#d97706' },
|
|
upcoming: { label: 'Futura', color: '#2563eb' },
|
|
done: { label: 'Assolta', color: '#16a34a' }
|
|
};
|
|
var WEEKDAYS = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom'];
|
|
var MONTHS = ['gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'];
|
|
|
|
function el(id) { return document.getElementById(id); }
|
|
function esc(s) { var d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
|
function escAttr(s) { return esc(s).replace(/"/g, '"'); }
|
|
function ymd(d) {
|
|
var m = String(d.getMonth() + 1).padStart(2, '0');
|
|
var day = String(d.getDate()).padStart(2, '0');
|
|
return d.getFullYear() + '-' + m + '-' + day;
|
|
}
|
|
function refreshIcons() { if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} } }
|
|
|
|
document.addEventListener('DOMContentLoaded', async function () {
|
|
if (typeof checkAuth === 'function' && !checkAuth()) return;
|
|
if (typeof loadSidebar === 'function') loadSidebar(); // = loadTopnavV3()
|
|
if (window.I18n && I18n.init) I18n.init();
|
|
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
|
|
refreshIcons();
|
|
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') calPopClose(); });
|
|
var pop = el('cal-pop'); if (pop) pop.addEventListener('click', function (e) { if (e.target === pop) calPopClose(); });
|
|
CAL.cursor = new Date(); CAL.cursor.setDate(1);
|
|
renderFilters();
|
|
await calLoad();
|
|
});
|
|
|
|
// ── Caricamento dati (finestra ampia attorno al cursore) ──
|
|
async function calLoad() {
|
|
var from = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() - 2, 1);
|
|
var to = new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 3, 0);
|
|
CAL.rangeFrom = ymd(from); CAL.rangeTo = ymd(to);
|
|
el('mv-grid').innerHTML = '<div class="mc-empty"><span class="v3-loading"><span class="v3-spin"></span>Caricamento…</span></div>';
|
|
try {
|
|
var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo });
|
|
CAL.events = (data && data.events) ? data.events : [];
|
|
} catch (e) {
|
|
CAL.events = [];
|
|
el('mv-grid').innerHTML = '<div class="mc-empty">Errore nel caricamento: ' + esc(e.message || '') + '</div>';
|
|
}
|
|
await loadSummary();
|
|
render();
|
|
}
|
|
|
|
async function loadSummary() {
|
|
try {
|
|
var s = await api.calendarSummary();
|
|
var bs = (s && s.by_status) || {};
|
|
var defs = [['overdue', 'In ritardo'], ['due_soon', 'In scadenza'], ['upcoming', 'Future'], ['done', 'Assolte']];
|
|
el('cal-summary').innerHTML = defs.map(function (d) {
|
|
return '<div class="cal-kpi k-' + d[0] + '"><div class="l">' + d[1] + '</div><div class="n">' + (bs[d[0]] || 0) + '</div></div>';
|
|
}).join('');
|
|
} catch (e) { el('cal-summary').innerHTML = ''; }
|
|
}
|
|
|
|
// ── Filtri (chip checkbox per tipo e per stato) ──
|
|
function renderFilters() {
|
|
el('cal-type-chips').innerHTML = Object.keys(TYPE_META).map(function (t) {
|
|
var m = TYPE_META[t];
|
|
return '<label class="cal-chip"><input type="checkbox" class="f-type" value="' + escAttr(t) + '" checked onchange="onTypeFilter()">' +
|
|
'<span class="dot" style="background:' + m.color + '"></span><span class="lbl">' + esc(m.label) + '</span></label>';
|
|
}).join('');
|
|
el('cal-status-chips').innerHTML = Object.keys(STATUS_META).map(function (s) {
|
|
var m = STATUS_META[s];
|
|
return '<label class="cal-chip"><input type="checkbox" class="f-status" value="' + escAttr(s) + '" checked onchange="onStatusFilter()">' +
|
|
'<span class="dot" style="background:' + m.color + '"></span><span class="lbl">' + esc(m.label) + '</span></label>';
|
|
}).join('');
|
|
}
|
|
function onTypeFilter() {
|
|
var all = Array.prototype.slice.call(document.querySelectorAll('.f-type'));
|
|
var checked = all.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
|
|
CAL.activeTypes = (checked.length === all.length) ? null : checked;
|
|
render();
|
|
}
|
|
function onStatusFilter() {
|
|
var all = Array.prototype.slice.call(document.querySelectorAll('.f-status'));
|
|
var checked = all.filter(function (c) { return c.checked; }).map(function (c) { return c.value; });
|
|
CAL.activeStatuses = (checked.length === all.length) ? null : checked;
|
|
render();
|
|
}
|
|
|
|
function visibleEvents() {
|
|
return CAL.events.filter(function (e) {
|
|
if (CAL.activeTypes && CAL.activeTypes.indexOf(e.type) < 0) return false;
|
|
if (CAL.activeStatuses && CAL.activeStatuses.indexOf(e.status) < 0) return false;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// ── Navigazione mese / vista ──
|
|
function calPrevMonth() { CAL.cursor.setMonth(CAL.cursor.getMonth() - 1); maybeReload(); }
|
|
function calNextMonth() { CAL.cursor.setMonth(CAL.cursor.getMonth() + 1); maybeReload(); }
|
|
function calToday() { CAL.cursor = new Date(); CAL.cursor.setDate(1); maybeReload(); }
|
|
function maybeReload() {
|
|
// Ricarica dal server solo se il mese esce dalla finestra gia' caricata.
|
|
var first = ymd(new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth(), 1));
|
|
var last = ymd(new Date(CAL.cursor.getFullYear(), CAL.cursor.getMonth() + 1, 0));
|
|
if (!CAL.rangeFrom || first < CAL.rangeFrom || last > CAL.rangeTo) { calLoad(); }
|
|
else { render(); }
|
|
}
|
|
function calSetView(v) {
|
|
CAL.view = v;
|
|
el('view-grid').classList.toggle('on', v === 'grid');
|
|
el('view-list').classList.toggle('on', v === 'list');
|
|
el('view-grid').setAttribute('aria-selected', v === 'grid' ? 'true' : 'false');
|
|
el('view-list').setAttribute('aria-selected', v === 'list' ? 'true' : 'false');
|
|
el('pane-grid').classList.toggle('hidden', v !== 'grid');
|
|
el('pane-list').classList.toggle('hidden', v !== 'list');
|
|
render();
|
|
}
|
|
|
|
function render() {
|
|
el('cal-month-label').textContent = MONTHS[CAL.cursor.getMonth()] + ' ' + CAL.cursor.getFullYear();
|
|
if (CAL.view === 'grid') renderGrid(); else renderList();
|
|
}
|
|
|
|
// ── Vista griglia mensile (.mv-grid del mockup) ──
|
|
function renderGrid() {
|
|
var year = CAL.cursor.getFullYear(), month = CAL.cursor.getMonth();
|
|
var firstOfMonth = new Date(year, month, 1);
|
|
// settimana inizia lunedi (getDay: 0=dom)
|
|
var startOffset = (firstOfMonth.getDay() + 6) % 7;
|
|
var gridStart = new Date(year, month, 1 - startOffset);
|
|
var todayStr = ymd(new Date());
|
|
|
|
// raggruppa eventi visibili per giorno
|
|
var byDay = {};
|
|
visibleEvents().forEach(function (e) { (byDay[e.date] = byDay[e.date] || []).push(e); });
|
|
|
|
var html = '';
|
|
for (var i = 0; i < 42; i++) {
|
|
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);
|
|
var key = ymd(cur);
|
|
var inMonth = (cur.getMonth() === month);
|
|
var evs = byDay[key] || [];
|
|
var cls = 'mc' + (inMonth ? '' : ' other') + (key === todayStr ? ' today' : '') + (evs.length ? ' has' : '');
|
|
var chips = '';
|
|
evs.slice(0, 3).forEach(function (e) {
|
|
var c = (TYPE_META[e.type] || {}).color || '#64748b';
|
|
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
|
|
chips += '<div class="chip" title="' + tip + '"><span class="cdot" style="background:' + c + '"></span>' + esc(e.title) + '</div>';
|
|
});
|
|
var more = evs.length > 3 ? '<div class="more">+' + (evs.length - 3) + ' altri</div>' : '';
|
|
var click = evs.length ? ' role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" data-day="' + key + '"' : '';
|
|
html += '<div class="' + cls + '"' + click + '>' +
|
|
'<div class="dnum">' + cur.getDate() + '</div>' + chips + more + '</div>';
|
|
}
|
|
var grid = el('mv-grid');
|
|
grid.innerHTML = html;
|
|
// wiring click/keyboard sui giorni con eventi
|
|
grid.querySelectorAll('.mc.has').forEach(function (cell) {
|
|
var key = cell.getAttribute('data-day');
|
|
cell.addEventListener('click', function () { calPopDay(key); });
|
|
cell.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); calPopDay(key); } });
|
|
});
|
|
}
|
|
|
|
// ── Vista lista (overdue in cima, poi per data) ──
|
|
function renderList() {
|
|
var evs = visibleEvents().slice().sort(function (a, b) {
|
|
var oa = a.status === 'overdue' ? 0 : 1, ob = b.status === 'overdue' ? 0 : 1;
|
|
if (oa !== ob) return oa - ob;
|
|
return a.date < b.date ? -1 : (a.date > b.date ? 1 : 0);
|
|
});
|
|
if (!evs.length) { el('cal-list-wrap').innerHTML = '<div class="cal-empty">Nessuna scadenza per i filtri selezionati.</div>'; return; }
|
|
var rows = evs.map(function (e) {
|
|
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
|
|
var sm = STATUS_META[e.status] || { label: e.status };
|
|
return '<tr class="row-' + esc(e.status) + '">' +
|
|
'<td>' + esc(calFmtDate(e.date)) + '</td>' +
|
|
'<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '"><span class="cdot" style="background:' + m.color + '"></span>' + esc(m.label) + '</span></td>' +
|
|
'<td><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></td>' +
|
|
'<td><span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
el('cal-list-wrap').innerHTML =
|
|
'<table class="cal-list"><thead><tr><th>Data</th><th>Tipo</th><th>Elemento</th><th>Stato</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
|
}
|
|
|
|
function calFmtDate(s) {
|
|
var p = String(s).split('-');
|
|
if (p.length !== 3) return s;
|
|
return p[2] + '/' + p[1] + '/' + p[0];
|
|
}
|
|
|
|
// ── Popover eventi di un giorno ──
|
|
function calPopDay(dateKey) {
|
|
var evs = visibleEvents().filter(function (e) { return e.date === dateKey; });
|
|
if (!evs.length) return;
|
|
el('cal-pop-title').textContent = 'Scadenze del ' + calFmtDate(dateKey);
|
|
el('cal-pop-body').innerHTML = evs.map(function (e) {
|
|
var m = TYPE_META[e.type] || { label: e.type, color: '#64748b' };
|
|
var sm = STATUS_META[e.status] || { label: e.status };
|
|
return '<div class="cal-pop-item">' +
|
|
'<span class="cdot" style="background:' + m.color + '"></span>' +
|
|
'<div style="flex:1 1 auto;min-width:0">' +
|
|
'<div><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></div>' +
|
|
'<div style="font-size:.8rem;color:var(--muted);margin-top:3px">' + esc(m.label) +
|
|
' · <span class="st-badge st-' + esc(e.status) + '">' + esc(sm.label) + '</span></div>' +
|
|
'</div></div>';
|
|
}).join('');
|
|
el('cal-pop').classList.add('open');
|
|
}
|
|
function calPopClose() { el('cal-pop').classList.remove('open'); }
|
|
</script>
|
|
</body>
|
|
</html>
|