/** * Dashboard View * Shows stats overview, customizable advanced ticket trends chart, distribution charts, and recent tickets. */ // Helper to calculate date boundaries function getDatesForPeriod(period, customFromVal, customToVal) { let start_date, end_date; const today = new Date(); if (period === 'last_week') { const past = new Date(); past.setDate(today.getDate() - 7); start_date = formatDateString(past); end_date = formatDateString(today); } else if (period === 'last_month') { const past = new Date(); past.setDate(today.getDate() - 30); start_date = formatDateString(past); end_date = formatDateString(today); } else { start_date = customFromVal || formatDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)); end_date = customToVal || formatDateString(today); } return { start_date, end_date }; } function formatDateString(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dateVal = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${dateVal}`; } /** * Dashboard View * Shows stats overview, customizable advanced ticket trends chart, distribution charts, and recent tickets. */ // Helper to calculate date boundaries function getDatesForPeriod(period, customFromVal, customToVal) { let start_date, end_date; const today = new Date(); if (period === 'last_week') { const past = new Date(); past.setDate(today.getDate() - 7); start_date = formatDateString(past); end_date = formatDateString(today); } else if (period === 'last_month') { const past = new Date(); past.setDate(today.getDate() - 30); start_date = formatDateString(past); end_date = formatDateString(today); } else { start_date = customFromVal || formatDateString(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)); end_date = customToVal || formatDateString(today); } return { start_date, end_date }; } function formatDateString(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const dateVal = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${dateVal}`; } const DashboardView = { chartInstance: null, activeLines: [], async render() { const container = document.getElementById('view-container'); container.innerHTML = '

Caricamento dashboard...

