Files
nis2-agile/public/admin/organizations.html
T
DevEnv nis2-agileandClaude Opus 4.8 58b4bdbe5e [FIX] UI V3 — allineamento help/i18n/KB/ARIA al menu top-nav
Dopo il revamp V3, allineati i layer che descrivevano ancora la sidebar V2:
- topnav-v3.js: aggiunte le pagine orfane in nav (Matrice RACI → Struttura
  interna, Scadenziario → Monitoraggio, Compliance Journey → Guida) + cablato
  data-i18n su gruppi e voci del mega-menu (traduzione IT/EN come in V2).
- i18n.js: nuove chiavi nav V3 (gruppi mega-menu + connettori/whistleblowing/
  normative/cross-analysis/kb/architettura/simulazione/integrazioni/workflow/
  audit ACN/registra incidente).
- AIService.php navigationMapBlock: riscritta sul menu IN ALTO V3 (9 gruppi +
  pulsante "Registra incidente"), sync con topnav-v3.js NAV_GROUPS; ARIA non
  parla più di "barra laterale".
- help.js: corrette le 3 menzioni "sidebar" → barra/menu in alto.
- KB nis2_kb: nuovo docs/kb/V3_UI_NAVIGAZIONE.md + scripts/ingest-v3-ui-doc.php
  (ingerito host-side: 3 chunk SYSTEM, rimossi i chunk V2; ARIA RAG ora guida
  sul top-nav).
- Cache-buster ?v=20260627n sui 3 JS modificati su tutte le pagine.

Deploy: JS/HTML statici live via bind-mount; php-fpm reload (host + nis2-app)
per AIService.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 16:38:07 +02:00

