245 lines
7.3 KiB
JavaScript
245 lines
7.3 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();
|
|
|
|
// 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 =>
|
|
`<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());
|