Files
otrs-turbo/public/js/app.js
T

303 lines
9.2 KiB
JavaScript

/**
* 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();
// Bind refresh lookups button
const refreshBtn = document.getElementById('refresh-lookups-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.refreshLookups());
}
// Initial route
if (!window.location.hash || window.location.hash === '#/') {
window.location.hash = '#/dashboard';
} else {
this.route();
}
},
/** Route based on current hash */
route() {
const fullHash = window.location.hash || '#/dashboard';
const hash = fullHash.split('?')[0];
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 === '#/tickets/bulk') {
document.getElementById('nav-bulk-tickets')?.classList.add('active');
titleEl.textContent = 'Apertura Massiva Ticket';
TicketBulkView.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(forceRefresh = false) {
if (this.lookupsLoaded && !forceRefresh) return;
// Check localStorage cache first if not forcing refresh
if (!forceRefresh) {
const cached = localStorage.getItem('otrs_lookups');
if (cached) {
try {
this.lookups = JSON.parse(cached);
this.lookupsLoaded = true;
return;
} catch (e) {
console.warn('Failed to parse cached lookups, reloading...', e);
}
}
}
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 };
localStorage.setItem('otrs_lookups', JSON.stringify(this.lookups));
this.lookupsLoaded = true;
} catch (err) {
console.error('Failed to load lookups:', err);
throw err;
}
},
/** Force refresh of lookups */
async refreshLookups() {
const btn = document.getElementById('refresh-lookups-btn');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0;"></div>';
}
try {
await this.ensureLookups(true);
await this.initAgentSelector();
Toast.success('Dati locali (code, utenti, ecc.) aggiornati con successo!');
// If we are on a view that needs lookups, we can re-render it
const hash = window.location.hash;
if (hash === '#/tickets/bulk') {
TicketBulkView.render();
} else if (hash === '#/tickets/new') {
TicketCreateView.render();
}
} catch (err) {
Toast.error('Errore durante l\'aggiornamento: ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = origHtml;
}
}
},
/** 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 =>
`<option value="${u.id}">${u.first_name} ${u.last_name} (${u.login})</option>`
).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());