264 lines
13 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>Gestione Organizzazioni · NIS2 Agile</title>
<!-- UI V3: Inter self-host + design system v3.css. Pagina in /admin/: asset con path ASSOLUTI. -->
<link rel="stylesheet" href="/vendor/inter/inter.css?v=20260627">
<link rel="stylesheet" href="/css/v3.css?v=20260627">
<style>
/* ── Tabella admin (v3.css non include stili tabella; markup sobrio, no CDN) ── */
.table-container { width:100%; overflow-x:auto; }
table { width:100%; border-collapse:collapse; font-size:13px; }
thead th { text-align:left; font-size:10.5px; font-weight:700; letter-spacing:.05em; text-transform:uppercase; color:#7a8aa0; padding:10px 14px; border-bottom:1px solid var(--line); white-space:nowrap; }
tbody td { padding:12px 14px; border-bottom:1px solid var(--line); vertical-align:middle; }
tbody tr:last-child td { border-bottom:0; }
tbody tr:hover { background:#F7FAFD; }
td strong { font-weight:700; }
.text-muted { color:var(--muted); }
.entity-type-badge, .plan-badge { display:inline-flex; padding:2px 7px; border-radius:6px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; white-space:nowrap; }
.entity-type-badge.essential { background:#FBE9E9; color:#B23636; }
.entity-type-badge.important { background:#FBF1E2; color:#9A6418; }
.entity-type-badge.not_applicable { background:#EEF2F7; color:#5b6b80; }
.plan-badge.free { background:#EEF2F7; color:#5b6b80; }
.plan-badge.professional { background:#E7F0FB; color:#1F5FA8; }
.plan-badge.enterprise { background:#F0E9FB; color:#7C4DD6; }
.status-dot { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; }
.status-dot::before { content:''; width:8px; height:8px; border-radius:50%; flex-shrink:0; }
.status-dot.active::before { background:var(--c-green); }
.status-dot.inactive::before { background:#94A3B8; }
.score-inline { display:inline-flex; align-items:center; gap:6px; font-size:.8125rem; font-weight:600; }
.score-inline .score-dot { width:8px; height:8px; border-radius:50%; }
.pagination { display:flex; align-items:center; justify-content:space-between; padding:14px 14px 2px; border-top:1px solid var(--line); margin-top:6px; }
.pagination-info { font-size:.8125rem; color:var(--muted); }
.pagination-controls { display:flex; gap:8px; }
.btn-ghost[disabled] { opacity:.5; cursor:not-allowed; }
.btn-sm { padding:6px 11px; font-size:11.5px; }
.empty-state { text-align:center; padding:60px 24px; color:var(--muted); }
.empty-state .ic { width:48px; height:48px; color:var(--line); margin:0 auto 16px; display:block; }
.empty-state h4 { font-size:1rem; font-weight:700; color:var(--ink); margin:0 0 8px; }
.empty-state p { margin:0; }
</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>
<!-- Chrome V3 (appbar + mega-menu) iniettata da topnav-v3.js in questo host -->
<div id="topnav"></div>
<main class="wrap">
<nav class="crumb" aria-label="breadcrumb">
<i data-lucide="home" class="ic"></i><a href="/dashboard.html">Home</a>
<i data-lucide="chevron-right" class="ic"></i><a href="/admin/index.html">Amministrazione</a>
<i data-lucide="chevron-right" class="ic"></i><span>Organizzazioni</span>
</nav>
<div class="page-head">
<div>
<h1>Gestione Organizzazioni</h1>
<p class="sub">Tutte le organizzazioni registrate sulla piattaforma</p>
</div>
<span class="badge-t red">Super Admin</span>
</div>
<section class="card card-pad">
<div id="orgs-container">
<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>
</div>
</section>
</main>
<!-- Scripts V3: api.js → common.js (helper+FAB reali) → Lucide → topnav-v3.js (chrome) → i18n → init pagina -->
<script src="/js/api.js?v=20260625d"></script>
<script src="/js/common.js?v=20260627v3"></script>
<script src="/vendor/lucide/lucide.min.js"></script>
<script src="/js/topnav-v3.js?v=20260627n"></script>
<script src="/js/i18n.js?v=20260627n"></script>
<script src="/js/help.js?v=20260627n"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
if (typeof I18n !== 'undefined') I18n.init();
if (typeof HelpSystem !== 'undefined') HelpSystem.init();
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
let currentPage = 1;
// ── Admin check ──────────────────────────────────────────
(async function checkAdmin() {
try {
const me = await api.getMe();
if (!me.success || !me.data || me.data.role !== 'super_admin') {
showNotification('Accesso non autorizzato.', 'error');
setTimeout(() => { window.location.href = '/dashboard.html'; }, 1500);
return;
}
loadOrganizations(1);
} catch (e) {
window.location.href = '/dashboard.html';
}
})();
// ── Mappature ────────────────────────────────────────────
const sectorLabels = {
energy: 'Energia', transport: 'Trasporti', banking: 'Banche',
health: 'Sanita\'', water: 'Acqua', digital_infra: 'Infrastrutture Digitali',
public_admin: 'Pubblica Amministrazione', manufacturing: 'Manifatturiero',
postal: 'Servizi Postali', chemical: 'Chimico', food: 'Alimentare',
waste: 'Rifiuti', ict_services: 'Servizi ICT',
digital_providers: 'Provider Digitali', space: 'Spazio',
research: 'Ricerca', other: 'Altro'
};
const entityTypeLabels = {
essential: 'Essenziale',
important: 'Importante',
not_applicable: 'N/A'
};
const planLabels = { free: 'Free', professional: 'Professional', enterprise: 'Enterprise' };
// ── Caricamento ──────────────────────────────────────────
async function loadOrganizations(page) {
const container = document.getElementById('orgs-container');
container.innerHTML = '<div class="v3-loading"><span class="v3-spin"></span>Caricamento…</div>';
currentPage = page;
try {
const result = await api.request('GET', '/admin/organizations?page=' + page);
if (result.success) {
// /admin/organizations risponde paginato: data = {items, total, page, per_page, pages}.
renderOrganizations((result.data && result.data.items) || (Array.isArray(result.data) ? result.data : []), (result.data && !Array.isArray(result.data)) ? result.data : (result.pagination || {}));
} else {
container.innerHTML = `
<div class="empty-state">
<h4>Errore nel caricamento</h4>
<p>${escapeHtml(result.message || 'Errore sconosciuto')}</p>
</div>`;
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderOrganizations(orgs, pagination) {
const container = document.getElementById('orgs-container');
if (!orgs || orgs.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i data-lucide="building-2" class="ic"></i>
<h4>Nessuna organizzazione trovata</h4>
<p>Le organizzazioni registrate appariranno qui.</p>
</div>`;
if (window.lucide && lucide.createIcons) { try { lucide.createIcons(); } catch (e) {} }
return;
}
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th scope="col">Nome</th>
<th scope="col">Settore</th>
<th scope="col">Tipo Entita'</th>
<th scope="col">Membri</th>
<th scope="col">Ultimo Score</th>
<th scope="col">Piano</th>
<th scope="col">Stato</th>
<th scope="col">Data Creazione</th>
</tr>
</thead>
<tbody>`;
orgs.forEach(org => {
const entityType = org.entity_type || 'not_applicable';
const sector = sectorLabels[org.sector] || org.sector || '-';
const entityLabel = entityTypeLabels[entityType] || entityType;
const plan = (org.subscription_plan || 'free').toLowerCase();
const planLabel = planLabels[plan] || plan;
const isActive = org.is_active == 1 || org.is_active === true;
const score = org.last_score;
let scoreHtml = '<span class="text-muted">-</span>';
if (score !== null && score !== undefined) {
const scoreColor = getScoreColor(score);
scoreHtml = `
<span class="score-inline">
<span class="score-dot" style="background:${scoreColor}"></span>
${Math.round(score)}%
</span>`;
}
html += `
<tr>
<td><strong>${escapeHtml(org.name || '-')}</strong></td>
<td>${escapeHtml(sector)}</td>
<td><span class="entity-type-badge ${entityType}">${escapeHtml(entityLabel)}</span></td>
<td>${org.member_count ?? '-'}</td>
<td>${scoreHtml}</td>
<td><span class="plan-badge ${plan}">${escapeHtml(planLabel)}</span></td>
<td><span class="status-dot ${isActive ? 'active' : 'inactive'}">${isActive ? 'Attiva' : 'Inattiva'}</span></td>
<td>${formatDate(org.created_at)}</td>
</tr>`;
});
html += '</tbody></table></div>';
// Paginazione
const total = pagination.total || orgs.length;
const perPage = pagination.per_page || 20;
const page = pagination.page || currentPage;
const totalPages = Math.ceil(total / perPage);
const start = ((page - 1) * perPage) + 1;
const end = Math.min(page * perPage, total);
if (totalPages > 1) {
html += `
<div class="pagination">
<div class="pagination-info">
Visualizzati ${start}-${end} di ${total} organizzazioni
</div>
<div class="pagination-controls">
<button class="btn-ghost btn-sm" onclick="loadOrganizations(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente
</button>
<button class="btn-ghost btn-sm" onclick="loadOrganizations(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>
Successivo &raquo;
</button>
</div>
</div>`;
} else if (total > 0) {
html += `
<div class="pagination">
<div class="pagination-info">
Totale: ${total} organizzazion${total === 1 ? 'e' : 'i'}
</div>
<div></div>
</div>`;
}
container.innerHTML = html;
}
</script>
</body>
</html>