'; try { // Ensure lookup tables are loaded in App.lookups await App.ensureLookups(); 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); // Load saved chart period preferences const savedPeriod = localStorage.getItem('otrs_chart_period') || 'last_week'; const savedDateFrom = localStorage.getItem('otrs_chart_date_from') || ''; const savedDateTo = localStorage.getItem('otrs_chart_date_to') || ''; container.innerHTML = `
Andamento Ticket
Da: A:
Ticket Aperti
${stats.total_open}
Creati Oggi
${stats.created_today}
Creati Settimana
${stats.created_this_week}
Escalated
${stats.escalated}
Per Stato
${(stats.by_state || []).map(s => `
${s.state} ${s.count}
`).join('')} ${(stats.by_state || []).length === 0 ? '

Nessun dato

' : ''}
Per Priorità
${(stats.by_priority || []).map((p, idx) => `
${p.priority} ${p.count}
`).join('')} ${(stats.by_priority || []).length === 0 ? '

Nessun dato

' : ''}
Per Coda (Top 10)
${(stats.by_queue || []).map(q => `
${q.queue} ${q.count}
`).join('')} ${(stats.by_queue || []).length === 0 ? '

Nessun dato

' : ''}
Ticket Recenti
Mostra:
${(stats.recent_tickets || []).length > 0 ? ` ${stats.recent_tickets.map(t => ` `).join('')}
Numero Titolo Stato Priorità Coda Data
${t.tn} ${App.escapeHtml(t.title || '')} ${t.state_name} ${t.priority_name} ${t.queue_name} ${App.formatDate(t.create_time)}
` : `
📭
Nessun ticket recente
`}
`; // Draw custom Chart.js lines this.updateChartData(); // Bind controls listeners const periodSelect = document.getElementById('chart-period-preset'); const customDatesDiv = document.getElementById('chart-custom-dates'); const dateFromInput = document.getElementById('chart-date-from'); const dateToInput = document.getElementById('chart-date-to'); periodSelect.addEventListener('change', () => { if (periodSelect.value === 'custom') { customDatesDiv.style.display = 'flex'; } else { customDatesDiv.style.display = 'none'; this.updateChartData(); } }); dateFromInput.addEventListener('change', () => { if (dateFromInput.value && dateToInput.value) { this.updateChartData(); } }); dateToInput.addEventListener('change', () => { if (dateFromInput.value && dateToInput.value) { this.updateChartData(); } }); // Bind Modal listeners const modal = document.getElementById('chart-series-modal'); document.getElementById('configure-series-btn').addEventListener('click', () => { modal.style.display = 'flex'; this.loadModalSeriesList(); }); document.getElementById('export-series-excel-btn').addEventListener('click', () => { const periodSelect = document.getElementById('chart-period-preset'); const dateFromInput = document.getElementById('chart-date-from'); const dateToInput = document.getElementById('chart-date-to'); const { start_date, end_date } = getDatesForPeriod(periodSelect.value, dateFromInput.value, dateToInput.value); const visibleSeriesIds = []; if (this.chartInstance && this.activeLines) { this.activeLines.forEach((line) => { const datasetIndex = this.chartInstance.data.datasets.findIndex(ds => ds.label === line.name); if (datasetIndex !== -1 && this.chartInstance.isDatasetVisible(datasetIndex)) { visibleSeriesIds.push(line.id); } }); } if (visibleSeriesIds.length === 0) { Toast.error('Nessuna serie visibile da esportare!'); return; } const idsParam = visibleSeriesIds.join(','); window.open(`/api/dashboard/export-excel?start_date=${start_date}&end_date=${end_date}&series_ids=${idsParam}`, '_blank'); }); document.getElementById('chart-series-modal-close').addEventListener('click', () => { modal.style.display = 'none'; this.closeSeriesForm(); }); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.style.display = 'none'; this.closeSeriesForm(); } }); document.getElementById('btn-add-chart-series').addEventListener('click', () => { this.openSeriesForm(); }); document.getElementById('btn-series-form-cancel').addEventListener('click', () => { this.closeSeriesForm(); }); document.getElementById('modal-series-form').addEventListener('submit', async (e) => { e.preventDefault(); const seriesId = document.getElementById('form-series-id').value; const name = document.getElementById('form-series-name').value; const color = document.getElementById('form-series-color').value; const statuses = this.getSelectedIds('form-select-statuses'); const types = this.getSelectedIds('form-select-types'); const queues = this.getSelectedIds('form-select-queues'); const owners = this.getSelectedIds('form-select-owners'); const responsibles = this.getSelectedIds('form-select-responsibles'); const bypass_state_filter = document.getElementById('form-series-bypass-state').checked ? 1 : 0; // Preserve current visibility if editing let is_visible = 1; if (seriesId) { const existing = this.activeLines.find(s => s.id === parseInt(seriesId, 10)); if (existing) is_visible = existing.is_visible; } const payload = { name, statuses, types, queues, owners, responsibles, color, is_visible, bypass_state_filter }; try { if (seriesId) { await App.api(`/api/dashboard/chart-lines/${seriesId}`, { method: 'PUT', body: JSON.stringify(payload) }); Toast.success('Serie aggiornata!'); } else { await App.api('/api/dashboard/chart-lines', { method: 'POST', body: JSON.stringify(payload) }); Toast.success('Serie creata!'); } this.closeSeriesForm(); this.loadModalSeriesList(); this.updateChartData(); } catch (err) { Toast.error('Errore durante il salvataggio: ' + err.message); } }); // 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 = `
⚠️
Errore caricamento dashboard
${App.escapeHtml(err.message)}
`; } }, async updateChartData() { const periodPreset = document.getElementById('chart-period-preset').value; const customFrom = document.getElementById('chart-date-from').value; const customTo = document.getElementById('chart-date-to').value; // Save selections localStorage.setItem('otrs_chart_period', periodPreset); if (customFrom) localStorage.setItem('otrs_chart_date_from', customFrom); if (customTo) localStorage.setItem('otrs_chart_date_to', customTo); const { start_date, end_date } = getDatesForPeriod(periodPreset, customFrom, customTo); try { const data = await App.api(`/api/dashboard/chart-data?start_date=${start_date}&end_date=${end_date}`); this.activeLines = data.lines; this.drawChart(data.labels, data.lines); } catch (err) { console.error("Error loading chart data:", err); const wrapper = document.querySelector('.chart-canvas-wrapper'); if (wrapper) { wrapper.innerHTML = `
⚠️ Errore durante il caricamento dei dati del grafico ${App.escapeHtml(err.message)}
`; } } }, drawChart(labels, lines) { const canvas = document.getElementById('dashboard-ticket-chart'); if (!canvas) return; if (this.chartInstance) { this.chartInstance.destroy(); } const isDarkMode = document.body.className.includes('theme-') && !document.body.className.includes('theme-light'); const textColor = isDarkMode ? '#a6adc8' : '#4b5563'; const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)'; const datasets = lines.map((line) => { const color = line.color || '#4f46e5'; return { label: line.name, data: line.data, borderColor: color, backgroundColor: color + '15', borderWidth: 2.5, tension: 0.3, fill: true, pointBackgroundColor: color, pointBorderColor: '#fff', pointHoverRadius: 6, pointRadius: 4, }; }); this.chartInstance = new Chart(canvas, { type: 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { color: textColor, font: { family: 'Inter', size: 12, weight: '500' }, padding: 15, } }, tooltip: { mode: 'index', intersect: false, backgroundColor: isDarkMode ? '#1e1e2e' : '#fff', titleColor: isDarkMode ? '#cdd6f4' : '#0f172a', bodyColor: isDarkMode ? '#a6adc8' : '#475569', borderColor: isDarkMode ? '#313244' : '#e2e8f0', borderWidth: 1, padding: 10, titleFont: { family: 'Inter', weight: '600' }, bodyFont: { family: 'Inter' }, } }, scales: { x: { grid: { color: gridColor, drawBorder: false, }, ticks: { color: textColor, font: { family: 'Inter', size: 10 }, maxRotation: 45, minRotation: 0, } }, y: { grid: { color: gridColor, drawBorder: false, }, ticks: { color: textColor, font: { family: 'Inter', size: 10 }, stepSize: 1, precision: 0, }, beginAtZero: true } } } }); }, async loadModalSeriesList() { const listContainer = document.getElementById('modal-series-list'); if (!listContainer) return; listContainer.innerHTML = '
'; try { const lines = await App.api('/api/dashboard/chart-lines'); this.activeLines = lines; listContainer.innerHTML = lines.map(line => `
${App.escapeHtml(line.name)} ${line.is_default ? '(Predefinita)' : ''}
${!line.is_default ? `` : ''}
`).join(''); // Bind actions listContainer.querySelectorAll('.btn-edit-series').forEach(btn => { btn.addEventListener('click', () => { const lineId = parseInt(btn.dataset.id); const line = this.activeLines.find(l => l.id === lineId); if (line) this.openSeriesForm(line); }); }); listContainer.querySelectorAll('.btn-toggle-visibility').forEach(btn => { btn.addEventListener('click', async () => { const lineId = parseInt(btn.dataset.id); const series = this.activeLines.find(l => l.id === lineId); if (series) { const nextVisibility = series.is_visible ? 0 : 1; try { await App.api(`/api/dashboard/chart-lines/${lineId}`, { method: 'PUT', body: JSON.stringify({ name: series.name, statuses: series.statuses, types: series.types, queues: series.queues, owners: series.owners, responsibles: series.responsibles, color: series.color, is_visible: nextVisibility }) }); Toast.success(nextVisibility ? 'Serie attivata!' : 'Serie disattivata!'); this.loadModalSeriesList(); this.updateChartData(); } catch (err) { Toast.error('Errore durante l\'aggiornamento dello stato: ' + err.message); } } }); }); listContainer.querySelectorAll('.btn-delete-series').forEach(btn => { btn.addEventListener('click', async () => { const lineId = parseInt(btn.dataset.id); const confirmed = await App.confirm('Elimina serie', 'Sei sicuro di voler eliminare questa serie dal grafico?'); if (!confirmed) return; try { await App.api(`/api/dashboard/chart-lines/${lineId}`, { method: 'DELETE' }); Toast.success('Serie eliminata con successo!'); this.loadModalSeriesList(); this.updateChartData(); } catch (err) { Toast.error('Errore durante l\'eliminazione: ' + err.message); } }); }); } catch (err) { listContainer.innerHTML = `
Errore: ${App.escapeHtml(err.message)}
`; } }, openSeriesForm(line = null) { const listSection = document.getElementById('modal-series-list-section'); const formSection = document.getElementById('modal-series-form'); listSection.style.display = 'none'; formSection.style.display = 'flex'; const idInput = document.getElementById('form-series-id'); const nameInput = document.getElementById('form-series-name'); const colorInput = document.getElementById('form-series-color'); const statuses = line ? line.statuses : []; const types = line ? line.types : []; const queues = line ? line.queues : []; const owners = line ? line.owners : []; const responsibles = line ? line.responsibles : []; idInput.value = line ? line.id : ''; nameInput.value = line ? line.name : ''; colorInput.value = line ? line.color : '#4f46e5'; document.getElementById('form-series-bypass-state').checked = line ? !!line.bypass_state_filter : false; this.renderMultiselect('form-select-statuses', App.lookups.states || [], statuses); this.renderMultiselect('form-select-types', App.lookups.types || [], types); this.renderMultiselect('form-select-queues', App.lookups.queues || [], queues); this.renderMultiselect('form-select-owners', App.lookups.users || [], owners, 'user'); this.renderMultiselect('form-select-responsibles', App.lookups.users || [], responsibles, 'user'); }, closeSeriesForm() { const listSection = document.getElementById('modal-series-list-section'); const formSection = document.getElementById('modal-series-form'); if (listSection && formSection) { listSection.style.display = 'block'; formSection.style.display = 'none'; } }, renderMultiselect(containerId, items, selectedIds, type = 'normal') { const container = document.getElementById(containerId); if (!container) return; const sorted = [...items].sort((a, b) => { const nameA = type === 'user' ? `${a.last_name || ''} ${a.first_name || ''}` : (a.name || ''); const nameB = type === 'user' ? `${b.last_name || ''} ${b.first_name || ''}` : (b.name || ''); return nameA.localeCompare(nameB, 'it', { sensitivity: 'base' }); }); container.innerHTML = sorted.map(item => { const id = item.id; const isSelected = selectedIds.includes(id) || selectedIds.includes(String(id)) || selectedIds.includes(parseInt(id)); let label = item.name; if (type === 'user') { label = `${item.first_name || ''} ${item.last_name || ''} (${item.login || ''})`; } return `
${App.escapeHtml(label)}
`; }).join(''); // Bind item click container.querySelectorAll('.multiselect-item').forEach(el => { el.addEventListener('click', () => { el.classList.toggle('selected'); }); }); }, getSelectedIds(containerId) { const container = document.getElementById(containerId); if (!container) return []; return Array.from(container.querySelectorAll('.multiselect-item.selected')).map(el => { const id = el.dataset.id; return isNaN(parseInt(id)) ? id : parseInt(id); }); } };