- 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>
769 lines
36 KiB
HTML
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>
|