/** * OTRS Turbo — Core Application * SPA router, API client, lookup cache, and utility functions. */ const App = { lookups: { queues: [], states: [], priorities: [], users: [], types: [], }, lookupsLoaded: false, /** Initialize the application */ init() { Toast.init(); // Hash-based SPA router window.addEventListener('hashchange', () => this.route()); // Global search const searchInput = document.getElementById('global-search'); if (searchInput) { let timeout; searchInput.addEventListener('input', () => { clearTimeout(timeout); timeout = setTimeout(() => { const hash = window.location.hash; if (hash.startsWith('#/tickets') && !hash.includes('/new') && !hash.match(/#\/tickets\/\d+/)) { TicketListView.currentPage = 1; TicketListView.render(); } else { // Navigate to ticket list with search window.location.hash = '#/tickets'; } }, 350); }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); const hash = window.location.hash; if (!hash.startsWith('#/tickets') || hash.includes('/new') || hash.match(/#\/tickets\/\d+/)) { window.location.hash = '#/tickets'; } else { TicketListView.currentPage = 1; TicketListView.render(); } } }); } // Check DB connection this.checkConnection(); // Init active agent selector this.initAgentSelector(); // Initial route if (!window.location.hash || window.location.hash === '#/') { window.location.hash = '#/dashboard'; } else { this.route(); } }, /** Route based on current hash */ route() { const hash = window.location.hash || '#/dashboard'; const titleEl = document.getElementById('page-title'); // Update active nav link document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); }); if (hash === '#/dashboard') { document.getElementById('nav-dashboard')?.classList.add('active'); titleEl.textContent = 'Dashboard'; DashboardView.render(); } else if (hash === '#/tickets') { document.getElementById('nav-tickets')?.classList.add('active'); titleEl.textContent = 'Ticket'; TicketListView.render(); } else if (hash === '#/tickets/new') { document.getElementById('nav-new-ticket')?.classList.add('active'); titleEl.textContent = 'Nuovo Ticket'; TicketCreateView.render(); } else if (hash.match(/^#\/tickets\/(\d+)$/)) { const id = hash.match(/^#\/tickets\/(\d+)$/)[1]; document.getElementById('nav-tickets')?.classList.add('active'); titleEl.textContent = `Ticket #${id}`; TicketDetailView.render(id); } else { // Fallback to dashboard window.location.hash = '#/dashboard'; } }, /** API fetch wrapper */ async api(url, options = {}) { const activeAgentId = localStorage.getItem('activeAgentId') || '1'; const defaultOptions = { headers: { 'Content-Type': 'application/json', 'X-Agent-ID': activeAgentId, }, }; const headers = { ...defaultOptions.headers, ...(options.headers || {}) }; const response = await fetch(url, { ...defaultOptions, ...options, headers }); if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.error || errData.message || `HTTP ${response.status}`); } return response.json(); }, /** Ensure lookup data is loaded (cached) */ async ensureLookups() { if (this.lookupsLoaded) return; try { const [queues, states, priorities, users, types] = await Promise.all([ this.api('/api/queues'), this.api('/api/states'), this.api('/api/priorities'), this.api('/api/users'), this.api('/api/types'), ]); this.lookups = { queues, states, priorities, users, types }; this.lookupsLoaded = true; } catch (err) { console.error('Failed to load lookups:', err); throw err; } }, /** Check database connection */ async checkConnection() { const dot = document.getElementById('connection-status'); const text = document.getElementById('connection-text'); try { await this.api('/api/queues'); dot.classList.add('connected'); dot.classList.remove('error'); text.textContent = 'DB connesso'; } catch (err) { dot.classList.add('error'); dot.classList.remove('connected'); text.textContent = 'DB non raggiungibile'; Toast.error('Impossibile connettersi al database OTRS'); } }, /** Escape HTML to prevent XSS */ escapeHtml(str) { const div = document.createElement('div'); div.textContent = str || ''; return div.innerHTML; }, /** Format date for display */ formatDate(dateStr) { if (!dateStr) return '—'; try { const d = new Date(dateStr); return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return dateStr; } }, /** Format date+time for display */ formatDateTime(dateStr) { if (!dateStr) return '—'; try { const d = new Date(dateStr); return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch { return dateStr; } }, /** Initialize active agent dropdown */ async initAgentSelector() { const select = document.getElementById('active-agent-select'); if (!select) return; try { // Ensure lookups are loaded await this.ensureLookups(); // Populate select dropdown select.innerHTML = (this.lookups.users || []).map(u => `` ).join(''); // Load saved agent ID or default to the first available const savedAgentId = localStorage.getItem('activeAgentId'); if (savedAgentId && (this.lookups.users || []).some(u => String(u.id) === String(savedAgentId))) { select.value = savedAgentId; } else if ((this.lookups.users || []).length > 0) { select.value = this.lookups.users[0].id; localStorage.setItem('activeAgentId', select.value); } // Handle dropdown change event select.addEventListener('change', () => { localStorage.setItem('activeAgentId', select.value); Toast.success(`Agente attivo cambiato: ${select.options[select.selectedIndex].text}`); }); } catch (err) { console.error('Failed to init agent selector:', err); } }, /** Map priority name to a 1-5 index for styling */ priorityIndex(name) { if (!name) return 3; const lower = name.toLowerCase(); if (lower.includes('very low') || lower.includes('1')) return 1; if (lower.includes('low') || lower.includes('2')) return 2; if (lower.includes('normal') || lower.includes('3')) return 3; if (lower.includes('high') && !lower.includes('very') || lower.includes('4')) return 4; if (lower.includes('very high') || lower.includes('5')) return 5; return 3; }, }; // Start the app when DOM is ready document.addEventListener('DOMContentLoaded', () => App.init());