[FEAT] A4 Fasi 4.4 + 4.5 (A4 COMPLETA 5/5): scadenziario centralizzato review_schedule (mig.044; ReviewScheduleController list/create/update/delete/complete/sync) + stakeholder estesi clienti/partner GV.SC-02 (mig.045 suppliers.stakeholder_type, SupplyChainController::stakeholderMap). Pagine review-schedule.html + stakeholders.html (Bootstrap Italia/AGID). Workflow build+review adversariale (3 major risolti: 045 da DELIMITER -> ALTER bare runner-safe). help GV.SC-04/05 allineati al titolo canonico ACN. Cache-buster ?v=20260620.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c7d36c910f
commit
c5b00ccd96
@@ -70,8 +70,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della app: api.js + common.js (helper) + common-bi.js (override BI) -->
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script src="js/common-bi.js?v=20260618"></script>
|
||||
<script>
|
||||
// Renderizza la sidebar BI (loadSidebar è stato ridefinito da common-bi.js)
|
||||
|
||||
+4
-4
@@ -152,8 +152,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -164,8 +164,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
const FUNC_LABELS = {
|
||||
|
||||
@@ -317,8 +317,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -329,7 +329,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -147,8 +147,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="../js/api.js?v=20260619"></script>
|
||||
<script src="../js/common.js?v=20260619"></script>
|
||||
<script src="../js/api.js?v=20260620"></script>
|
||||
<script src="../js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -159,7 +159,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="../js/common-bi.js?v=20260618"></script>
|
||||
<script src="../js/i18n.js?v=20260619"></script>
|
||||
<script src="../js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -169,8 +169,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="../js/api.js?v=20260619"></script>
|
||||
<script src="../js/common.js?v=20260619"></script>
|
||||
<script src="../js/api.js?v=20260620"></script>
|
||||
<script src="../js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -181,7 +181,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="../js/common-bi.js?v=20260618"></script>
|
||||
<script src="../js/i18n.js?v=20260619"></script>
|
||||
<script src="../js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -1088,10 +1088,10 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/i18n.js?v=20260619"></script>
|
||||
<script src="js/help.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script src="js/i18n.js?v=20260620"></script>
|
||||
<script src="js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -154,8 +154,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -166,8 +166,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+4
-4
@@ -366,8 +366,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -378,8 +378,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -349,14 +349,14 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle + override sidebar BI (common-bi.js dopo common.js: ridefinisce solo loadSidebar) -->
|
||||
<script src="vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script>if (window.bootstrap && bootstrap.loadFonts) { bootstrap.loadFonts('vendor/bootstrap-italia/dist/fonts'); }</script>
|
||||
<script src="js/common-bi.js?v=20260618"></script>
|
||||
<script src="js/i18n.js?v=20260619"></script>
|
||||
<script src="js/help.js?v=20260619"></script>
|
||||
<script src="js/i18n.js?v=20260620"></script>
|
||||
<script src="js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
|
||||
@@ -183,12 +183,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script src="/js/competenze.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -382,8 +382,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -394,7 +394,7 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
const SPRITE = '/vendor/bootstrap-italia/dist/svg/sprites.svg';
|
||||
function ico(name, cls) { return `<svg class="ico ${cls||''}" aria-hidden="true"><use href="${SPRITE}#${name}"></use></svg>`; }
|
||||
|
||||
@@ -143,8 +143,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -155,8 +155,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della forgot-password.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script>
|
||||
const form = document.getElementById('forgot-form');
|
||||
const err = document.getElementById('err');
|
||||
|
||||
+4
-4
@@ -1142,8 +1142,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (accordion) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
@@ -1153,8 +1153,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// La guida è accessibile anche da non autenticati (utile per onboarding)
|
||||
// ma se sei loggato carichi sidebar + i18n normalmente. Stessa logica
|
||||
|
||||
@@ -351,8 +351,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -363,8 +363,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -89,6 +89,7 @@ $controllerMap = [
|
||||
'org-roles' => 'OrgRoleController', // A4 Fase 4.1 — Organigramma (ruoli/gerarchia, Art.23 governance)
|
||||
'competences' => 'CompetenceController', // A4 Fase 4.2 — Competenze (skill, requisiti ruolo, gap, alert->azione)
|
||||
'raci' => 'RaciController', // A4 Fase 4.3 — Matrice RACI + link m2m (procedure/inventario/rischi/misure)
|
||||
'review-schedule' => 'ReviewScheduleController', // A4 Fase 4.4 — Scadenziario centralizzato (revisioni periodiche GV.PO-02/GV.SC-07/PR.AT/DE.CM)
|
||||
'assessments' => 'AssessmentController',
|
||||
'acn-gap' => 'AcnAssessmentController', // Gap Analysis ACN (Det. 164179/2025, misure/requisiti)
|
||||
'isms' => 'IsmsModelController', // Modello Organizzativo SGSI (ISO 27001/27017/27018 + SoA)
|
||||
@@ -235,6 +236,19 @@ $actionMap = [
|
||||
'DELETE:link' => 'unlink',
|
||||
],
|
||||
|
||||
// ── ReviewScheduleController — Scadenziario centralizzato (A4 Fase 4.4) ──
|
||||
// Revisioni periodiche (GV.PO-02, GV.SC-07, PR.AT, DE.CM). Status calcolato live.
|
||||
// sync = upsert idempotente da policies/compliance_controls/risk_treatments (org-scoped).
|
||||
// I DELETE usano {id} numerico in URL → nessun body necessario.
|
||||
'review-schedule' => [
|
||||
'GET:list' => 'list',
|
||||
'POST:create' => 'create',
|
||||
'POST:sync' => 'sync',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
'POST:{id}/complete' => 'complete',
|
||||
],
|
||||
|
||||
// ── AssessmentController ────────────────────────
|
||||
'assessments' => [
|
||||
'GET:list' => 'list',
|
||||
@@ -375,6 +389,8 @@ $actionMap = [
|
||||
// Campagne questionario (Fase 2): cruscotto + invio template a fornitore
|
||||
'GET:campaigns' => 'campaigns',
|
||||
'POST:{id}/campaigns' => 'createCampaign', // {id} = supplier id
|
||||
// Stakeholder estesi (A4 Fase 4.5): vista di sintesi fornitori+clienti+partner (GV.SC-02). Riusa suppliers.
|
||||
'GET:stakeholderMap' => 'stakeholderMap', // URL: /api/supply-chain/stakeholder-map
|
||||
'GET:{id}' => 'get',
|
||||
'PUT:{id}' => 'update',
|
||||
'DELETE:{id}' => 'delete',
|
||||
|
||||
@@ -910,8 +910,8 @@ curl -H <span class="str">"X-API-Key: nis2_TUA_CHIAVE"</span> \
|
||||
|
||||
</main>
|
||||
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/i18n.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script src="js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
function showTab(id) {
|
||||
document.querySelectorAll('.tab-btn').forEach((b, i) => {
|
||||
|
||||
+4
-4
@@ -184,8 +184,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -196,8 +196,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script src="/js/isms.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -270,6 +270,33 @@ class NIS2API {
|
||||
raciLink(d) { return this._acn(this.post('/raci/link', d)); }
|
||||
raciUnlink(d) { return this._acn(this.del('/raci/link?link_type=' + encodeURIComponent(d.link_type) + '&a_id=' + encodeURIComponent(d.a_id) + '&b_id=' + encodeURIComponent(d.b_id))); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Scadenziario centralizzato (A4 Fase 4.4) — review_schedule. _acn.
|
||||
// Revisioni periodiche (GV.PO-02/GV.SC-07/PR.AT/DE.CM). Status calcolato live.
|
||||
// DELETE per {id} numerico (no body). sync = import idempotente da scadenze esistenti.
|
||||
// Alias rs* === rev* (stessa firma) per compatibilità di naming.
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
revList() { return this._acn(this.get('/review-schedule/list')); }
|
||||
revCreate(d) { return this._acn(this.post('/review-schedule/create', d || {})); }
|
||||
revUpdate(id, d) { return this._acn(this.put(`/review-schedule/${id}`, d)); }
|
||||
revDelete(id) { return this._acn(this.del(`/review-schedule/${id}`)); }
|
||||
revComplete(id) { return this._acn(this.post(`/review-schedule/${id}/complete`, {})); }
|
||||
revSync() { return this._acn(this.post('/review-schedule/sync', {})); }
|
||||
rsList() { return this.revList(); }
|
||||
rsCreate(d) { return this.revCreate(d); }
|
||||
rsUpdate(id, d) { return this.revUpdate(id, d); }
|
||||
rsDelete(id) { return this.revDelete(id); }
|
||||
rsComplete(id) { return this.revComplete(id); }
|
||||
rsSync() { return this.revSync(); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Stakeholder estesi (A4 Fase 4.5) — riusa suppliers (GV.SC-02).
|
||||
// stakeholderMap = vista di sintesi raggruppata (supplier/customer/partner). _acn.
|
||||
// Per aggiungere/etichettare clienti/partner riusare createSupplier/updateSupplier
|
||||
// passando `stakeholder_type` nel data (NB: NON sono _acn → gestire r.success).
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
stakeholderMap() { return this._acn(this.get('/supply-chain/stakeholder-map')); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// Dashboard
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -198,10 +198,12 @@ function loadSidebar() {
|
||||
{ name: 'Organigramma', href: 'organigramma.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M8 2.5h4a.5.5 0 01.5.5v2a.5.5 0 01-.5.5h-1.25v2.5H15a.5.5 0 01.5.5v1.25H17a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5h1.25V9.5H6v1.25H7.5a.5.5 0 01.5.5v3a.5.5 0 01-.5.5h-4a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H4.5V9.5a.5.5 0 01.5-.5h4.75V6H8a.5.5 0 01-.5-.5V3a.5.5 0 01.5-.5z"/></svg>`, i18nKey: 'nav.org_chart' },
|
||||
{ name: 'Competenze', href: 'competenze.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 11-2 0V6H3a1 1 0 010-2h1V3a1 1 0 011-1zm0 10a1 1 0 011 1v1h1a1 1 0 110 2H6v1a1 1 0 11-2 0v-1H3a1 1 0 110-2h1v-1a1 1 0 011-1zM12 2a1 1 0 01.967.744L14.146 7.2 17.5 8.134a1 1 0 010 1.732l-3.354.934-1.18 4.455a1 1 0 01-1.933 0L9.854 10.8 6.5 9.866a1 1 0 010-1.732l3.354-.934 1.179-4.456A1 1 0 0112 2z"/></svg>`, i18nKey: 'nav.competences' },
|
||||
{ name: 'Matrice RACI', href: 'raci.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zm0 6a1 1 0 011-1h5a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zm9 0a1 1 0 011-1h3a1 1 0 011 1v6a1 1 0 01-1 1h-3a1 1 0 01-1-1v-6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.raci' },
|
||||
{ name: 'Scadenziario', href: 'review-schedule.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"/></svg>`, i18nKey: 'nav.review_schedule' },
|
||||
{ name: 'Rischi', href: 'risks.html', icon: iconShieldExclamation(), i18nKey: 'nav.risks' },
|
||||
{ name: 'Incidenti', href: 'incidents.html', icon: iconBell(), i18nKey: 'nav.incidents' },
|
||||
{ name: 'Policy', href: 'policies.html', icon: iconDocumentText(), i18nKey: 'nav.policies' },
|
||||
{ name: 'Supply Chain', href: 'supply-chain.html', icon: iconLink(), i18nKey: 'nav.supply_chain' },
|
||||
{ name: 'Stakeholder', href: 'stakeholders.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"/><path d="M5.5 7.5a2 2 0 11-2.999.001A2 2 0 015.5 7.5zM17.5 7.5a2 2 0 11-2.999.001A2 2 0 0117.5 7.5z"/></svg>`, i18nKey: 'nav.stakeholders' },
|
||||
{ name: 'Segnalazioni', href: 'whistleblowing.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'Normative', href: 'normative.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/></svg>` },
|
||||
{ name: 'AI Cross-Analysis', href: 'cross-analysis.html', icon: `<svg viewBox="0 0 20 20" fill="currentColor"><path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"/></svg>` },
|
||||
|
||||
@@ -317,6 +317,90 @@ const HelpSystem = (function () {
|
||||
]
|
||||
},
|
||||
|
||||
// ─── Scadenziario centralizzato (A4 Fase 4.4) ─────────────────
|
||||
'review-schedule': {
|
||||
title: 'Guida - Scadenziario',
|
||||
intro: 'Lo Scadenziario raccoglie in un unico posto le revisioni periodiche di policy, controlli, trattamenti del rischio, ruoli, competenze, inventario, fornitori e misure. Per ciascuna voce indica chi è il responsabile (ruolo), ogni quanto va rivista e quando scade la prossima revisione, segnalandone lo stato. È uno strumento di supporto organizzativo, non un parere legale.',
|
||||
sections: [
|
||||
{
|
||||
heading: 'Stati e badge',
|
||||
items: [
|
||||
'<strong>OK (verde)</strong>: la prossima revisione è oltre i 30 giorni.',
|
||||
'<strong>In scadenza (arancione)</strong>: la prossima revisione cade entro 30 giorni.',
|
||||
'<strong>Scaduto (rosso)</strong>: la data di prossima revisione è già passata.',
|
||||
'Lo stato è ricalcolato automaticamente in base alla data odierna: non serve aggiornarlo a mano.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Come si usa',
|
||||
items: [
|
||||
'<strong>Nuova scadenza</strong>: crei una voce indicando titolo, tipo di entità, ruolo responsabile, frequenza (in mesi) e data della prossima revisione.',
|
||||
'<strong>Segna revisionato</strong>: registra la revisione di oggi; se è impostata una frequenza, la prossima data avanza automaticamente.',
|
||||
'<strong>Sincronizza scadenze esistenti</strong>: importa (senza duplicare) le scadenze già presenti nel sistema — riesami delle policy, dei controlli e le date dei trattamenti del rischio della tua organizzazione.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Riferimenti normativi',
|
||||
items: [
|
||||
'<strong>GV.PO-02</strong>: le politiche di cybersicurezza sono riviste periodicamente e aggiornate.',
|
||||
'<strong>GV.SC-07</strong>: i rischi e i rapporti con i fornitori sono monitorati e rivisti nel tempo.',
|
||||
'<strong>PR.AT</strong>: la formazione e consapevolezza è mantenuta e ripetuta nel tempo.',
|
||||
'<strong>DE.CM</strong>: il monitoraggio continuo presuppone controlli rivisti con regolarità.',
|
||||
'In Italia gli obblighi fanno capo al <strong>D.Lgs. 138/2024</strong> (in particolare l\'art. 24 sulla gestione del rischio). La periodicità puntuale, dove non fissata dalla norma, è buona prassi.'
|
||||
]
|
||||
}
|
||||
],
|
||||
references: [
|
||||
'NIST CSF 2.0 / GV.PO-02 - Politiche riviste e aggiornate periodicamente',
|
||||
'NIST CSF 2.0 / GV.SC-07 - Monitoraggio e revisione dei fornitori',
|
||||
'NIST CSF 2.0 / PR.AT - Formazione e consapevolezza mantenute nel tempo',
|
||||
'NIST CSF 2.0 / DE.CM - Monitoraggio continuo',
|
||||
'Obblighi: art. 24 D.Lgs. 138/2024 (gestione del rischio)',
|
||||
'NOTA: gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024; lo scadenziario e i framework NIST CSF/ACN sono strumenti di supporto. La periodicità puntuale, dove non fissata, è buona prassi, non un parere legale.'
|
||||
]
|
||||
},
|
||||
|
||||
// ─── Stakeholder estesi (A4 Fase 4.5) ─────────────────────────
|
||||
'stakeholders': {
|
||||
title: 'Guida - Stakeholder',
|
||||
intro: 'La Mappa Stakeholder è una vista di sintesi delle parti interessate dell\'organizzazione, raggruppate in fornitori, clienti e partner. Riusa i dati del modulo Supply Chain (nessuna duplicazione): il dettaglio e la valutazione dei fornitori restano lì, mentre qui puoi avere il quadro d\'insieme e aggiungere o etichettare clienti e partner. È uno strumento di supporto organizzativo, non un parere legale.',
|
||||
sections: [
|
||||
{
|
||||
heading: 'I tre gruppi',
|
||||
items: [
|
||||
'<strong>Fornitori</strong>: chi fornisce beni o servizi (inclusi i fornitori ICT). Il loro dettaglio e la valutazione di sicurezza si gestiscono nel modulo Supply Chain.',
|
||||
'<strong>Clienti</strong>: le parti a cui l\'organizzazione eroga servizi rilevanti per la continuità.',
|
||||
'<strong>Partner</strong>: soggetti con cui esistono accordi o collaborazioni rilevanti per la sicurezza.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Come si usa',
|
||||
items: [
|
||||
'<strong>Aggiungi stakeholder</strong>: registri nome, tipo (fornitore/cliente/partner), tipo di servizio o relazione, contatto e criticità.',
|
||||
'Per i fornitori, usa il modulo <strong>Supply Chain</strong> per il dettaglio completo, i questionari e la valutazione del rischio (qui niente duplicati).',
|
||||
'La vista raggruppata aiuta a documentare ruoli e responsabilità verso tutte le parti interessate, non solo i fornitori.'
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: 'Riferimenti normativi',
|
||||
items: [
|
||||
'<strong>GV.SC-02</strong>: ruoli e responsabilità per la sicurezza della catena di fornitura sono stabiliti verso fornitori, <strong>clienti</strong> e <strong>partner</strong>.',
|
||||
'<strong>GV.SC-04</strong>: i fornitori sono noti e prioritizzati in base alla criticità (l\'estensione a clienti e partner è coperta da GV.SC-02).',
|
||||
'<strong>GV.SC-05</strong>: i requisiti di sicurezza sono integrati nei contratti e negli accordi con i fornitori e le altre terze parti rilevanti.',
|
||||
'<strong>GV.SC-07</strong>: i rischi e i rapporti con le parti interessate sono monitorati e rivisti nel tempo.'
|
||||
]
|
||||
}
|
||||
],
|
||||
references: [
|
||||
'NIST CSF 2.0 / GV.SC-02 - Ruoli e responsabilità verso fornitori, clienti e partner',
|
||||
'NIST CSF 2.0 / GV.SC-04 - Fornitori noti e prioritizzati per criticità (clienti/partner: GV.SC-02)',
|
||||
'NIST CSF 2.0 / GV.SC-05 - Requisiti di sicurezza negli accordi',
|
||||
'NIST CSF 2.0 / GV.SC-07 - Monitoraggio e revisione delle parti interessate',
|
||||
'Obblighi: la gestione della supply chain è prevista dall\'art. 24 del D.Lgs. 138/2024.',
|
||||
'NOTA: gli obblighi normativi in Italia derivano dal D.Lgs. 138/2024; la mappa stakeholder e i framework NIST CSF/ACN sono strumenti di supporto, non un parere legale.'
|
||||
]
|
||||
},
|
||||
|
||||
// ─── Risk Management ─────────────────────────────────────────
|
||||
'risks': {
|
||||
title: 'Guida - Gestione Rischi',
|
||||
@@ -1161,6 +1245,10 @@ const HelpSystem = (function () {
|
||||
'competenze': 'competences',
|
||||
'raci.html': 'raci',
|
||||
'raci': 'raci',
|
||||
'review-schedule.html': 'review-schedule',
|
||||
'review-schedule': 'review-schedule',
|
||||
'stakeholders.html': 'stakeholders',
|
||||
'stakeholders': 'stakeholders',
|
||||
'risks.html': 'risks',
|
||||
'risks': 'risks',
|
||||
'incidents.html': 'incidents',
|
||||
|
||||
@@ -86,6 +86,10 @@ const I18n = (function () {
|
||||
'org.new_role': { it: 'Nuovo ruolo', en: 'New role' },
|
||||
'comp.title': { it: 'Competenze', en: 'Skills' },
|
||||
'raci.title': { it: 'Matrice RACI', en: 'RACI Matrix' },
|
||||
'nav.review_schedule': { it: 'Scadenziario', en: 'Review schedule' },
|
||||
'nav.stakeholders': { it: 'Stakeholder', en: 'Stakeholders' },
|
||||
'rev.title': { it: 'Scadenziario', en: 'Review Schedule' },
|
||||
'stk.title': { it: 'Stakeholder', en: 'Stakeholders' },
|
||||
'nav.risks': { it: 'Rischi', en: 'Risks' },
|
||||
'nav.incidents': { it: 'Incidenti', en: 'Incidents' },
|
||||
'nav.policies': { it: 'Policy', en: 'Policies' },
|
||||
|
||||
+4
-4
@@ -151,8 +151,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Stessa logica JS della pagina live (parita' funzionale assoluta, zero backend). -->
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI. -->
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
@@ -162,8 +162,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script src="/js/kb.js"></script>
|
||||
<script>
|
||||
// Gate auth + chrome come le altre pagine -bi.
|
||||
|
||||
@@ -515,7 +515,7 @@ body { background: var(--bg-main); }
|
||||
|
||||
<div id="toast"></div>
|
||||
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script>
|
||||
// ══════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
|
||||
+2
-2
@@ -94,8 +94,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della login attuale: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
@@ -424,6 +424,6 @@ curl https://nis2.agile.software/api/services/status</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -124,8 +124,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -494,8 +494,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della onboarding.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ──────────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -138,12 +138,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script src="/js/organigramma.js?v=20260617"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -333,8 +333,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -345,8 +345,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+4
-4
@@ -131,12 +131,12 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script src="/js/raci.js?v=20260619"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -268,8 +268,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Stessa logica della register.html: ZERO modifiche backend -->
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script>
|
||||
if (api.isAuthenticated()) { window.location.href = 'dashboard.html'; }
|
||||
|
||||
|
||||
+4
-4
@@ -443,8 +443,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -455,8 +455,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Scadenziario - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260617">
|
||||
<style>
|
||||
.rev-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
|
||||
.rev-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||
.rev-stats { display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px; }
|
||||
.rev-stat { flex:1 1 150px; min-width:130px; background:#fff; border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; padding:12px 14px; }
|
||||
.rev-stat .n { font-size:1.5rem; font-weight:800; line-height:1; }
|
||||
.rev-stat .l { font-size:.78rem; color:var(--gray-500,#6b7280); margin-top:4px; }
|
||||
.rev-stat.s-overdue .n { color:#991b1b; }
|
||||
.rev-stat.s-due .n { color:#b45309; }
|
||||
.rev-stat.s-ok .n { color:#166534; }
|
||||
.rev-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.rev-toolbar select { padding:8px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.88rem; }
|
||||
.rev-toolbar .grow { flex:1 1 auto; }
|
||||
.rev-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.rev-table th, .rev-table td { text-align:left; padding:9px 10px; border-bottom:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||||
.rev-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.rev-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.rev-badge.type { background:#f1f5f9; color:#475569; }
|
||||
.gap-pill { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 9px; border-radius:20px; }
|
||||
.gap-pill.ok { background:#dcfce7; color:#166534; }
|
||||
.gap-pill.due { background:#fef3c7; color:#92400e; }
|
||||
.gap-pill.bad { background:#fee2e2; color:#991b1b; }
|
||||
.rev-actions-cell { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.rev-actions-cell .btn, .rev-toolbar .btn { padding:4px 9px; font-size:.76rem; }
|
||||
.rev-empty { text-align:center; padding:34px 16px; color:var(--gray-500,#6b7280); }
|
||||
/* Modale */
|
||||
.rm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||||
.rm-overlay.open { display:flex; }
|
||||
.rm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||||
.rm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.rm-head h3 { margin:0; font-size:1.05rem; }
|
||||
.rm-body { padding:18px 20px; }
|
||||
.rm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.rm-field { margin-bottom:14px; }
|
||||
.rm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.rm-field input[type=text], .rm-field input[type=number], .rm-field input[type=date], .rm-field select, .rm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||||
.rm-field textarea { min-height:64px; resize:vertical; }
|
||||
.rm-row { display:flex; gap:12px; }
|
||||
.rm-row > .rm-field { flex:1 1 0; }
|
||||
.rm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.rm-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="rev.title">Scadenziario</h2>
|
||||
<div class="content-header-actions">
|
||||
<span class="savehint" id="rev-hint" role="status" aria-live="polite"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<div class="rev-intro">
|
||||
<strong>Revisioni e scadenze in un unico posto.</strong>
|
||||
Tieni traccia delle revisioni periodiche di ruoli, competenze, inventario, procedure, rischi, fornitori e misure. Ogni voce mostra quando è prevista la prossima revisione e ti avvisa quando è in scadenza o scaduta.
|
||||
</div>
|
||||
<div class="rev-note">
|
||||
Ancoraggio: <strong>GV.PO-02</strong> (riesame periodico delle policy), <strong>GV.SC-07</strong> (revisione dei fornitori), <strong>PR.AT</strong>, <strong>DE.CM</strong> (monitoraggio continuo). Gli obblighi di gestione del rischio fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. La periodicità puntuale è una buona prassi dove non fissata da norma o contratto. Strumento di supporto, non un parere legale.
|
||||
</div>
|
||||
|
||||
<div class="rev-stats" id="rev-stats" style="display:none;">
|
||||
<div class="rev-stat"><div class="n" id="rev-stat-total">0</div><div class="l">Voci totali</div></div>
|
||||
<div class="rev-stat s-overdue"><div class="n" id="rev-stat-overdue">0</div><div class="l">Scadute</div></div>
|
||||
<div class="rev-stat s-due"><div class="n" id="rev-stat-due">0</div><div class="l">In scadenza (30 gg)</div></div>
|
||||
<div class="rev-stat s-ok"><div class="n" id="rev-stat-ok">0</div><div class="l">In regola</div></div>
|
||||
</div>
|
||||
|
||||
<div class="rev-toolbar">
|
||||
<button class="btn btn-primary btn-sm" onclick="revOpen()">+ Nuova scadenza</button>
|
||||
<button class="btn btn-outline btn-sm" id="rev-sync-btn" onclick="revDoSync()">Sincronizza scadenze esistenti</button>
|
||||
<span class="grow"></span>
|
||||
<label for="rev-filter" style="font-weight:600;font-size:.82rem;">Stato:</label>
|
||||
<select id="rev-filter" onchange="revRender()">
|
||||
<option value="all">Tutti</option>
|
||||
<option value="overdue">Scadute</option>
|
||||
<option value="due">In scadenza</option>
|
||||
<option value="ok">In regola</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="rev-list" aria-live="polite"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modale scadenza (crea/modifica) -->
|
||||
<div class="rm-overlay" id="rev-modal" role="dialog" aria-modal="true" aria-labelledby="rev-modal-title">
|
||||
<div class="rm-dialog">
|
||||
<div class="rm-head"><h3 id="rev-modal-title">Nuova scadenza</h3><button class="rm-close" type="button" onclick="revClose()" aria-label="Chiudi">×</button></div>
|
||||
<div class="rm-body">
|
||||
<input type="hidden" id="rev-id">
|
||||
<div class="rm-field"><label for="rev-f-title">Titolo *</label><input type="text" id="rev-f-title" maxlength="255" placeholder="es. Riesame annuale della policy di backup"></div>
|
||||
<div class="rm-row">
|
||||
<div class="rm-field"><label for="rev-f-type">Tipo entità</label>
|
||||
<select id="rev-f-type">
|
||||
<option value="custom">Personalizzata</option>
|
||||
<option value="role">Ruolo</option>
|
||||
<option value="skill">Competenza</option>
|
||||
<option value="inventory">Inventario</option>
|
||||
<option value="procedure">Procedura</option>
|
||||
<option value="risk">Rischio</option>
|
||||
<option value="supplier">Fornitore</option>
|
||||
<option value="measure">Misura</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="rm-field"><label for="rev-f-owner">Owner (ruolo)</label><select id="rev-f-owner"><option value="">— nessuno —</option></select></div>
|
||||
</div>
|
||||
<div class="rm-row">
|
||||
<div class="rm-field"><label for="rev-f-next">Prossima revisione *</label><input type="date" id="rev-f-next"></div>
|
||||
<div class="rm-field"><label for="rev-f-freq">Frequenza (mesi)</label><input type="number" id="rev-f-freq" min="1" max="120" placeholder="es. 12"></div>
|
||||
</div>
|
||||
<div class="rm-field"><label for="rev-f-notes">Note</label><textarea id="rev-f-notes" maxlength="2000"></textarea></div>
|
||||
<div class="rm-err" id="rev-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="rm-foot"><button class="btn btn-outline" type="button" onclick="revClose()">Annulla</button><button class="btn btn-primary" type="button" id="rev-save" onclick="revSave()">Salva</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
* NIS2 Agile - Scadenziario centralizzato (A4 Fase 4.4)
|
||||
* review_schedule: revisioni periodiche di ruoli/competenze/inventario/procedure/rischi/fornitori/misure.
|
||||
* Client api.rev* -> _acn (ritorna data, lancia su success=false).
|
||||
* Ancoraggio GV.PO-02, GV.SC-07, PR.AT, DE.CM. Obblighi: art. 24 D.Lgs. 138/2024.
|
||||
*/
|
||||
let REV = { items: [], roles: [] };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
|
||||
const TYPE_LABELS = {
|
||||
custom: 'Personalizzata', role: 'Ruolo', skill: 'Competenza', inventory: 'Inventario',
|
||||
procedure: 'Procedura', risk: 'Rischio', supplier: 'Fornitore', measure: 'Misura'
|
||||
};
|
||||
const STATUS_META = {
|
||||
ok: { cls: 'ok', label: 'In regola' },
|
||||
due: { cls: 'due', label: 'In scadenza' },
|
||||
overdue: { cls: 'bad', label: 'Scaduta' }
|
||||
};
|
||||
function typeLabel(t) { return TYPE_LABELS[t] || (t || '—'); }
|
||||
function fmtDate(d) {
|
||||
if (!d) return '—';
|
||||
try { return new Date(d + 'T00:00:00').toLocaleDateString('it-IT'); } catch (e) { return esc(d); }
|
||||
}
|
||||
|
||||
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') revClose(); });
|
||||
const ov = el('rev-modal');
|
||||
if (ov) ov.addEventListener('click', function (e) { if (e.target === ov) revClose(); });
|
||||
|
||||
await revLoadShared();
|
||||
await revLoad();
|
||||
});
|
||||
|
||||
async function revLoadShared() {
|
||||
try {
|
||||
const rolesData = await api.orgRolesList();
|
||||
REV.roles = (rolesData && rolesData.roles) || [];
|
||||
revFillOwnerSelect();
|
||||
} catch (e) {
|
||||
// l'organigramma può non essere ancora popolato: non bloccare la pagina
|
||||
REV.roles = [];
|
||||
}
|
||||
}
|
||||
|
||||
function revFillOwnerSelect() {
|
||||
const sel = el('rev-f-owner');
|
||||
let h = '<option value="">— nessuno —</option>';
|
||||
REV.roles.forEach(function (r) {
|
||||
h += '<option value="' + r.id + '">' + esc(r.role_name || r.name || ('Ruolo #' + r.id)) + '</option>';
|
||||
});
|
||||
sel.innerHTML = h;
|
||||
}
|
||||
|
||||
async function revLoad() {
|
||||
el('rev-list').innerHTML = '<div class="rev-empty">Caricamento…</div>';
|
||||
try {
|
||||
const data = await api.revList();
|
||||
REV.items = (data && data.items) || [];
|
||||
revRenderStats(data);
|
||||
revRender();
|
||||
} catch (e) {
|
||||
el('rev-list').innerHTML = '<div class="rev-empty">Errore nel caricamento: ' + esc(e.message || e) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function revRenderStats(data) {
|
||||
const total = (data && data.total != null) ? data.total : REV.items.length;
|
||||
const overdue = (data && data.overdue != null) ? data.overdue : REV.items.filter(i => i.status === 'overdue').length;
|
||||
const due = (data && data.due != null) ? data.due : REV.items.filter(i => i.status === 'due').length;
|
||||
const ok = (data && data.ok != null) ? data.ok : REV.items.filter(i => i.status === 'ok').length;
|
||||
el('rev-stat-total').textContent = total;
|
||||
el('rev-stat-overdue').textContent = overdue;
|
||||
el('rev-stat-due').textContent = due;
|
||||
el('rev-stat-ok').textContent = ok;
|
||||
el('rev-stats').style.display = total ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function revRender() {
|
||||
const filter = el('rev-filter').value;
|
||||
const rows = REV.items.filter(i => filter === 'all' || i.status === filter);
|
||||
if (!REV.items.length) {
|
||||
el('rev-list').innerHTML = '<div class="rev-empty">Nessuna scadenza registrata. Aggiungine una con "+ Nuova scadenza" oppure importa quelle esistenti con "Sincronizza scadenze esistenti".</div>';
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
el('rev-list').innerHTML = '<div class="rev-empty">Nessuna voce per il filtro selezionato.</div>';
|
||||
return;
|
||||
}
|
||||
const body = rows.map(function (i) {
|
||||
const meta = STATUS_META[i.status] || STATUS_META.ok;
|
||||
const owner = i.owner_role_name ? esc(i.owner_role_name) : '—';
|
||||
const freq = (i.frequency_months != null && i.frequency_months !== '') ? (esc(i.frequency_months) + ' mesi') : '';
|
||||
return '<tr>'
|
||||
+ '<td><strong>' + esc(i.title) + '</strong>'
|
||||
+ (i.notes ? '<div class="gap-role-sub" style="font-size:.8rem;color:var(--gray-500,#6b7280);margin-top:3px;">' + esc(i.notes) + '</div>' : '') + '</td>'
|
||||
+ '<td><span class="rev-badge type">' + esc(typeLabel(i.entity_type)) + '</span></td>'
|
||||
+ '<td>' + owner + '</td>'
|
||||
+ '<td>' + fmtDate(i.last_reviewed_at) + '</td>'
|
||||
+ '<td>' + fmtDate(i.next_review_date) + (freq ? '<div style="font-size:.78rem;color:var(--gray-500,#6b7280);">ogni ' + freq + '</div>' : '') + '</td>'
|
||||
+ '<td><span class="gap-pill ' + meta.cls + '">' + meta.label + '</span></td>'
|
||||
+ '<td><div class="rev-actions-cell">'
|
||||
+ '<button class="btn btn-outline" onclick="revComplete(' + i.id + ')">Segna revisionato</button>'
|
||||
+ '<button class="btn btn-outline" onclick="revOpen(' + i.id + ')">Modifica</button>'
|
||||
+ '<button class="btn btn-outline" onclick="revDelete(' + i.id + ')">Elimina</button>'
|
||||
+ '</div></td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
el('rev-list').innerHTML = '<table class="rev-table"><thead><tr>'
|
||||
+ '<th>Titolo</th><th>Tipo</th><th>Owner</th><th>Ultima revisione</th><th>Prossima revisione</th><th>Stato</th><th>Azioni</th>'
|
||||
+ '</tr></thead><tbody>' + body + '</tbody></table>';
|
||||
}
|
||||
|
||||
function revOpen(id) {
|
||||
const i = id ? REV.items.find(x => x.id === id) : null;
|
||||
el('rev-modal-title').textContent = i ? 'Modifica scadenza' : 'Nuova scadenza';
|
||||
el('rev-id').value = i ? i.id : '';
|
||||
el('rev-f-title').value = i ? (i.title || '') : '';
|
||||
el('rev-f-type').value = i ? (i.entity_type || 'custom') : 'custom';
|
||||
el('rev-f-owner').value = i && i.owner_role_id != null ? String(i.owner_role_id) : '';
|
||||
el('rev-f-next').value = i ? (i.next_review_date || '') : '';
|
||||
el('rev-f-freq').value = i && i.frequency_months != null ? i.frequency_months : '';
|
||||
el('rev-f-notes').value = i ? (i.notes || '') : '';
|
||||
el('rev-err').textContent = '';
|
||||
el('rev-modal').classList.add('open');
|
||||
setTimeout(() => el('rev-f-title').focus(), 50);
|
||||
}
|
||||
|
||||
function revClose() { el('rev-modal').classList.remove('open'); }
|
||||
|
||||
async function revSave() {
|
||||
const id = el('rev-id').value;
|
||||
const title = el('rev-f-title').value.trim();
|
||||
const next = el('rev-f-next').value;
|
||||
if (!title) { el('rev-err').textContent = 'Il titolo è obbligatorio.'; return; }
|
||||
if (!next) { el('rev-err').textContent = 'La data di prossima revisione è obbligatoria.'; return; }
|
||||
const freqRaw = el('rev-f-freq').value.trim();
|
||||
const ownerRaw = el('rev-f-owner').value;
|
||||
const payload = {
|
||||
title: title,
|
||||
entity_type: el('rev-f-type').value,
|
||||
owner_role_id: ownerRaw ? parseInt(ownerRaw, 10) : null,
|
||||
frequency_months: freqRaw ? parseInt(freqRaw, 10) : null,
|
||||
next_review_date: next,
|
||||
notes: el('rev-f-notes').value.trim()
|
||||
};
|
||||
const btn = el('rev-save'); btn.disabled = true;
|
||||
try {
|
||||
if (id) { await api.revUpdate(parseInt(id, 10), payload); showNotification('Scadenza aggiornata.', 'success'); }
|
||||
else { await api.revCreate(payload); showNotification('Scadenza creata.', 'success'); }
|
||||
revClose();
|
||||
await revLoad();
|
||||
} catch (e) {
|
||||
el('rev-err').textContent = e.message || 'Errore nel salvataggio.';
|
||||
} finally { btn.disabled = false; }
|
||||
}
|
||||
|
||||
async function revComplete(id) {
|
||||
try {
|
||||
await api.revComplete(id);
|
||||
showNotification('Revisione registrata.', 'success');
|
||||
await revLoad();
|
||||
} catch (e) { showNotification('Errore: ' + (e.message || e), 'error'); }
|
||||
}
|
||||
|
||||
async function revDelete(id) {
|
||||
const i = REV.items.find(x => x.id === id);
|
||||
if (!confirm('Eliminare la scadenza "' + (i ? i.title : '') + '"?')) return;
|
||||
try {
|
||||
await api.revDelete(id);
|
||||
showNotification('Scadenza eliminata.', 'success');
|
||||
await revLoad();
|
||||
} catch (e) { showNotification('Errore: ' + (e.message || e), 'error'); }
|
||||
}
|
||||
|
||||
async function revDoSync() {
|
||||
const btn = el('rev-sync-btn'); btn.disabled = true;
|
||||
try {
|
||||
const r = await api.revSync();
|
||||
const created = (r && r.created != null) ? r.created : 0;
|
||||
const updated = (r && r.updated != null) ? r.updated : 0;
|
||||
const skipped = (r && r.skipped != null) ? r.skipped : 0;
|
||||
showNotification('Sincronizzazione completata: ' + created + ' aggiunte, ' + updated + ' aggiornate, ' + skipped + ' già presenti.', 'success');
|
||||
await revLoad();
|
||||
} catch (e) {
|
||||
showNotification('Errore nella sincronizzazione: ' + (e.message || e), 'error');
|
||||
} finally { btn.disabled = false; }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+4
-4
@@ -494,8 +494,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -506,8 +506,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ──────────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -672,8 +672,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -684,8 +684,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth check ───────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Stakeholder - NIS2 Agile</title>
|
||||
<!-- Bootstrap Italia v2.18.1 self-hostato (regola: MAI CDN). Caricato PRIMA di style.css. -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css">
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260617">
|
||||
<style>
|
||||
.stk-intro { background:#eff6ff; border-left:4px solid var(--primary,#2563eb); padding:14px 18px; border-radius:8px; margin-bottom:16px; font-size:.92rem; line-height:1.6; }
|
||||
.stk-note { font-size:.82rem; color:#92400e; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; padding:10px 12px; margin:0 0 18px; }
|
||||
.stk-toolbar { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-bottom:14px; }
|
||||
.stk-toolbar .grow { flex:1 1 auto; }
|
||||
.stk-toolbar .btn { padding:4px 12px; font-size:.8rem; }
|
||||
.stk-group { border:1px solid var(--gray-200,#e5e7eb); border-radius:10px; margin-bottom:14px; overflow:hidden; }
|
||||
.stk-group-head { background:var(--gray-50,#f9fafb); padding:11px 14px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:6px; }
|
||||
.stk-group-title { font-weight:700; font-size:.98rem; }
|
||||
.stk-count { display:inline-block; font-size:.72rem; font-weight:800; padding:2px 10px; border-radius:20px; background:#e0f2fe; color:#075985; margin-left:8px; }
|
||||
.stk-table { width:100%; border-collapse:collapse; font-size:.88rem; }
|
||||
.stk-table th, .stk-table td { text-align:left; padding:9px 12px; border-top:1px solid var(--gray-100,#f3f4f6); vertical-align:top; }
|
||||
.stk-table th { font-size:.74rem; text-transform:uppercase; letter-spacing:.04em; color:var(--gray-500,#6b7280); }
|
||||
.stk-badge { display:inline-block; font-size:.7rem; font-weight:700; padding:2px 8px; border-radius:6px; }
|
||||
.stk-badge.crit-low { background:#dcfce7; color:#166534; }
|
||||
.stk-badge.crit-medium { background:#fef3c7; color:#92400e; }
|
||||
.stk-badge.crit-high { background:#ffedd5; color:#9a3412; }
|
||||
.stk-badge.crit-critical { background:#fee2e2; color:#991b1b; }
|
||||
.stk-badge.st { background:#f1f5f9; color:#475569; }
|
||||
.stk-empty { text-align:center; padding:22px 16px; color:var(--gray-500,#6b7280); font-size:.86rem; }
|
||||
.stk-link { font-size:.78rem; }
|
||||
/* Modale */
|
||||
.sm-overlay { display:none; position:fixed; inset:0; background:rgba(15,23,42,.5); z-index:1050; align-items:flex-start; justify-content:center; overflow:auto; padding:40px 16px; }
|
||||
.sm-overlay.open { display:flex; }
|
||||
.sm-dialog { background:#fff; border-radius:12px; width:100%; max-width:560px; box-shadow:0 20px 50px rgba(0,0,0,.25); }
|
||||
.sm-head { display:flex; justify-content:space-between; align-items:center; padding:16px 20px; border-bottom:1px solid var(--gray-100,#f3f4f6); }
|
||||
.sm-head h3 { margin:0; font-size:1.05rem; }
|
||||
.sm-body { padding:18px 20px; }
|
||||
.sm-foot { padding:14px 20px; border-top:1px solid var(--gray-100,#f3f4f6); display:flex; justify-content:flex-end; gap:10px; }
|
||||
.sm-field { margin-bottom:14px; }
|
||||
.sm-field label { display:block; font-weight:600; font-size:.86rem; margin-bottom:4px; }
|
||||
.sm-field input[type=text], .sm-field input[type=email], .sm-field select, .sm-field textarea { width:100%; padding:9px 10px; border:1px solid var(--gray-200,#e5e7eb); border-radius:8px; font-size:.9rem; }
|
||||
.sm-field textarea { min-height:64px; resize:vertical; }
|
||||
.sm-row { display:flex; gap:12px; }
|
||||
.sm-row > .sm-field { flex:1 1 0; }
|
||||
.sm-err { color:#b91c1c; font-size:.82rem; min-height:18px; }
|
||||
.sm-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="stk.title">Stakeholder</h2>
|
||||
<div class="content-header-actions">
|
||||
<button class="btn btn-primary btn-sm" onclick="stkOpen()">+ Aggiungi stakeholder</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content-body">
|
||||
<div class="stk-intro">
|
||||
<strong>Mappa degli stakeholder.</strong>
|
||||
Vista di sintesi di fornitori, clienti e partner della tua organizzazione, raggruppati per tipo. Riusa i dati della Supply Chain: il dettaglio e la valutazione dei fornitori restano nel modulo dedicato.
|
||||
</div>
|
||||
<div class="stk-note">
|
||||
Ancoraggio: <strong>GV.SC-02</strong> (ruoli e responsabilità definiti verso fornitori, clienti e partner) con <strong>GV.SC-04/05/07</strong>. Gli obblighi di gestione del rischio della catena di fornitura fanno capo all'<strong>art. 24 D.Lgs. 138/2024</strong>. Strumento di supporto, non un parere legale.
|
||||
</div>
|
||||
|
||||
<div class="stk-toolbar">
|
||||
<span class="grow"></span>
|
||||
<a class="btn btn-outline btn-sm" href="supply-chain.html" data-i18n="stk.manage_suppliers">Gestisci fornitori in Supply Chain</a>
|
||||
</div>
|
||||
|
||||
<div id="stk-groups" aria-live="polite"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modale stakeholder (crea/modifica) -->
|
||||
<div class="sm-overlay" id="stk-modal" role="dialog" aria-modal="true" aria-labelledby="stk-modal-title">
|
||||
<div class="sm-dialog">
|
||||
<div class="sm-head"><h3 id="stk-modal-title">Aggiungi stakeholder</h3><button class="sm-close" type="button" onclick="stkClose()" aria-label="Chiudi">×</button></div>
|
||||
<div class="sm-body">
|
||||
<input type="hidden" id="stk-id">
|
||||
<div class="sm-field"><label for="stk-f-name">Nome *</label><input type="text" id="stk-f-name" maxlength="255" placeholder="es. Acme Cloud S.p.A."></div>
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="stk-f-type">Tipo</label>
|
||||
<select id="stk-f-type" onchange="stkOnTypeChange()">
|
||||
<option value="supplier">Fornitore</option>
|
||||
<option value="customer">Cliente</option>
|
||||
<option value="partner">Partner</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm-field"><label for="stk-f-service">Servizio / relazione *</label><input type="text" id="stk-f-service" maxlength="255" placeholder="es. Hosting, consulenza, contratto quadro"></div>
|
||||
</div>
|
||||
<div class="sm-row">
|
||||
<div class="sm-field"><label for="stk-f-crit">Criticità</label>
|
||||
<select id="stk-f-crit">
|
||||
<option value="low">Bassa</option>
|
||||
<option value="medium" selected>Media</option>
|
||||
<option value="high">Alta</option>
|
||||
<option value="critical">Critica</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="sm-field"><label for="stk-f-contact-name">Nome contatto</label><input type="text" id="stk-f-contact-name" maxlength="255"></div>
|
||||
</div>
|
||||
<div class="sm-field"><label for="stk-f-contact-email">Email contatto</label><input type="email" id="stk-f-contact-email" maxlength="255" placeholder="referente@esempio.it"></div>
|
||||
<div class="sm-err" id="stk-err" role="alert"></div>
|
||||
</div>
|
||||
<div class="sm-foot"><button class="btn btn-outline" type="button" onclick="stkClose()">Annulla</button><button class="btn btn-primary" type="button" id="stk-save" onclick="stkSave()">Salva</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<script src="/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js"></script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
'use strict';
|
||||
/*
|
||||
* NIS2 Agile - Mappa stakeholder (A4 Fase 4.5)
|
||||
* Vista di sintesi fornitori/clienti/partner: riusa la tabella suppliers (stakeholder_type).
|
||||
* Lettura via api.stakeholderMap() (_acn). Creazione/etichettatura via api.createSupplier/updateSupplier
|
||||
* (NON _acn -> ritornano {success,...}). Ancoraggio GV.SC-02 + GV.SC-04/05/07. Obblighi: art. 24 D.Lgs. 138/2024.
|
||||
*/
|
||||
let STK = { groups: { supplier: [], customer: [], partner: [] }, counts: {} };
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function esc(s) { const d = document.createElement('div'); d.textContent = (s == null ? '' : String(s)); return d.innerHTML; }
|
||||
|
||||
const TYPE_TITLE = { supplier: 'Fornitori', customer: 'Clienti', partner: 'Partner' };
|
||||
const TYPE_ORDER = ['supplier', 'customer', 'partner'];
|
||||
const CRIT_LABELS = { low: 'Bassa', medium: 'Media', high: 'Alta', critical: 'Critica' };
|
||||
const STATUS_LABELS = { active: 'Attivo', under_review: 'In revisione', suspended: 'Sospeso', terminated: 'Cessato' };
|
||||
|
||||
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') stkClose(); });
|
||||
const ov = el('stk-modal');
|
||||
if (ov) ov.addEventListener('click', function (e) { if (e.target === ov) stkClose(); });
|
||||
|
||||
await stkLoad();
|
||||
});
|
||||
|
||||
async function stkLoad() {
|
||||
el('stk-groups').innerHTML = '<div class="stk-empty">Caricamento…</div>';
|
||||
try {
|
||||
const data = await api.stakeholderMap();
|
||||
STK.groups = (data && data.groups) || { supplier: [], customer: [], partner: [] };
|
||||
STK.counts = (data && data.counts) || {};
|
||||
stkRender();
|
||||
} catch (e) {
|
||||
el('stk-groups').innerHTML = '<div class="stk-empty">Errore nel caricamento: ' + esc(e.message || e) + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function stkRender() {
|
||||
const total = TYPE_ORDER.reduce((n, t) => n + ((STK.groups[t] || []).length), 0);
|
||||
if (!total) {
|
||||
el('stk-groups').innerHTML = '<div class="stk-empty">Nessuno stakeholder registrato. Aggiungine uno con "+ Aggiungi stakeholder". I fornitori inseriti dalla Supply Chain compaiono automaticamente qui.</div>';
|
||||
return;
|
||||
}
|
||||
el('stk-groups').innerHTML = TYPE_ORDER.map(stkGroupHtml).join('');
|
||||
}
|
||||
|
||||
function stkGroupHtml(type) {
|
||||
const items = STK.groups[type] || [];
|
||||
const count = STK.counts[type] != null ? STK.counts[type] : items.length;
|
||||
let inner;
|
||||
if (!items.length) {
|
||||
inner = '<div class="stk-empty">Nessun ' + esc(TYPE_TITLE[type].toLowerCase().replace(/i$/, 'e')) + ' registrato.</div>';
|
||||
} else {
|
||||
const rows = items.map(function (s) {
|
||||
const crit = s.criticality || 'medium';
|
||||
const isSupplier = (s.stakeholder_type || 'supplier') === 'supplier';
|
||||
const actions = '<button class="btn btn-outline" style="padding:3px 8px;font-size:.74rem;" onclick="stkOpen(' + s.id + ')">Modifica</button>'
|
||||
+ (isSupplier ? ' <a class="stk-link" href="supply-chain.html">Dettaglio in Supply Chain</a>' : '');
|
||||
return '<tr>'
|
||||
+ '<td><strong>' + esc(s.name) + '</strong>'
|
||||
+ (s.contact_name || s.contact_email ? '<div style="font-size:.78rem;color:var(--gray-500,#6b7280);margin-top:2px;">' + esc(s.contact_name || '') + (s.contact_email ? ' · ' + esc(s.contact_email) : '') + '</div>' : '') + '</td>'
|
||||
+ '<td>' + (s.service_type ? esc(s.service_type) : '—') + '</td>'
|
||||
+ '<td><span class="stk-badge crit-' + esc(crit) + '">' + esc(CRIT_LABELS[crit] || crit) + '</span></td>'
|
||||
+ '<td><span class="stk-badge st">' + esc(STATUS_LABELS[s.status] || s.status || '—') + '</span></td>'
|
||||
+ '<td>' + actions + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
inner = '<table class="stk-table"><thead><tr>'
|
||||
+ '<th>Nome</th><th>Servizio / relazione</th><th>Criticità</th><th>Stato</th><th>Azioni</th>'
|
||||
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
}
|
||||
return '<div class="stk-group">'
|
||||
+ '<div class="stk-group-head"><div class="stk-group-title">' + esc(TYPE_TITLE[type]) + '<span class="stk-count">' + count + '</span></div></div>'
|
||||
+ inner + '</div>';
|
||||
}
|
||||
|
||||
function stkAllItems() {
|
||||
return TYPE_ORDER.reduce((acc, t) => acc.concat(STK.groups[t] || []), []);
|
||||
}
|
||||
|
||||
function stkOpen(id) {
|
||||
const s = id ? stkAllItems().find(x => x.id === id) : null;
|
||||
el('stk-modal-title').textContent = s ? 'Modifica stakeholder' : 'Aggiungi stakeholder';
|
||||
el('stk-id').value = s ? s.id : '';
|
||||
el('stk-f-name').value = s ? (s.name || '') : '';
|
||||
el('stk-f-type').value = s ? (s.stakeholder_type || 'supplier') : 'supplier';
|
||||
el('stk-f-service').value = s ? (s.service_type || '') : '';
|
||||
el('stk-f-crit').value = s ? (s.criticality || 'medium') : 'medium';
|
||||
el('stk-f-contact-name').value = s ? (s.contact_name || '') : '';
|
||||
el('stk-f-contact-email').value = s ? (s.contact_email || '') : '';
|
||||
el('stk-err').textContent = '';
|
||||
el('stk-modal').classList.add('open');
|
||||
setTimeout(() => el('stk-f-name').focus(), 50);
|
||||
}
|
||||
|
||||
function stkClose() { el('stk-modal').classList.remove('open'); }
|
||||
|
||||
// Per clienti/partner il servizio è meno ovvio: precompila un default ragionevole se vuoto.
|
||||
function stkOnTypeChange() {
|
||||
const t = el('stk-f-type').value;
|
||||
const svc = el('stk-f-service');
|
||||
if (!svc.value.trim()) {
|
||||
if (t === 'customer') svc.value = 'Cliente';
|
||||
else if (t === 'partner') svc.value = 'Partner';
|
||||
}
|
||||
}
|
||||
|
||||
async function stkSave() {
|
||||
const id = el('stk-id').value;
|
||||
const name = el('stk-f-name').value.trim();
|
||||
let service = el('stk-f-service').value.trim();
|
||||
const type = el('stk-f-type').value;
|
||||
if (!name) { el('stk-err').textContent = 'Il nome è obbligatorio.'; return; }
|
||||
// service_type è obbligatorio lato backend: forziamo un default coerente per clienti/partner.
|
||||
if (!service) {
|
||||
service = type === 'customer' ? 'Cliente' : (type === 'partner' ? 'Partner' : '');
|
||||
}
|
||||
if (!service) { el('stk-err').textContent = 'Indica il servizio o la relazione.'; return; }
|
||||
const payload = {
|
||||
name: name,
|
||||
stakeholder_type: type,
|
||||
service_type: service,
|
||||
criticality: el('stk-f-crit').value,
|
||||
contact_name: el('stk-f-contact-name').value.trim(),
|
||||
contact_email: el('stk-f-contact-email').value.trim()
|
||||
};
|
||||
const btn = el('stk-save'); btn.disabled = true;
|
||||
try {
|
||||
const r = id ? await api.updateSupplier(parseInt(id, 10), payload) : await api.createSupplier(payload);
|
||||
if (r && r.success === false) { el('stk-err').textContent = r.message || 'Errore nel salvataggio.'; return; }
|
||||
showNotification(id ? 'Stakeholder aggiornato.' : 'Stakeholder aggiunto.', 'success');
|
||||
stkClose();
|
||||
await stkLoad();
|
||||
} catch (e) {
|
||||
el('stk-err').textContent = (e && e.message) ? e.message : 'Errore nel salvataggio.';
|
||||
} finally { btn.disabled = false; }
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -477,8 +477,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -489,8 +489,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
@@ -292,8 +292,8 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -304,8 +304,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"version": "1.17.1", "build": "2026-06-15-v1.17.1", "date": "2026-06-15", "changelog": "A4 Fase 4.3 — Matrice RACI (ruoli x oggetti) + link molti-a-molti procedure/inventario/rischi/misure ACN. Nuove tabelle raci_assignments + procedure_inventory/procedure_risk/inventory_risk/risk_measure (mig.043). RaciController org-scoped (anti-IDOR), pagina raci.html (Bootstrap Italia/AGID), help+i18n. Nessuna modifica a dati esistenti."}
|
||||
{"version": "1.17.2", "build": "2026-06-15-v1.17.2", "date": "2026-06-15", "changelog": "A4 Fasi 4.4 (scadenziario centralizzato review_schedule: list/complete/sync, mig.044) e 4.5 (stakeholder estesi a clienti/partner GV.SC-02: suppliers.stakeholder_type, stakeholderMap, mig.045) - completano A4 (5/5 fasi). Nuove pagine review-schedule.html, stakeholders.html. Nessuna modifica a dati esistenti."}
|
||||
@@ -207,8 +207,8 @@
|
||||
|
||||
<!-- Report Detail Modal handled by common.js showModal -->
|
||||
|
||||
<script src="/js/api.js?v=20260619"></script>
|
||||
<script src="/js/common.js?v=20260619"></script>
|
||||
<script src="/js/api.js?v=20260620"></script>
|
||||
<script src="/js/common.js?v=20260620"></script>
|
||||
<!-- Bootstrap Italia bundle (componenti) + override sidebar BI (common-bi.js).
|
||||
common-bi.js va DOPO common.js: ridefinisce solo loadSidebar() con markup BI,
|
||||
riusando tutti gli helper di common.js (zero backend). -->
|
||||
@@ -219,8 +219,8 @@
|
||||
}
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260618"></script>
|
||||
<script src="/js/i18n.js?v=20260619"></script>
|
||||
<script src="/js/help.js?v=20260619"></script>
|
||||
<script src="/js/i18n.js?v=20260620"></script>
|
||||
<script src="/js/help.js?v=20260620"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
@@ -319,9 +319,9 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="js/api.js?v=20260619"></script>
|
||||
<script src="js/common.js?v=20260619"></script>
|
||||
<script src="js/i18n.js?v=20260619"></script>
|
||||
<script src="js/api.js?v=20260620"></script>
|
||||
<script src="js/common.js?v=20260620"></script>
|
||||
<script src="js/i18n.js?v=20260620"></script>
|
||||
<script>
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
loadSidebar();
|
||||
|
||||
Reference in New Issue
Block a user