nis2-agile/public/admin/organizations.html
Cristiano Benassati 73e78ea6b4 [FEAT] Add all frontend pages - complete UI for NIS2 platform
- risks.html: Risk register with 5x5 matrix heatmap, treatments, AI suggest
- incidents.html: Incident management with NIS2 Art.23 timeline (24h/72h/30d)
- policies.html: Policy management with templates, approval workflow, AI generate
- supply-chain.html: Supplier registry with 10-question security assessment
- training.html: Courses, assignments, compliance status tracking
- assets.html: Asset inventory with dependency mapping
- reports.html: Compliance report, controls, audit log, ISO 27001 mapping
- settings.html: Organization, profile, members, security settings
- admin/index.html: Platform admin dashboard with stats
- admin/organizations.html: Organization management for super_admin
- admin/users.html: User management for super_admin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:46:03 +01:00

298 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gestione Organizzazioni - NIS2 Agile</title>
<link rel="stylesheet" href="/nis2/css/style.css">
<style>
.entity-type-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.entity-type-badge.essential { background: var(--danger-bg); color: var(--danger); }
.entity-type-badge.important { background: var(--warning-bg); color: #a16207; }
.entity-type-badge.not_applicable { background: var(--gray-100); color: var(--gray-600); }
.plan-badge {
display: inline-flex;
padding: 3px 10px;
border-radius: 100px;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.plan-badge.free { background: var(--gray-100); color: var(--gray-600); }
.plan-badge.professional { background: var(--primary-bg); color: var(--primary); }
.plan-badge.enterprise { background: #f3e8ff; color: #7c3aed; }
.status-dot {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
}
.status-dot::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.active::before { background: var(--secondary); }
.status-dot.inactive::before { background: var(--gray-400); }
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid var(--gray-100);
background: var(--gray-50);
}
.pagination-info {
font-size: 0.8125rem;
color: var(--gray-500);
}
.pagination-controls {
display: flex;
gap: 8px;
}
.score-inline {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.8125rem;
font-weight: 600;
}
.score-inline .score-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.admin-breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8125rem;
color: var(--gray-500);
margin-bottom: 20px;
}
.admin-breadcrumb a {
color: var(--primary);
text-decoration: none;
font-weight: 500;
}
.admin-breadcrumb a:hover {
text-decoration: underline;
}
.admin-breadcrumb svg {
width: 14px;
height: 14px;
color: var(--gray-400);
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2>Gestione Organizzazioni</h2>
<div class="content-header-actions">
<span class="badge badge-danger">Super Admin</span>
</div>
</header>
<div class="content-body">
<!-- Breadcrumb -->
<div class="admin-breadcrumb">
<a href="/nis2/admin/index.html">Amministrazione</a>
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/></svg>
<span>Organizzazioni</span>
</div>
<!-- Table -->
<div class="card">
<div class="card-body" style="padding:0;">
<div id="orgs-container">
<div class="spinner" style="margin:60px auto;"></div>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="/nis2/js/api.js"></script>
<script src="/nis2/js/common.js"></script>
<script>
// ── Auth check ───────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
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 = '/nis2/dashboard.html'; }, 1500);
return;
}
loadOrganizations(1);
} catch (e) {
window.location.href = '/nis2/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="spinner" style="margin:60px auto;"></div>';
currentPage = page;
try {
const result = await api.request('GET', '/admin/organizations?page=' + page);
if (result.success) {
renderOrganizations(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">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
<h4>Nessuna organizzazione trovata</h4>
<p>Le organizzazioni registrate appariranno qui.</p>
</div>`;
return;
}
let html = `
<div class="table-container">
<table>
<thead>
<tr>
<th>Nome</th>
<th>Settore</th>
<th>Tipo Entita'</th>
<th>Membri</th>
<th>Ultimo Score</th>
<th>Piano</th>
<th>Stato</th>
<th>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 btn-secondary btn-sm" onclick="loadOrganizations(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
&laquo; Precedente
</button>
<button class="btn btn-secondary 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>