Prima importazione
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* 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());
|
||||
Reference in New Issue
Block a user