Files
nis2-agile/public/calendario.html
T
DevEnv nis2-agileandClaude Opus 4.8 bd1e95542c [FIX] ARIA chat: indicatore 'sta scrivendo' + input multi-riga (Shift+Invio)
Risolve #418/#420 (ARIA sembrava non rispondere / serviva riaprire il pannello):
l'attesa della risposta /api/ai/ask (RAG+Claude, alcuni secondi) non mostrava
alcun indicatore -> ora compare 'ARIA sta scrivendo...' con input disabilitato
(anti doppio-invio). Indicatore rispetta prefers-reduced-motion (AGID/WCAG).

Risolve #425: il campo di scrittura era <input> a riga singola -> ora <textarea>
con Invio=invia, Shift+Invio=a capo, auto-crescita e aria-label.

common.js e' statico (no PHP/USR2): bump cache-buster ?v=20260623 su 39 pagine
(public + admin) + version.json 1.24.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 18:13:38 +02:00

405 lines
24 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 scadenze - NIS2 Agile</title>
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
<link rel="stylesheet" href="/css/style.css?v=20260618">
<style>
.cal-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:12px; font-size:.92rem; line-height:1.6; }
.cal-note { font-size:.82rem; color:#3730a3; background:#eef2ff; border:1px solid #c7d2fe; border-radius:8px; padding:10px 12px; margin:0 0 16px; }
.cal-bar { display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between; margin-bottom:14px; }
.cal-nav { display:flex; align-items:center; gap:8px; }
.cal-nav h3 { margin:0; font-size:1.05rem; min-width:170px; text-align:center; text-transform:capitalize; }
.cal-views { display:flex; gap:6px; }
.cal-view-btn { padding:7px 14px; font-weight:600; font-size:.85rem; cursor:pointer; border:1px solid var(--gray-200,#e5e7eb); background:#fff; color:var(--gray-600,#4b5563); border-radius:8px; }
.cal-view-btn.active { background:var(--primary,#2563eb); color:#fff; border-color:var(--primary,#2563eb); }
.cal-summary { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
.cal-kpi { flex:1 1 0; min-width:120px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:10px 14px; }
.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(--gray-500,#6b7280); margin-top:4px; }
.cal-kpi.k-overdue .n { color:#b91c1c; } .cal-kpi.k-due_soon .n { color:#b45309; }
.cal-kpi.k-upcoming .n { color:#1d4ed8; } .cal-kpi.k-done .n { color:#15803d; }
.cal-filters { background:#f8fafc; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; margin-bottom:14px; }
.cal-filters h4 { margin:0 0 8px; font-size:.78rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-chips { display:flex; flex-wrap:wrap; gap:8px; }
.cal-chip { display:inline-flex; align-items:center; gap:6px; font-size:.82rem; padding:5px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:20px; background:#fff; cursor:pointer; user-select:none; }
.cal-chip input { margin:0; cursor:pointer; }
.cal-chip .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
.cal-filter-row { margin-top:10px; }
table.cal-grid { width:100%; border-collapse:collapse; table-layout:fixed; }
table.cal-grid th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); padding:6px 4px; text-align:center; }
table.cal-grid td { border:1px solid var(--gray-100,#f3f4f6); vertical-align:top; height:96px; padding:4px; background:#fff; }
table.cal-grid td.cal-out { background:#fafafa; }
table.cal-grid td.cal-today { background:#fffbeb; outline:2px solid #fbbf24; outline-offset:-2px; }
.cal-daynum { font-size:.78rem; font-weight:600; color:var(--gray-600,#4b5563); }
.cal-day-dots { display:flex; flex-wrap:wrap; gap:3px; margin-top:4px; }
.cal-dot { width:9px; height:9px; border-radius:50%; cursor:pointer; }
.cal-more { font-size:.7rem; color:var(--gray-500,#6b7280); cursor:pointer; margin-top:3px; }
.st-overdue { background:#dc2626; } .st-due_soon { background:#d97706; }
.st-upcoming { background:#2563eb; } .st-done { background:#16a34a; }
.cal-list { width:100%; border-collapse:collapse; font-size:.88rem; }
.cal-list th, .cal-list td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
.cal-list th { font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
.cal-list tr.row-overdue td { background:#fef2f2; }
.cal-list a.cal-link { color:var(--primary,#2563eb); text-decoration:none; font-weight:600; }
.cal-list a.cal-link:hover { text-decoration:underline; }
.st-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; white-space:nowrap; color:#fff; }
.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:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
.hidden { display:none !important; }
.type-tag { display:inline-block; font-size:.7rem; padding:2px 7px; border-radius:6px; background:#eef2ff; color:#3730a3; }
/* popover giorno */
.cal-pop { position:fixed; inset:0; background:rgba(15,23,42,.4); z-index:1050; display:none; align-items:flex-start; justify-content:center; padding:40px 16px; overflow:auto; }
.cal-pop.open { display:flex; }
.cal-pop-card { background:#fff; border-radius:12px; width:100%; max-width:520px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
.cal-pop-head { display:flex; justify-content:space-between; align-items:center; padding:14px 18px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-head h3 { margin:0; font-size:1rem; }
.cal-pop-body { padding:14px 18px; }
.cal-pop-item { display:flex; gap:8px; align-items:flex-start; padding:8px 0; border-top:1px solid var(--gray-100,#f3f4f6); }
.cal-pop-item:first-child { border-top:none; }
.cal-pop-close { background:none; border:none; font-size:1.4rem; line-height:1; cursor:pointer; color:var(--gray-400,#9ca3af); }
</style>
<!-- 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 -->
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="cal.title">Calendario scadenze</h2>
</header>
<div class="content-body">
<div class="cal-intro">
<strong>Tutte le scadenze in un solo posto.</strong>
Il calendario aggrega <em>in tempo reale</em> ogni scadenza del sistema — incidenti, revisioni policy e controlli,
trattamenti di rischio, non conformita e azioni correttive, formazione, attivita stakeholder, scadenziario revisioni,
audit interni e decisioni del riesame — con stato e link diretto all'elemento.
</div>
<div class="cal-note">
Ancoraggio: buona prassi di monitoraggio scadenze e revisioni periodiche (<strong>ISO 9.1/9.3</strong>,
NIS2 <strong>GV.PO-02 / GV.SC-07 / DE.CM</strong>). Strumento di supporto operativo, non un parere legale.
</div>
<!-- KPI sintetici -->
<div class="cal-summary" id="cal-summary" aria-live="polite"></div>
<!-- Barra: navigazione mese + selettore vista -->
<div class="cal-bar">
<div class="cal-nav">
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calPrevMonth()" aria-label="Mese precedente">&lsaquo;</button>
<h3 id="cal-month-label">—</h3>
<button class="btn btn-outline-primary btn-sm" type="button" onclick="calNextMonth()" aria-label="Mese successivo">&rsaquo;</button>
<button class="btn btn-outline-secondary btn-sm" type="button" onclick="calToday()">Oggi</button>
</div>
<div class="cal-views" role="tablist" aria-label="Vista calendario">
<button class="cal-view-btn active" id="view-grid" role="tab" aria-selected="true" aria-controls="pane-grid" onclick="calSetView('grid')">Griglia</button>
<button class="cal-view-btn" id="view-list" role="tab" aria-selected="false" aria-controls="pane-list" onclick="calSetView('list')">Lista</button>
</div>
</div>
<!-- Filtri -->
<div class="cal-filters">
<h4>Filtra per tipo</h4>
<div class="cal-chips" id="cal-type-chips"></div>
<div class="cal-filter-row">
<h4>Filtra per stato</h4>
<div class="cal-chips" id="cal-status-chips"></div>
</div>
</div>
<!-- Vista griglia mensile -->
<section id="pane-grid" role="tabpanel" aria-labelledby="view-grid" aria-live="polite">
<table class="cal-grid">
<thead>
<tr id="cal-weekhead"></tr>
</thead>
<tbody id="cal-grid-body"></tbody>
</table>
</section>
<!-- Vista lista -->
<section id="pane-list" class="hidden" role="tabpanel" aria-labelledby="view-list" aria-live="polite">
<div id="cal-list-wrap"></div>
</section>
</div>
</main>
</div>
<!-- Popover eventi di un giorno -->
<div class="cal-pop" id="cal-pop" role="dialog" aria-modal="true" aria-labelledby="cal-pop-title">
<div class="cal-pop-card">
<div class="cal-pop-head">
<h3 id="cal-pop-title">Eventi</h3>
<button class="cal-pop-close" type="button" aria-label="Chiudi" onclick="calPopClose()">&times;</button>
</div>
<div class="cal-pop-body" id="cal-pop-body"></div>
</div>
</div>
<script src="/js/api.js?v=20260631"></script>
<script src="/js/common.js?v=20260623"></script>
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
<script src="/js/common-bi.js?v=20260631"></script>
<script src="/js/i18n.js?v=20260631"></script>
<script src="/js/help.js?v=20260618p2"></script>
<script>
'use strict';
/*
* NIS2 Agile - Calendario unico delle scadenze (Modulo C)
* Aggregatore READ-ONLY: griglia mensile + lista + filtri (tipo/stato) + deep-link.
* Dependency-free: nessuna libreria calendario, render in JS puro.
*/
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, '&quot;'); }
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;
}
document.addEventListener('DOMContentLoaded', async function () {
if (typeof checkAuth === 'function' && !checkAuth()) return;
if (window.I18n && I18n.init) I18n.init('it');
if (typeof loadSidebar === 'function') loadSidebar();
if (window.HelpSystem && HelpSystem.init) HelpSystem.init();
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('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Caricamento…</td></tr>';
try {
var data = await api.calendarEvents({ from: CAL.rangeFrom, to: CAL.rangeTo });
CAL.events = (data && data.events) ? data.events : [];
} catch (e) {
CAL.events = [];
el('cal-grid-body').innerHTML = '<tr><td colspan="7" class="cal-empty">Errore nel caricamento: ' + esc(e.message || '') + '</td></tr>';
}
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="n">' + (bs[d[0]] || 0) + '</div><div class="l">' + d[1] + '</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>' + esc(m.label) + '</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>' + esc(m.label) + '</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('active', v === 'grid');
el('view-list').classList.toggle('active', 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 ──
function renderGrid() {
el('cal-weekhead').innerHTML = WEEKDAYS.map(function (w) { return '<th>' + w + '</th>'; }).join('');
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 w = 0; w < 6; w++) {
html += '<tr>';
for (var d = 0; d < 7; d++) {
var cur = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + (w * 7 + d));
var key = ymd(cur);
var inMonth = (cur.getMonth() === month);
var cls = inMonth ? '' : ' cal-out';
if (key === todayStr) cls += ' cal-today';
var evs = byDay[key] || [];
var dots = '';
var shown = evs.slice(0, 6);
shown.forEach(function (e) {
var c = (TYPE_META[e.type] || {}).color || '#64748b';
var tip = escAttr((TYPE_META[e.type] || { label: e.type }).label + ' — ' + e.title);
dots += '<span class="cal-dot st-' + esc(e.status) + '" style="background:' + c + '" title="' + tip + '" onclick="calPopDay(\'' + key + '\')"></span>';
});
var more = evs.length > shown.length ? '<div class="cal-more" onclick="calPopDay(\'' + key + '\')">+' + (evs.length - shown.length) + ' altri</div>' : '';
var clickDay = evs.length ? ' style="cursor:pointer" role="button" tabindex="0" aria-label="' + escAttr(cur.getDate() + ': ' + evs.length + ' scadenze') + '" onclick="calPopDay(\'' + key + '\')" onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();calPopDay(\'' + key + '\');}"' : '';
html += '<td class="' + cls + '">' +
'<div class="cal-daynum"' + clickDay + '>' + cur.getDate() + '</div>' +
(evs.length ? '<div class="cal-day-dots">' + dots + '</div>' + more : '') +
'</td>';
}
html += '</tr>';
}
el('cal-grid-body').innerHTML = html;
}
// ── 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(formatDate(e.date)) + '</td>' +
'<td><span class="type-tag" style="background:' + m.color + '22;color:' + m.color + '">' + 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 formatDate(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 ' + formatDate(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="cal-dot st-' + esc(e.status) + '" style="background:' + m.color + ';margin-top:4px;flex:0 0 auto"></span>' +
'<div style="flex:1 1 auto">' +
'<div><a class="cal-link" href="' + escAttr(e.link || '#') + '">' + esc(e.title) + '</a></div>' +
'<div style="font-size:.78rem;color:#6b7280;margin-top:2px">' + 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>