nis2-agile/public/policies.html
DevEnv nis2-agile 59fad22c0e [UX+SEC] Eccellenza pre-audit: idle timeout, loading states, i18n, UX polish
- common.js: idle session timeout 30min con avviso countdown 5min prima del logout
- common.js: checkAuth() attiva automaticamente il monitor di inattività
- api.js: messaggi errore connessione usano i18n (IT/EN) tramite I18n.t()
- risks.html: saveRisk() e aiSuggest() con setButtonLoading durante salvataggio
- risks.html: deleteRisk() ricarica la matrice se si è in matrix view
- incidents.html: createIncident() con setButtonLoading durante registrazione
- policies.html: savePolicy() e saveAIGeneratedPolicy() con setButtonLoading
- policies.html: banner AI-draft con pulsante X per dismissione

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 12:25:52 +01:00

955 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Policy - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Policy Page Styles ──────────────────────────────────── */
.filters-bar {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 20px;
}
.filters-bar .form-select {
min-width: 180px;
padding: 8px 12px;
font-size: 0.85rem;
}
.filters-bar .btn {
white-space: nowrap;
}
.policy-actions {
display: flex;
gap: 6px;
}
.policy-actions .btn {
padding: 5px 10px;
font-size: 0.75rem;
}
.tabs {
display: flex;
gap: 0;
border-bottom: 2px solid var(--gray-200);
margin-bottom: 24px;
}
.tab-btn {
padding: 10px 20px;
font-size: 0.875rem;
font-weight: 600;
color: var(--gray-500);
background: none;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: all var(--transition-fast);
}
.tab-btn:hover {
color: var(--gray-700);
}
.tab-btn.active {
color: var(--primary);
border-bottom-color: var(--primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.template-card {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 20px;
cursor: pointer;
transition: all var(--transition-fast);
}
.template-card:hover {
border-color: var(--primary);
box-shadow: var(--card-shadow-hover);
transform: translateY(-2px);
}
.template-card h4 {
font-size: 0.9rem;
color: var(--gray-800);
margin-bottom: 8px;
}
.template-card .template-meta {
font-size: 0.78rem;
color: var(--gray-500);
display: flex;
align-items: center;
gap: 12px;
}
.template-card .template-meta .tag {
font-size: 0.7rem;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 24px;
gap: 16px;
}
.detail-header h3 {
font-size: 1.25rem;
color: var(--gray-900);
}
.detail-header-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.detail-grid {
display: grid;
grid-template-columns: 1fr 300px;
gap: 24px;
}
@media (max-width: 900px) {
.detail-grid {
grid-template-columns: 1fr;
}
}
.policy-content-box {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 24px;
white-space: pre-wrap;
font-size: 0.875rem;
line-height: 1.7;
color: var(--gray-700);
min-height: 200px;
max-height: 600px;
overflow-y: auto;
}
.meta-card {
background: var(--card-bg);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 20px;
}
.meta-card h4 {
font-size: 0.85rem;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--gray-100);
}
.meta-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 0.82rem;
border-bottom: 1px solid var(--gray-50);
}
.meta-row:last-child {
border-bottom: none;
}
.meta-label {
color: var(--gray-500);
font-weight: 500;
}
.meta-value {
color: var(--gray-800);
font-weight: 600;
text-align: right;
}
.ai-preview {
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: var(--border-radius);
padding: 20px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
font-size: 0.85rem;
line-height: 1.6;
margin-top: 12px;
}
.ai-preview-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
color: var(--primary);
font-weight: 600;
font-size: 0.9rem;
}
.ai-preview-header svg {
width: 20px;
height: 20px;
}
.loading-overlay {
display: flex;
align-items: center;
justify-content: center;
padding: 60px 20px;
flex-direction: column;
gap: 12px;
color: var(--gray-500);
}
.loading-overlay .spinner {
width: 32px;
height: 32px;
border: 3px solid var(--gray-200);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.text-link {
color: var(--primary);
cursor: pointer;
font-weight: 500;
}
.text-link:hover {
text-decoration: underline;
}
.content-header-actions {
display: flex;
gap: 8px;
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="policies.title">Gestione Policy</h2>
<div class="content-header-actions">
<button class="btn btn-outline" onclick="openAIGenerateModal()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
Genera con AI
</button>
<button class="btn btn-primary" onclick="openCreateModal()">+ Nuova Policy</button>
</div>
</header>
<div class="content-body" id="page-content">
<!-- Tabs -->
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('list')">Elenco Policy</button>
<button class="tab-btn" onclick="switchTab('templates')">Template</button>
</div>
<!-- Tab: Lista Policy -->
<div class="tab-content active" id="tab-list">
<div class="filters-bar">
<select class="form-select" id="filter-category" onchange="loadPolicies()">
<option value="">Tutte le Categorie</option>
<option value="information_security">Sicurezza Informazioni</option>
<option value="access_control">Controllo Accessi</option>
<option value="incident_response">Risposta Incidenti</option>
<option value="business_continuity">Continuita' Operativa</option>
<option value="supply_chain">Supply Chain</option>
<option value="encryption">Crittografia</option>
<option value="hr_security">Sicurezza HR</option>
<option value="asset_management">Gestione Asset</option>
<option value="network_security">Sicurezza Rete</option>
<option value="vulnerability_management">Gestione Vulnerabilita'</option>
</select>
<select class="form-select" id="filter-status" onchange="loadPolicies()">
<option value="">Tutti gli Stati</option>
<option value="draft">Bozza</option>
<option value="review">In Revisione</option>
<option value="approved">Approvata</option>
<option value="published">Pubblicata</option>
<option value="archived">Archiviata</option>
</select>
</div>
<div class="card">
<div class="table-container" id="policies-table">
<div class="loading-overlay">
<div class="spinner"></div>
<span>Caricamento policy...</span>
</div>
</div>
</div>
</div>
<!-- Tab: Template -->
<div class="tab-content" id="tab-templates">
<div class="card">
<div class="card-header">
<h3>Template Policy NIS2</h3>
</div>
<div class="card-body" id="templates-grid">
<div class="loading-overlay">
<div class="spinner"></div>
<span>Caricamento template...</span>
</div>
</div>
</div>
</div>
<!-- Detail View (hidden by default) -->
<div id="policy-detail-view" style="display:none;"></div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/common.js"></script>
<script src="/js/i18n.js"></script>
<script src="/js/help.js"></script>
<script>
// ── Auth & Init ─────────────────────────────────────────
if (!checkAuth()) throw new Error('Not authenticated');
loadSidebar();
I18n.init();
HelpSystem.init();
// ── Labels ──────────────────────────────────────────────
const CATEGORY_LABELS = {
information_security: 'Sicurezza Informazioni',
access_control: 'Controllo Accessi',
incident_response: 'Risposta Incidenti',
business_continuity: 'Continuita\' Operativa',
supply_chain: 'Supply Chain',
encryption: 'Crittografia',
hr_security: 'Sicurezza HR',
asset_management: 'Gestione Asset',
network_security: 'Sicurezza Rete',
vulnerability_management: 'Gestione Vulnerabilita\''
};
const STATUS_LABELS = {
draft: 'Bozza',
review: 'In Revisione',
approved: 'Approvata',
published: 'Pubblicata',
archived: 'Archiviata'
};
const STATUS_BADGE_CLASS = {
draft: 'badge-neutral',
review: 'badge-info',
approved: 'badge-success',
published: 'badge-primary',
archived: 'badge-neutral'
};
// ── State ───────────────────────────────────────────────
let currentView = 'list'; // 'list' | 'detail'
let policiesData = [];
// ── Tab switching ───────────────────────────────────────
function switchTab(tab) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
if (tab === 'list') {
document.querySelectorAll('.tab-btn')[0].classList.add('active');
document.getElementById('tab-list').classList.add('active');
} else if (tab === 'templates') {
document.querySelectorAll('.tab-btn')[1].classList.add('active');
document.getElementById('tab-templates').classList.add('active');
loadTemplates();
}
// Hide detail view when switching tabs
document.getElementById('policy-detail-view').style.display = 'none';
document.querySelector('.tabs').style.display = '';
document.getElementById('tab-list').style.display = '';
document.getElementById('tab-templates').style.display = '';
currentView = 'list';
}
// ── Load Policies ───────────────────────────────────────
async function loadPolicies() {
const params = {};
const category = document.getElementById('filter-category').value;
const status = document.getElementById('filter-status').value;
if (category) params.category = category;
if (status) params.status = status;
const container = document.getElementById('policies-table');
container.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento policy...</span></div>';
try {
const result = await api.listPolicies(params);
if (result.success) {
policiesData = result.data || [];
renderPoliciesTable(policiesData);
} 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><p>Impossibile caricare le policy.</p></div>';
}
}
function renderPoliciesTable(policies) {
const container = document.getElementById('policies-table');
if (!policies || policies.length === 0) {
container.innerHTML = `
<div class="empty-state" style="padding:40px 20px;">
<svg viewBox="0 0 20 20" fill="currentColor" style="width:48px;height:48px;color:var(--gray-300);">
<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>
<h4>Nessuna policy trovata</h4>
<p>Crea la tua prima policy o utilizza un template per iniziare.</p>
</div>`;
return;
}
let html = `
<table>
<thead>
<tr>
<th>Titolo</th>
<th>Categoria</th>
<th>Art. NIS2</th>
<th>Versione</th>
<th>Stato</th>
<th>Prossima Revisione</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>`;
policies.forEach(p => {
const catLabel = CATEGORY_LABELS[p.category] || p.category || '-';
const statusLabel = STATUS_LABELS[p.status] || p.status || 'Bozza';
const badgeClass = STATUS_BADGE_CLASS[p.status] || 'badge-neutral';
html += `
<tr>
<td>
<span class="text-link" onclick="viewPolicy(${p.id})">${escapeHtml(p.title)}</span>
</td>
<td>${escapeHtml(catLabel)}</td>
<td>${escapeHtml(p.nis2_article || '-')}</td>
<td>${escapeHtml(p.version || '1.0')}</td>
<td><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></td>
<td>${formatDate(p.next_review_date)}</td>
<td>
<div class="policy-actions">
<button class="btn btn-sm btn-ghost" onclick="viewPolicy(${p.id})" title="Visualizza">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/></svg>
</button>
<button class="btn btn-sm btn-ghost" onclick="openEditModal(${p.id})" title="Modifica">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
</button>
${p.status === 'draft' || p.status === 'review' ? `
<button class="btn btn-sm btn-success" onclick="approvePolicy(${p.id})" title="Approva">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
</button>` : ''}
<button class="btn btn-sm btn-ghost" onclick="confirmDeletePolicy(${p.id}, '${escapeHtml(p.title).replace(/'/g, "\\'")}')" title="Elimina" style="color:var(--danger);">
<svg viewBox="0 0 20 20" fill="currentColor" width="14" height="14"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
</button>
</div>
</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// ── View Policy Detail ──────────────────────────────────
async function viewPolicy(id) {
// Hide tabs and list
document.querySelector('.tabs').style.display = 'none';
document.getElementById('tab-list').classList.remove('active');
document.getElementById('tab-templates').classList.remove('active');
const detailView = document.getElementById('policy-detail-view');
detailView.style.display = 'block';
detailView.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento dettagli...</span></div>';
currentView = 'detail';
try {
const result = await api.getPolicy(id);
if (result.success && result.data) {
renderPolicyDetail(result.data);
} else {
detailView.innerHTML = '<div class="empty-state"><h4>Policy non trovata</h4></div>';
}
} catch (e) {
detailView.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderPolicyDetail(policy) {
const detailView = document.getElementById('policy-detail-view');
const catLabel = CATEGORY_LABELS[policy.category] || policy.category || '-';
const statusLabel = STATUS_LABELS[policy.status] || policy.status || 'Bozza';
const badgeClass = STATUS_BADGE_CLASS[policy.status] || 'badge-neutral';
detailView.innerHTML = `
<div class="detail-header">
<div>
<div style="margin-bottom:8px;">
<span class="text-link" onclick="backToList()" style="font-size:0.82rem;">
&larr; Torna all'elenco
</span>
</div>
<h3>${escapeHtml(policy.title)}</h3>
<div style="margin-top:8px;">
<span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span>
${policy.ai_generated ? '<span class="badge badge-info" style="margin-left:6px;">AI Generata</span>' : ''}
</div>
${policy.ai_generated && (policy.status === 'draft' || policy.status === 'review') ? `
<div id="ai-draft-warning-${policy.id}" style="background:#fff7ed; border:1px solid #fed7aa; border-radius:var(--border-radius); padding:12px 16px; margin-top:12px; display:flex; gap:10px; align-items:flex-start; position:relative;">
<svg viewBox="0 0 20 20" fill="#c2410c" width="18" height="18" style="flex-shrink:0; margin-top:2px;"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
<div style="flex:1;">
<strong style="color:#c2410c; font-size:0.875rem;">Bozza AI — Revisione obbligatoria prima dell'approvazione</strong>
<p style="font-size:0.78rem; color:#92400e; margin:4px 0 0;">Questo documento e' stato generato automaticamente da un sistema AI. Prima di approvarlo, verificare che sia conforme alle procedure interne, al quadro normativo applicabile e alla realta' specifica dell'organizzazione. L'AI puo' produrre contenuti imprecisi o incompleti.</p>
</div>
<button onclick="document.getElementById('ai-draft-warning-${policy.id}').remove();" title="Chiudi" aria-label="Chiudi avviso"
style="flex-shrink:0;background:none;border:none;cursor:pointer;color:#c2410c;padding:2px;line-height:1;opacity:0.7;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.7">
<svg width="16" height="16" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
</button>
</div>` : ''}
</div>
<div class="detail-header-actions">
${policy.status === 'draft' || policy.status === 'review' ? `
<button class="btn btn-success" onclick="approvePolicy(${policy.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>
Approva
</button>` : ''}
<button class="btn btn-secondary" onclick="openEditModal(${policy.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/></svg>
Modifica
</button>
</div>
</div>
<div class="detail-grid">
<div>
<div class="card">
<div class="card-header">
<h3>Contenuto Policy</h3>
</div>
<div class="card-body">
<div class="policy-content-box">${escapeHtml(policy.content || 'Nessun contenuto disponibile.')}</div>
</div>
</div>
</div>
<div>
<div class="meta-card">
<h4>Informazioni</h4>
<div class="meta-row">
<span class="meta-label">Categoria</span>
<span class="meta-value">${escapeHtml(catLabel)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Art. NIS2</span>
<span class="meta-value">${escapeHtml(policy.nis2_article || '-')}</span>
</div>
<div class="meta-row">
<span class="meta-label">Versione</span>
<span class="meta-value">${escapeHtml(policy.version || '1.0')}</span>
</div>
<div class="meta-row">
<span class="meta-label">Stato</span>
<span class="meta-value"><span class="badge ${badgeClass}">${escapeHtml(statusLabel)}</span></span>
</div>
<div class="meta-row">
<span class="meta-label">Creata il</span>
<span class="meta-value">${formatDate(policy.created_at)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Aggiornata il</span>
<span class="meta-value">${formatDate(policy.updated_at)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Prossima Revisione</span>
<span class="meta-value">${formatDate(policy.next_review_date)}</span>
</div>
${policy.approved_by_name ? `
<div class="meta-row">
<span class="meta-label">Approvata da</span>
<span class="meta-value">${escapeHtml(policy.approved_by_name)}</span>
</div>
<div class="meta-row">
<span class="meta-label">Approvata il</span>
<span class="meta-value">${formatDate(policy.approved_at)}</span>
</div>` : ''}
</div>
</div>
</div>
`;
}
function backToList() {
document.getElementById('policy-detail-view').style.display = 'none';
document.querySelector('.tabs').style.display = '';
switchTab('list');
currentView = 'list';
}
// ── Create / Edit Modal ─────────────────────────────────
function openCreateModal() {
showPolicyFormModal(null);
}
async function openEditModal(id) {
try {
const result = await api.getPolicy(id);
if (result.success && result.data) {
showPolicyFormModal(result.data);
} else {
showNotification('Impossibile caricare la policy.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
function showPolicyFormModal(policy) {
const isEdit = !!policy;
const title = isEdit ? 'Modifica Policy' : 'Nuova Policy';
const categoryOptions = Object.entries(CATEGORY_LABELS).map(([val, label]) =>
`<option value="${val}" ${policy && policy.category === val ? 'selected' : ''}>${label}</option>`
).join('');
const content = `
<form id="policy-form" onsubmit="return false;">
<div class="form-group">
<label class="form-label">Titolo <span class="required">*</span></label>
<input type="text" class="form-input" id="form-title" value="${isEdit ? escapeHtml(policy.title) : ''}" required placeholder="Inserisci il titolo della policy">
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Categoria <span class="required">*</span></label>
<select class="form-select" id="form-category" required>
<option value="">Seleziona...</option>
${categoryOptions}
</select>
</div>
<div class="form-group">
<label class="form-label">Articolo NIS2</label>
<input type="text" class="form-input" id="form-nis2-article" value="${isEdit && policy.nis2_article ? escapeHtml(policy.nis2_article) : ''}" placeholder="es. 21.2.a">
</div>
</div>
<div class="form-row" style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">Versione</label>
<input type="text" class="form-input" id="form-version" value="${isEdit && policy.version ? escapeHtml(policy.version) : '1.0'}" placeholder="1.0">
</div>
<div class="form-group">
<label class="form-label">Prossima Revisione</label>
<input type="date" class="form-input" id="form-review-date" value="${isEdit && policy.next_review_date ? policy.next_review_date.substring(0, 10) : ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Contenuto</label>
<textarea class="form-textarea" id="form-content" rows="12" placeholder="Inserisci il contenuto della policy...&#10;&#10;Puoi strutturare il documento con sezioni:&#10;1. Scopo&#10;2. Ambito&#10;3. Responsabilita'&#10;4. Procedure&#10;5. Revisione">${isEdit && policy.content ? escapeHtml(policy.content) : ''}</textarea>
</div>
</form>
`;
showModal(title, content, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="savePolicy(${isEdit ? policy.id : 'null'})">${isEdit ? 'Salva Modifiche' : 'Crea Policy'}</button>
`
});
}
async function savePolicy(id) {
const title = document.getElementById('form-title').value.trim();
const category = document.getElementById('form-category').value;
if (!title) {
showNotification('Il titolo e\' obbligatorio.', 'warning');
return;
}
if (!category) {
showNotification('La categoria e\' obbligatoria.', 'warning');
return;
}
const data = {
title: title,
category: category,
nis2_article: document.getElementById('form-nis2-article').value.trim(),
version: document.getElementById('form-version').value.trim() || '1.0',
content: document.getElementById('form-content').value,
next_review_date: document.getElementById('form-review-date').value || null,
};
const btn = document.querySelector('#modal-overlay .btn-primary');
setButtonLoading(btn, true);
try {
let result;
if (id) {
result = await api.updatePolicy(id, data);
} else {
result = await api.createPolicy(data);
}
if (result.success) {
closeModal();
showNotification(id ? 'Policy aggiornata con successo.' : 'Policy creata con successo.', 'success');
loadPolicies();
if (currentView === 'detail' && id) {
viewPolicy(id);
}
} else {
setButtonLoading(btn, false);
showNotification(result.message || 'Errore nel salvataggio.', 'error');
}
} catch (e) {
setButtonLoading(btn, false);
showNotification('Errore di connessione.', 'error');
}
}
// ── Approve Policy ──────────────────────────────────────
async function approvePolicy(id) {
showModal('Conferma Approvazione', `
<p>Sei sicuro di voler approvare questa policy?</p>
<p style="margin-top:8px;font-size:0.85rem;color:var(--gray-500);">Lo stato verra' modificato in "Approvata" e non sara' piu' possibile annullare l'operazione.</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-success" onclick="doApprovePolicy(${id})">Approva</button>
`
});
}
async function doApprovePolicy(id) {
closeModal();
try {
const result = await api.approvePolicy(id);
if (result.success) {
showNotification('Policy approvata con successo.', 'success');
loadPolicies();
if (currentView === 'detail') {
viewPolicy(id);
}
} else {
showNotification(result.message || 'Errore nell\'approvazione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── Delete Policy ───────────────────────────────────────
function confirmDeletePolicy(id, title) {
showModal('Conferma Eliminazione', `
<p>Sei sicuro di voler eliminare la policy <strong>${title}</strong>?</p>
<p style="margin-top:8px;font-size:0.85rem;color:var(--danger);">Questa azione e' irreversibile.</p>
`, {
size: 'sm',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-danger" onclick="doDeletePolicy(${id})">Elimina</button>
`
});
}
async function doDeletePolicy(id) {
closeModal();
try {
const result = await api.del(`/policies/${id}`);
if (result.success) {
showNotification('Policy eliminata.', 'success');
loadPolicies();
if (currentView === 'detail') {
backToList();
}
} else {
showNotification(result.message || 'Errore nell\'eliminazione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
}
// ── AI Generate ─────────────────────────────────────────
function openAIGenerateModal() {
const categoryOptions = Object.entries(CATEGORY_LABELS).map(([val, label]) =>
`<option value="${val}">${label}</option>`
).join('');
showModal('Genera Policy con AI', `
<p style="margin-bottom:16px;color:var(--gray-600);font-size:0.9rem;">
Seleziona la categoria per generare automaticamente una bozza di policy tramite intelligenza artificiale.
Il contenuto generato potra' essere rivisto e modificato prima della pubblicazione.
</p>
<div class="form-group">
<label class="form-label">Categoria Policy <span class="required">*</span></label>
<select class="form-select" id="ai-category">
${categoryOptions}
</select>
</div>
<div id="ai-preview-container" style="display:none;"></div>
`, {
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" id="ai-generate-btn" onclick="doAIGenerate()">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
Genera
</button>
`
});
}
let aiGeneratedData = null;
async function doAIGenerate() {
const category = document.getElementById('ai-category').value;
const btn = document.getElementById('ai-generate-btn');
const previewContainer = document.getElementById('ai-preview-container');
btn.disabled = true;
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0 auto;"></div> Generazione...';
try {
const result = await api.aiGeneratePolicy(category);
if (result.success && result.data) {
aiGeneratedData = result.data;
previewContainer.style.display = 'block';
previewContainer.innerHTML = `
<div class="ai-preview-header">
<svg viewBox="0 0 20 20" fill="currentColor"><path d="M5 2a1 1 0 011 1v1h1a1 1 0 010 2H6v1a1 1 0 01-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-1zm7-10a1 1 0 01.967.744L14.146 7.2 17.5 8.512a1 1 0 010 1.836l-3.354 1.311-1.18 4.456a1 1 0 01-1.932 0L9.854 11.66 6.5 10.348a1 1 0 010-1.836l3.354-1.311 1.18-4.456A1 1 0 0112 2z"/></svg>
Policy Generata: ${escapeHtml(aiGeneratedData.title || '')}
</div>
<div class="ai-preview">${escapeHtml(aiGeneratedData.content || '')}</div>
`;
// Change footer buttons
const footer = btn.closest('.modal-footer');
footer.innerHTML = `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveAIGeneratedPolicy()">Salva come Bozza</button>
`;
} else {
showNotification(result.message || 'Errore nella generazione AI.', 'error');
btn.disabled = false;
btn.textContent = 'Genera';
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
btn.disabled = false;
btn.textContent = 'Genera';
}
}
async function saveAIGeneratedPolicy() {
if (!aiGeneratedData) return;
const data = {
title: aiGeneratedData.title || 'Policy Generata AI',
category: aiGeneratedData.category || document.getElementById('ai-category')?.value || 'information_security',
nis2_article: aiGeneratedData.nis2_article || '',
content: aiGeneratedData.content || '',
version: '1.0',
ai_generated: 1,
};
const btn = document.querySelector('#modal-overlay .btn-primary');
setButtonLoading(btn, true);
try {
const result = await api.createPolicy(data);
if (result.success) {
closeModal();
showNotification('Policy AI salvata come bozza.', 'success');
aiGeneratedData = null;
loadPolicies();
} else {
setButtonLoading(btn, false);
showNotification(result.message || 'Errore nel salvataggio.', 'error');
}
} catch (e) {
setButtonLoading(btn, false);
showNotification('Errore di connessione.', 'error');
}
}
// ── Templates ───────────────────────────────────────────
async function loadTemplates() {
const container = document.getElementById('templates-grid');
container.innerHTML = '<div class="loading-overlay"><div class="spinner"></div><span>Caricamento template...</span></div>';
try {
const result = await api.getPolicyTemplates();
if (result.success && result.data) {
renderTemplates(result.data);
} else {
container.innerHTML = '<div class="empty-state"><h4>Nessun template disponibile</h4></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state"><h4>Errore di connessione</h4></div>';
}
}
function renderTemplates(templates) {
const container = document.getElementById('templates-grid');
if (!templates || templates.length === 0) {
container.innerHTML = '<div class="empty-state"><h4>Nessun template disponibile</h4></div>';
return;
}
let html = '<div class="template-grid">';
templates.forEach(t => {
const catLabel = CATEGORY_LABELS[t.category] || t.category;
html += `
<div class="template-card" onclick="createFromTemplate('${escapeHtml(t.category)}', '${escapeHtml(t.title).replace(/'/g, "\\'")}', '${escapeHtml(t.nis2_article || '').replace(/'/g, "\\'")}')">
<h4>${escapeHtml(t.title)}</h4>
<div class="template-meta">
<span class="tag">${escapeHtml(catLabel)}</span>
${t.nis2_article ? `<span style="color:var(--gray-400);">Art. ${escapeHtml(t.nis2_article)}</span>` : ''}
</div>
</div>
`;
});
html += '</div>';
container.innerHTML = html;
}
function createFromTemplate(category, title, nis2Article) {
showPolicyFormModal({
title: title,
category: category,
nis2_article: nis2Article,
version: '1.0',
content: '',
});
}
// ── Init ────────────────────────────────────────────────
loadPolicies();
</script>
</body>
</html>