nis2-agile/public/assets.html
DevEnv nis2-agile 0e78ec24c1 [FIX] i18n funzionante + bug audit.html + help system
- common.js: aggiunto i18nKey a navItems, data-i18n su sezioni e voci
  sidebar → toggle IT/EN ora traduce la navigazione in tempo reale
- Tutte e 10 le pagine HTML: aggiunto data-i18n="*.title" agli h2
  (dashboard, assessment, risks, incidents, policies, supply-chain,
  training, assets, reports, settings)
- FIX BUG: sidebar puntava ad audit.html (inesistente) → corretto
  in reports.html
- HelpSystem: funziona correttamente in tutte le 10 pagine
  (content-header-actions presente, init() chiamato)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 11:17:04 +01:00

769 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inventario Asset - NIS2 Agile</title>
<link rel="stylesheet" href="/css/style.css">
<style>
/* ── Stats Cards ────────────────────────────────────────── */
.asset-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.asset-stat-card {
background: var(--card-bg);
border-radius: var(--border-radius);
box-shadow: var(--card-shadow);
padding: 20px;
display: flex;
align-items: center;
gap: 16px;
}
.asset-stat-icon {
width: 48px;
height: 48px;
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.asset-stat-icon svg { width: 24px; height: 24px; }
.asset-stat-icon.primary { background: var(--primary-bg); color: var(--primary); }
.asset-stat-icon.success { background: var(--secondary-bg); color: var(--secondary); }
.asset-stat-icon.danger { background: var(--danger-bg); color: var(--danger); }
.asset-stat-icon.warning { background: var(--warning-bg); color: var(--gray-600); }
.asset-stat-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--gray-800);
line-height: 1.2;
}
.asset-stat-label {
font-size: 0.8rem;
color: var(--gray-500);
}
/* ── Filters Bar ────────────────────────────────────────── */
.filters-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 20px;
align-items: center;
}
.filters-bar select {
padding: 8px 12px;
border: 1px solid var(--gray-200);
border-radius: var(--border-radius-sm);
background: var(--card-bg);
color: var(--gray-700);
font-size: 0.85rem;
cursor: pointer;
}
.filters-bar select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-bg);
}
/* ── Data Table ─────────────────────────────────────────── */
.data-table {
width: 100%;
border-collapse: collapse;
background: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--card-shadow);
}
.data-table thead {
background: var(--gray-50);
}
.data-table th {
text-align: left;
padding: 12px 16px;
font-size: 0.8rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--gray-200);
}
.data-table td {
padding: 12px 16px;
font-size: 0.875rem;
color: var(--gray-700);
border-bottom: 1px solid var(--gray-100);
}
.data-table tr:last-child td { border-bottom: none; }
.data-table tbody tr { cursor: pointer; transition: background var(--transition-fast); }
.data-table tbody tr:hover td { background: var(--gray-50); }
/* ── Criticality Badges ─────────────────────────────────── */
.criticality-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
}
.criticality-badge.critical { background: var(--danger-bg); color: var(--danger); }
.criticality-badge.high { background: #fff3e0; color: #e65100; }
.criticality-badge.medium { background: var(--warning-bg); color: #b8860b; }
.criticality-badge.low { background: var(--secondary-bg); color: var(--secondary); }
/* ── Status Badge ───────────────────────────────────────── */
.status-badge {
display: inline-flex;
align-items: center;
padding: 3px 10px;
border-radius: 20px;
font-size: 0.78rem;
font-weight: 500;
}
.status-badge.active { background: var(--secondary-bg); color: var(--secondary); }
.status-badge.maintenance { background: var(--warning-bg); color: #b8860b; }
.status-badge.decommissioned { background: var(--gray-100); color: var(--gray-500); }
/* ── Asset Detail Panel ─────────────────────────────────── */
.detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.detail-field {
margin-bottom: 4px;
}
.detail-field-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--gray-500);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 2px;
}
.detail-field-value {
font-size: 0.9rem;
color: var(--gray-800);
}
.detail-section-title {
font-size: 0.9rem;
font-weight: 600;
color: var(--gray-700);
margin: 20px 0 12px;
padding-bottom: 6px;
border-bottom: 1px solid var(--gray-200);
}
.dep-tag {
display: inline-flex;
align-items: center;
gap: 4px;
background: var(--primary-bg);
color: var(--primary);
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
margin: 2px 4px 2px 0;
}
/* ── Action Buttons ─────────────────────────────────────── */
.btn-sm {
padding: 5px 12px;
font-size: 0.8rem;
}
.btn-icon {
padding: 6px;
background: none;
border: none;
color: var(--gray-400);
cursor: pointer;
border-radius: var(--border-radius-sm);
transition: all var(--transition-fast);
}
.btn-icon:hover { color: var(--primary); background: var(--primary-bg); }
.btn-icon svg { width: 18px; height: 18px; }
/* ── Type breakdown chips ───────────────────────────────── */
.type-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.type-chip {
background: var(--gray-100);
padding: 3px 10px;
border-radius: 20px;
font-size: 0.75rem;
color: var(--gray-600);
}
.type-chip strong {
color: var(--gray-800);
margin-right: 3px;
}
/* ── Loading/Empty ──────────────────────────────────────── */
.loading-state {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.loading-state .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;
margin: 0 auto 12px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.empty-state-box {
text-align: center;
padding: 48px 20px;
color: var(--gray-400);
}
.empty-state-box svg {
width: 48px;
height: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
.empty-state-box h4 {
color: var(--gray-600);
margin-bottom: 4px;
}
/* ── Modal form enhancements ───────────────────────────── */
.form-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) {
.asset-stats { grid-template-columns: 1fr 1fr; }
.detail-grid { grid-template-columns: 1fr; }
.filters-bar { flex-direction: column; }
.form-grid-2 { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="sidebar"></aside>
<main class="main-content">
<header class="content-header">
<h2 data-i18n="assets.title">Inventario Asset</h2>
<div class="content-header-actions">
<span class="text-muted" style="font-size:0.8rem; margin-right:8px;">Art. 21.2.i NIS2</span>
<button class="btn btn-primary" onclick="showCreateAssetModal()">+ Nuovo Asset</button>
</div>
</header>
<div class="content-body">
<!-- Stats Cards -->
<div class="asset-stats" id="asset-stats">
<div class="asset-stat-card">
<div class="asset-stat-icon primary">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd"/></svg>
</div>
<div>
<div class="asset-stat-value" id="stat-total">--</div>
<div class="asset-stat-label">Totale Asset</div>
</div>
</div>
<div class="asset-stat-card">
<div class="asset-stat-icon success">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"/></svg>
</div>
<div>
<div class="asset-stat-value" id="stat-types">--</div>
<div class="asset-stat-label">Tipologie</div>
<div class="type-chips" id="stat-type-chips"></div>
</div>
</div>
<div class="asset-stat-card">
<div class="asset-stat-icon danger">
<svg viewBox="0 0 20 20" fill="currentColor"><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>
<div>
<div class="asset-stat-value" id="stat-critical">--</div>
<div class="asset-stat-label">Asset Critici</div>
</div>
</div>
<div class="asset-stat-card">
<div class="asset-stat-icon warning">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>
</div>
<div>
<div class="asset-stat-value" id="stat-decommissioned">--</div>
<div class="asset-stat-label">Dismessi</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filters-bar">
<select id="filter-type" onchange="applyFilters()">
<option value="">Tutti i tipi</option>
<option value="hardware">Hardware</option>
<option value="software">Software</option>
<option value="network">Rete</option>
<option value="data">Dati</option>
<option value="service">Servizio</option>
<option value="personnel">Personale</option>
<option value="facility">Struttura</option>
</select>
<select id="filter-criticality" onchange="applyFilters()">
<option value="">Tutte le criticita'</option>
<option value="critical">Critica</option>
<option value="high">Alta</option>
<option value="medium">Media</option>
<option value="low">Bassa</option>
</select>
<select id="filter-status" onchange="applyFilters()">
<option value="">Tutti gli stati</option>
<option value="active">Attivo</option>
<option value="maintenance">Manutenzione</option>
<option value="decommissioned">Dismesso</option>
</select>
</div>
<!-- Asset Table -->
<div id="assets-container">
<div class="loading-state">
<div class="spinner"></div>
<p>Caricamento asset...</p>
</div>
</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 typeLabels = {
hardware: 'Hardware',
software: 'Software',
network: 'Rete',
data: 'Dati',
service: 'Servizio',
personnel: 'Personale',
facility: 'Struttura'
};
const criticalityLabels = {
critical: 'Critica',
high: 'Alta',
medium: 'Media',
low: 'Bassa'
};
const statusLabels = {
active: 'Attivo',
maintenance: 'Manutenzione',
decommissioned: 'Dismesso'
};
// ── State ───────────────────────────────────────────────
let allAssets = [];
// ── Load Assets ─────────────────────────────────────────
async function loadAssets() {
const container = document.getElementById('assets-container');
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Caricamento asset...</p></div>';
const params = {};
const type = document.getElementById('filter-type').value;
const criticality = document.getElementById('filter-criticality').value;
const status = document.getElementById('filter-status').value;
if (type) params.asset_type = type;
if (criticality) params.criticality = criticality;
if (status) params.status = status;
try {
const result = await api.listAssets(params);
if (result.success) {
allAssets = result.data || [];
updateStats(allAssets);
renderAssets(allAssets);
} else {
container.innerHTML = '<div class="empty-state-box"><h4>Errore nel caricamento</h4><p>' + escapeHtml(result.message || '') + '</p></div>';
}
} catch (e) {
container.innerHTML = '<div class="empty-state-box"><h4>Errore di connessione</h4><p>Impossibile caricare gli asset.</p></div>';
}
}
function applyFilters() {
loadAssets();
}
function updateStats(assets) {
// For stats, we count from the current filtered results
// In a full implementation, stats would come from the server unfiltered
document.getElementById('stat-total').textContent = assets.length;
// Type breakdown
const typeCount = {};
let criticalCount = 0;
let decommCount = 0;
assets.forEach(a => {
const t = a.asset_type || 'unknown';
typeCount[t] = (typeCount[t] || 0) + 1;
if (a.criticality === 'critical') criticalCount++;
if (a.status === 'decommissioned') decommCount++;
});
const uniqueTypes = Object.keys(typeCount).length;
document.getElementById('stat-types').textContent = uniqueTypes;
document.getElementById('stat-critical').textContent = criticalCount;
document.getElementById('stat-decommissioned').textContent = decommCount;
// Type chips
const chipsEl = document.getElementById('stat-type-chips');
chipsEl.innerHTML = Object.entries(typeCount).map(([type, count]) =>
`<span class="type-chip"><strong>${count}</strong> ${typeLabels[type] || type}</span>`
).join('');
}
function renderAssets(assets) {
const container = document.getElementById('assets-container');
if (!assets || assets.length === 0) {
container.innerHTML = `
<div class="empty-state-box">
<svg viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm14 1a1 1 0 11-2 0 1 1 0 012 0zM2 13a2 2 0 012-2h12a2 2 0 012 2v2a2 2 0 01-2 2H4a2 2 0 01-2-2v-2zm14 1a1 1 0 11-2 0 1 1 0 012 0z" clip-rule="evenodd"/></svg>
<h4>Nessun asset trovato</h4>
<p>Registra il primo asset per costruire l'inventario.</p>
</div>`;
return;
}
let html = `
<table class="data-table">
<thead>
<tr>
<th>Nome</th>
<th>Tipo</th>
<th>Categoria</th>
<th>Criticita'</th>
<th>Owner</th>
<th>Stato</th>
<th>Azioni</th>
</tr>
</thead>
<tbody>`;
assets.forEach(asset => {
const crit = asset.criticality || 'medium';
const st = asset.status || 'active';
html += `
<tr onclick="showAssetDetail(${asset.id})" title="Visualizza dettagli">
<td style="font-weight:500;">${escapeHtml(asset.name)}</td>
<td>${typeLabels[asset.asset_type] || asset.asset_type || '-'}</td>
<td>${escapeHtml(asset.category || '-')}</td>
<td><span class="criticality-badge ${crit}">${criticalityLabels[crit] || crit}</span></td>
<td>${escapeHtml(asset.owner_name || '-')}</td>
<td><span class="status-badge ${st}">${statusLabels[st] || st}</span></td>
<td>
<button class="btn-icon" onclick="event.stopPropagation(); showEditAssetModal(${asset.id})" title="Modifica">
<svg viewBox="0 0 20 20" fill="currentColor"><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>
</td>
</tr>`;
});
html += '</tbody></table>';
container.innerHTML = html;
}
// ── Asset Detail View ───────────────────────────────────
async function showAssetDetail(id) {
try {
const result = await api.getAsset(id);
if (!result.success || !result.data) {
showNotification('Asset non trovato.', 'error');
return;
}
const a = result.data;
const crit = a.criticality || 'medium';
const st = a.status || 'active';
let depsHtml = '<span style="color:var(--gray-400);">Nessuna dipendenza</span>';
if (a.dependencies) {
let deps = a.dependencies;
if (typeof deps === 'string') {
try { deps = JSON.parse(deps); } catch(e) { deps = []; }
}
if (deps && deps.length > 0) {
depsHtml = deps.map(d => `<span class="dep-tag">ID: ${escapeHtml(String(d))}</span>`).join('');
}
}
showModal(escapeHtml(a.name), `
<div class="detail-grid">
<div class="detail-field">
<div class="detail-field-label">Tipo</div>
<div class="detail-field-value">${typeLabels[a.asset_type] || a.asset_type || '-'}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Categoria</div>
<div class="detail-field-value">${escapeHtml(a.category || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Criticita'</div>
<div class="detail-field-value"><span class="criticality-badge ${crit}">${criticalityLabels[crit] || crit}</span></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Stato</div>
<div class="detail-field-value"><span class="status-badge ${st}">${statusLabels[st] || st}</span></div>
</div>
<div class="detail-field">
<div class="detail-field-label">Owner</div>
<div class="detail-field-value">${escapeHtml(a.owner_name || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Posizione</div>
<div class="detail-field-value">${escapeHtml(a.location || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Indirizzo IP</div>
<div class="detail-field-value">${escapeHtml(a.ip_address || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Fornitore</div>
<div class="detail-field-value">${escapeHtml(a.vendor || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Versione</div>
<div class="detail-field-value">${escapeHtml(a.version || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Numero Seriale</div>
<div class="detail-field-value">${escapeHtml(a.serial_number || '-')}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Data Acquisto</div>
<div class="detail-field-value">${formatDate(a.purchase_date)}</div>
</div>
<div class="detail-field">
<div class="detail-field-label">Scadenza Garanzia</div>
<div class="detail-field-value">${formatDate(a.warranty_expiry)}</div>
</div>
</div>
${a.description ? `
<div class="detail-section-title">Descrizione</div>
<p style="font-size:0.9rem; color:var(--gray-600);">${escapeHtml(a.description)}</p>
` : ''}
<div class="detail-section-title">Dipendenze</div>
<div>${depsHtml}</div>
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Chiudi</button>
<button class="btn btn-primary" onclick="closeModal(); showEditAssetModal(${a.id})">Modifica</button>
`
});
} catch (e) {
showNotification('Errore nel caricamento dell\'asset.', 'error');
}
}
// ── Create Asset Modal ──────────────────────────────────
function showCreateAssetModal() {
showAssetModal('Nuovo Asset', {}, async function(data) {
try {
const result = await api.createAsset(data);
if (result.success) {
showNotification('Asset registrato con successo!', 'success');
closeModal();
loadAssets();
} else {
showNotification(result.message || 'Errore nella creazione.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
});
}
// ── Edit Asset Modal ────────────────────────────────────
async function showEditAssetModal(id) {
try {
const result = await api.getAsset(id);
if (!result.success || !result.data) {
showNotification('Asset non trovato.', 'error');
return;
}
showAssetModal('Modifica Asset', result.data, async function(data) {
try {
const res = await api.updateAsset(id, data);
if (res.success) {
showNotification('Asset aggiornato con successo!', 'success');
closeModal();
loadAssets();
} else {
showNotification(res.message || 'Errore nell\'aggiornamento.', 'error');
}
} catch (e) {
showNotification('Errore di connessione.', 'error');
}
});
} catch (e) {
showNotification('Errore nel caricamento.', 'error');
}
}
// ── Generic Asset Modal ─────────────────────────────────
function showAssetModal(title, asset, onSave) {
const a = asset || {};
const typeOptions = Object.entries(typeLabels).map(([val, label]) =>
`<option value="${val}" ${a.asset_type === val ? 'selected' : ''}>${label}</option>`
).join('');
const critOptions = Object.entries(criticalityLabels).map(([val, label]) =>
`<option value="${val}" ${a.criticality === val ? 'selected' : ''}>${label}</option>`
).join('');
const statusOptions = Object.entries(statusLabels).map(([val, label]) =>
`<option value="${val}" ${a.status === val ? 'selected' : ''}>${label}</option>`
).join('');
// Store the callback
window._assetSaveCallback = onSave;
showModal(title, `
<div class="form-group">
<label class="form-label">Nome *</label>
<input type="text" class="form-input" id="asset-name" value="${escapeHtml(a.name || '')}" required>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Tipo *</label>
<select class="form-select" id="asset-type">${typeOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Categoria</label>
<input type="text" class="form-input" id="asset-category" value="${escapeHtml(a.category || '')}" placeholder="Es: Server, Database...">
</div>
</div>
<div class="form-group">
<label class="form-label">Descrizione</label>
<textarea class="form-input" id="asset-description" rows="2">${escapeHtml(a.description || '')}</textarea>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Criticita'</label>
<select class="form-select" id="asset-criticality">${critOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Stato</label>
<select class="form-select" id="asset-status">${statusOptions}</select>
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Owner (ID utente)</label>
<input type="text" class="form-input" id="asset-owner" value="${escapeHtml(a.owner_user_id || '')}">
</div>
<div class="form-group">
<label class="form-label">Posizione</label>
<input type="text" class="form-input" id="asset-location" value="${escapeHtml(a.location || '')}" placeholder="Es: Data Center Roma">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Indirizzo IP</label>
<input type="text" class="form-input" id="asset-ip" value="${escapeHtml(a.ip_address || '')}" placeholder="Es: 192.168.1.100">
</div>
<div class="form-group">
<label class="form-label">Fornitore</label>
<input type="text" class="form-input" id="asset-vendor" value="${escapeHtml(a.vendor || '')}">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Versione</label>
<input type="text" class="form-input" id="asset-version" value="${escapeHtml(a.version || '')}">
</div>
<div class="form-group">
<label class="form-label">Numero Seriale</label>
<input type="text" class="form-input" id="asset-serial" value="${escapeHtml(a.serial_number || '')}">
</div>
</div>
<div class="form-grid-2">
<div class="form-group">
<label class="form-label">Data Acquisto</label>
<input type="date" class="form-input" id="asset-purchase-date" value="${a.purchase_date || ''}">
</div>
<div class="form-group">
<label class="form-label">Scadenza Garanzia</label>
<input type="date" class="form-input" id="asset-warranty" value="${a.warranty_expiry || ''}">
</div>
</div>
`, {
size: 'lg',
footer: `
<button class="btn btn-secondary" onclick="closeModal()">Annulla</button>
<button class="btn btn-primary" onclick="saveAsset()">Salva</button>
`
});
}
function saveAsset() {
const name = document.getElementById('asset-name').value.trim();
if (!name) {
showNotification('Il nome e\' obbligatorio.', 'warning');
return;
}
const data = {
name: name,
asset_type: document.getElementById('asset-type').value,
category: document.getElementById('asset-category').value.trim() || null,
description: document.getElementById('asset-description').value.trim() || null,
criticality: document.getElementById('asset-criticality').value,
owner_user_id: document.getElementById('asset-owner').value.trim() || null,
location: document.getElementById('asset-location').value.trim() || null,
ip_address: document.getElementById('asset-ip').value.trim() || null,
vendor: document.getElementById('asset-vendor').value.trim() || null,
version: document.getElementById('asset-version').value.trim() || null,
serial_number: document.getElementById('asset-serial').value.trim() || null,
purchase_date: document.getElementById('asset-purchase-date').value || null,
warranty_expiry: document.getElementById('asset-warranty').value || null,
status: document.getElementById('asset-status').value,
};
if (window._assetSaveCallback) {
window._assetSaveCallback(data);
}
}
// ── Initial Load ────────────────────────────────────────
loadAssets();
</script>
</body>
</html>