188 lines
8.4 KiB
JavaScript
188 lines
8.4 KiB
JavaScript
/**
|
|
* Dashboard View
|
|
* Shows stats overview, distribution charts, and recent tickets.
|
|
*/
|
|
const DashboardView = {
|
|
async render() {
|
|
const container = document.getElementById('view-container');
|
|
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento dashboard...</p></div>';
|
|
|
|
try {
|
|
const stats = await App.api('/api/dashboard/stats');
|
|
|
|
const maxByState = Math.max(...(stats.by_state || []).map(s => parseInt(s.count)), 1);
|
|
const maxByPriority = Math.max(...(stats.by_priority || []).map(s => parseInt(s.count)), 1);
|
|
const maxByQueue = Math.max(...(stats.by_queue || []).map(s => parseInt(s.count)), 1);
|
|
|
|
container.innerHTML = `
|
|
<!-- Stats Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card accent">
|
|
<div class="stat-label">Ticket Aperti</div>
|
|
<div class="stat-value">${stats.total_open}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Creati Oggi</div>
|
|
<div class="stat-value">${stats.created_today}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Creati Settimana</div>
|
|
<div class="stat-value">${stats.created_this_week}</div>
|
|
</div>
|
|
<div class="stat-card ${stats.escalated > 0 ? 'danger' : ''}">
|
|
<div class="stat-label">Escalated</div>
|
|
<div class="stat-value">${stats.escalated}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Distributions -->
|
|
<div class="distribution-section">
|
|
<div class="card">
|
|
<div class="card-title">Per Stato</div>
|
|
${(stats.by_state || []).map(s => `
|
|
<div class="dist-bar-container">
|
|
<div class="dist-bar-header">
|
|
<span class="dist-bar-label">${s.state}</span>
|
|
<span class="dist-bar-value">${s.count}</span>
|
|
</div>
|
|
<div class="dist-bar-track">
|
|
<div class="dist-bar-fill" style="width: ${(parseInt(s.count) / maxByState * 100).toFixed(1)}%"></div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
${(stats.by_state || []).length === 0 ? '<p style="color:var(--text-tertiary);font-size:0.85rem;">Nessun dato</p>' : ''}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">Per Priorità</div>
|
|
${(stats.by_priority || []).map((p, idx) => `
|
|
<div class="dist-bar-container">
|
|
<div class="dist-bar-header">
|
|
<span class="dist-bar-label">${p.priority}</span>
|
|
<span class="dist-bar-value">${p.count}</span>
|
|
</div>
|
|
<div class="dist-bar-track">
|
|
<div class="dist-bar-fill" style="width: ${(parseInt(p.count) / maxByPriority * 100).toFixed(1)}%; background: linear-gradient(90deg, var(--priority-${idx + 1}-text, var(--accent-primary)), var(--accent-secondary));"></div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
${(stats.by_priority || []).length === 0 ? '<p style="color:var(--text-tertiary);font-size:0.85rem;">Nessun dato</p>' : ''}
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">Per Coda (Top 10)</div>
|
|
${(stats.by_queue || []).map(q => `
|
|
<div class="dist-bar-container">
|
|
<div class="dist-bar-header">
|
|
<span class="dist-bar-label">${q.queue}</span>
|
|
<span class="dist-bar-value">${q.count}</span>
|
|
</div>
|
|
<div class="dist-bar-track">
|
|
<div class="dist-bar-fill" style="width: ${(parseInt(q.count) / maxByQueue * 100).toFixed(1)}%"></div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
${(stats.by_queue || []).length === 0 ? '<p style="color:var(--text-tertiary);font-size:0.85rem;">Nessun dato</p>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Tickets -->
|
|
<div class="card">
|
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:var(--space-md); flex-wrap:wrap; gap:var(--space-xs);">
|
|
<div class="card-title" style="margin-bottom:0;">Ticket Recenti</div>
|
|
<div style="display:flex; align-items:center; gap:var(--space-xs); font-size:0.85rem; color:var(--text-secondary);">
|
|
<span>Mostra:</span>
|
|
<select id="dashboard-preview-limit" class="form-select" style="padding: 4px 28px 4px 8px; font-size: 0.8rem; height: 28px; min-width: 70px; margin: 0; background-position: right 8px center; border-color: var(--border-light);">
|
|
<option value="5" ${stats.preview_limit === 5 ? 'selected' : ''}>5</option>
|
|
<option value="10" ${stats.preview_limit === 10 ? 'selected' : ''}>10</option>
|
|
<option value="20" ${stats.preview_limit === 20 ? 'selected' : ''}>20</option>
|
|
<option value="50" ${stats.preview_limit === 50 ? 'selected' : ''}>50</option>
|
|
<option value="100" ${stats.preview_limit === 100 ? 'selected' : ''}>100</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
${(stats.recent_tickets || []).length > 0 ? `
|
|
<table class="recent-tickets-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Numero</th>
|
|
<th>Titolo</th>
|
|
<th>Stato</th>
|
|
<th>Priorità</th>
|
|
<th>Coda</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${stats.recent_tickets.map(t => `
|
|
<tr onclick="window.location.hash='#/tickets/${t.id}'">
|
|
<td><span class="ticket-tn">${t.tn}</span></td>
|
|
<td class="ticket-title-cell">${App.escapeHtml(t.title || '')}</td>
|
|
<td><span class="badge badge-state" data-state-type="${(t.state_name || '').toLowerCase()}">${t.state_name}</span></td>
|
|
<td><span class="badge badge-priority" data-priority="${t.priority_name ? App.priorityIndex(t.priority_name) : 3}">${t.priority_name}</span></td>
|
|
<td><span class="badge badge-queue">${t.queue_name}</span></td>
|
|
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDate(t.create_time)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
` : `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📭</div>
|
|
<div class="empty-state-text">Nessun ticket recente</div>
|
|
</div>
|
|
`}
|
|
</div>
|
|
`;
|
|
|
|
// Animate bars after render
|
|
requestAnimationFrame(() => {
|
|
document.querySelectorAll('.dist-bar-fill').forEach(bar => {
|
|
const w = bar.style.width;
|
|
bar.style.width = '0%';
|
|
requestAnimationFrame(() => { bar.style.width = w; });
|
|
});
|
|
});
|
|
|
|
// Bind preview limit change event
|
|
const limitSelect = document.getElementById('dashboard-preview-limit');
|
|
if (limitSelect) {
|
|
limitSelect.addEventListener('change', async () => {
|
|
const newLimit = parseInt(limitSelect.value, 10);
|
|
try {
|
|
await App.api('/api/dashboard/settings', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ preview_limit: newLimit }),
|
|
});
|
|
Toast.success(`Limite anteprima aggiornato a ${newLimit} ticket!`);
|
|
this.render();
|
|
} catch (err) {
|
|
Toast.error('Errore durante il salvataggio dell\'impostazione: ' + err.message);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update open ticket count in sidebar badge
|
|
const badge = document.getElementById('open-ticket-count');
|
|
if (badge) {
|
|
badge.textContent = stats.total_open > 0 ? stats.total_open : '';
|
|
}
|
|
|
|
// Update my ticket count in sidebar badge
|
|
const myBadge = document.getElementById('my-ticket-count');
|
|
if (myBadge) {
|
|
myBadge.textContent = stats.total_my_open > 0 ? stats.total_my_open : '';
|
|
}
|
|
|
|
} catch (err) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⚠️</div>
|
|
<div class="empty-state-text">Errore caricamento dashboard</div>
|
|
<div class="empty-state-sub">${App.escapeHtml(err.message)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
};
|