967 lines
34 KiB
JavaScript
967 lines
34 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,
|
|
demotivationalPhrases: [],
|
|
motivationalPhrases: [],
|
|
drafts: {},
|
|
tabs: [],
|
|
lastListView: '#/tickets',
|
|
|
|
loadTabs() {
|
|
try {
|
|
const saved = localStorage.getItem('otrs_turbo_tabs');
|
|
if (saved) this.tabs = JSON.parse(saved);
|
|
const savedDrafts = localStorage.getItem('otrs_turbo_drafts');
|
|
if (savedDrafts) this.drafts = JSON.parse(savedDrafts);
|
|
} catch (e) {}
|
|
this.renderTabs();
|
|
},
|
|
|
|
saveTabs() {
|
|
localStorage.setItem('otrs_turbo_tabs', JSON.stringify(this.tabs));
|
|
this.renderTabs();
|
|
},
|
|
|
|
saveDraft(ticketId, draft) {
|
|
if (!this.drafts[ticketId]) this.drafts[ticketId] = {};
|
|
this.drafts[ticketId][draft.type] = draft;
|
|
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
|
|
},
|
|
|
|
getDraft(ticketId, type) {
|
|
return this.drafts[ticketId] ? this.drafts[ticketId][type] : null;
|
|
},
|
|
|
|
clearDraft(ticketId, type) {
|
|
if (this.drafts[ticketId] && this.drafts[ticketId][type]) {
|
|
delete this.drafts[ticketId][type];
|
|
if (Object.keys(this.drafts[ticketId]).length === 0) {
|
|
delete this.drafts[ticketId];
|
|
}
|
|
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
|
|
this.renderTabs();
|
|
}
|
|
},
|
|
|
|
openTab(id, tn, title) {
|
|
const exists = this.tabs.find(t => String(t.id) === String(id));
|
|
if (!exists) {
|
|
this.tabs.push({ id, tn, title });
|
|
this.saveTabs();
|
|
}
|
|
},
|
|
|
|
addTabWithoutRedirect(id, tn, title) {
|
|
const exists = this.tabs.find(t => String(t.id) === String(id));
|
|
if (!exists) {
|
|
this.tabs.push({ id, tn, title });
|
|
this.saveTabs();
|
|
} else {
|
|
if (title && exists.title !== title) {
|
|
exists.title = title;
|
|
this.saveTabs();
|
|
} else {
|
|
this.renderTabs();
|
|
}
|
|
}
|
|
},
|
|
|
|
closeTab(id, e) {
|
|
if (e) e.stopPropagation();
|
|
this.tabs = this.tabs.filter(t => String(t.id) !== String(id));
|
|
this.saveTabs();
|
|
|
|
// Clear drafts for closed tab
|
|
delete this.drafts[id];
|
|
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
|
|
|
|
const hash = window.location.hash;
|
|
if (hash === `#/tickets/${id}`) {
|
|
if (this.tabs.length > 0) {
|
|
window.location.hash = `#/tickets/${this.tabs[this.tabs.length - 1].id}`;
|
|
} else {
|
|
window.location.hash = '#/tickets';
|
|
}
|
|
}
|
|
},
|
|
|
|
renderTabs() {
|
|
const bar = document.getElementById('tabs-bar');
|
|
if (!bar) return;
|
|
if (this.tabs.length === 0) {
|
|
bar.style.display = 'none';
|
|
this.updateHeaderHeight();
|
|
return;
|
|
}
|
|
bar.style.display = 'flex';
|
|
|
|
const currentHash = window.location.hash;
|
|
|
|
bar.innerHTML = this.tabs.map(t => {
|
|
const isActive = currentHash === `#/tickets/${t.id}`;
|
|
const hasEmailDraft = this.getDraft(t.id, 'email');
|
|
const emailIconHtml = hasEmailDraft ? `<span style="color:#22c55e; margin-right:4px;" title="Bozza email presente">✉️</span>` : '';
|
|
const displayTitle = t.title ? (t.title.length > 25 ? t.title.substring(0, 22) + '...' : t.title) : `#${t.tn}`;
|
|
return `
|
|
<div class="tab-item ${isActive ? 'active' : ''}" onclick="window.location.hash = '#/tickets/${t.id}'" title="${App.escapeHtml(t.title || '')}">
|
|
${emailIconHtml}
|
|
<span>${App.escapeHtml(displayTitle)}</span>
|
|
<button class="tab-close" onclick="App.closeTab(${t.id}, event)">✕</button>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
this.updateHeaderHeight();
|
|
},
|
|
|
|
updateHeaderHeight() {
|
|
const topbar = document.getElementById('topbar');
|
|
if (topbar) {
|
|
const height = topbar.offsetHeight;
|
|
document.documentElement.style.setProperty('--topbar-total-height', `${height}px`);
|
|
}
|
|
},
|
|
|
|
get currentAgentId() {
|
|
return parseInt(localStorage.getItem('activeAgentId') || '1', 10);
|
|
},
|
|
|
|
/** Initialize the application */
|
|
init() {
|
|
this.initTheme();
|
|
this.loadDemotivationalPhrases();
|
|
this.loadMotivationalPhrases();
|
|
this.loadTabs();
|
|
Toast.init();
|
|
|
|
// Hash-based SPA router
|
|
window.addEventListener('hashchange', () => this.route());
|
|
|
|
// Global search
|
|
const searchInput = document.getElementById('global-search');
|
|
const searchClearBtn = document.getElementById('global-search-clear');
|
|
if (searchInput) {
|
|
const toggleClearBtn = () => {
|
|
if (searchClearBtn) {
|
|
searchClearBtn.style.display = searchInput.value.trim() ? 'flex' : 'none';
|
|
}
|
|
};
|
|
|
|
let timeout;
|
|
searchInput.addEventListener('input', () => {
|
|
toggleClearBtn();
|
|
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();
|
|
}
|
|
}
|
|
});
|
|
|
|
if (searchClearBtn) {
|
|
searchClearBtn.addEventListener('click', () => {
|
|
searchInput.value = '';
|
|
searchClearBtn.style.display = 'none';
|
|
searchInput.focus();
|
|
const hash = window.location.hash;
|
|
if (hash.startsWith('#/tickets') && !hash.includes('/new') && !hash.match(/#\/tickets\/\d+/)) {
|
|
TicketListView.currentPage = 1;
|
|
TicketListView.render();
|
|
} else {
|
|
window.location.hash = '#/tickets';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initial state of clear button
|
|
toggleClearBtn();
|
|
}
|
|
|
|
// 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
|
|
window.addEventListener('resize', () => this.updateHeaderHeight());
|
|
setTimeout(() => this.updateHeaderHeight(), 100);
|
|
|
|
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');
|
|
|
|
if (hash === '#/tickets' || hash === '#/tickets/my') {
|
|
this.lastListView = hash;
|
|
}
|
|
|
|
// Save draft for previous ticket before routing
|
|
if (typeof TicketDetailView !== 'undefined' && TicketDetailView.ticketId) {
|
|
TicketDetailView.saveDraft();
|
|
if (typeof EmailCompose !== 'undefined') {
|
|
EmailCompose.saveDraft();
|
|
const overlay = document.getElementById('email-compose-overlay');
|
|
if (overlay) overlay.remove();
|
|
}
|
|
}
|
|
|
|
this.renderTabs();
|
|
this.updateHeaderHeight();
|
|
|
|
const container = document.getElementById('view-container');
|
|
if (container) {
|
|
if (hash.match(/^#\/tickets\/(\d+)$/)) {
|
|
container.classList.add('ticket-view-active');
|
|
} else {
|
|
container.classList.remove('ticket-view-active');
|
|
}
|
|
}
|
|
|
|
// 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/my') {
|
|
document.getElementById('nav-tickets-my')?.classList.add('active');
|
|
titleEl.textContent = 'Ticket a mio carico';
|
|
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 === '#/tickets/groups') {
|
|
document.getElementById('nav-ticket-groups')?.classList.add('active');
|
|
titleEl.textContent = 'Gruppi ticket';
|
|
TicketGroupsView.render();
|
|
|
|
} else if (hash === '#/activity') {
|
|
document.getElementById('nav-activity')?.classList.add('active');
|
|
titleEl.textContent = 'Storico Attività';
|
|
ActivityLogView.render();
|
|
|
|
} else if (hash === '#/mail-management') {
|
|
document.getElementById('nav-mail-management')?.classList.add('active');
|
|
titleEl.textContent = 'Gestione Mail';
|
|
MailManagementView.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);
|
|
if (!this.lookups.config || this.lookups.config.autoTimeMinHour === undefined || !this.lookups.customerUsers || !this.lookups.customer_users_version_1) {
|
|
throw new Error('Outdated config cache (missing autoTimeMinHour or customerUsers)');
|
|
}
|
|
this.lookupsLoaded = true;
|
|
return;
|
|
} catch (e) {
|
|
console.warn('Failed to parse cached lookups, reloading...', e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
const [queues, states, priorities, users, types, config, customerUsers] = await Promise.all([
|
|
this.api('/api/queues'),
|
|
this.api('/api/states'),
|
|
this.api('/api/priorities'),
|
|
this.api('/api/users'),
|
|
this.api('/api/types'),
|
|
this.api('/api/config').catch(() => ({ defaultAgentLogin: '' })),
|
|
this.api('/api/customer-users/search?q=').catch(() => []),
|
|
]);
|
|
|
|
this.lookups = { queues, states, priorities, users, types, config, customerUsers, customer_users_version_1: true };
|
|
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 {
|
|
// Sync LDAP customer users to local DB cache
|
|
await this.api('/api/customer-users/sync', { method: 'POST' });
|
|
|
|
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 config-specified default agent, or first available
|
|
const savedAgentId = localStorage.getItem('activeAgentId');
|
|
const defaultAgentLogin = this.lookups.config ? this.lookups.config.defaultAgentLogin : null;
|
|
const defaultAgent = defaultAgentLogin
|
|
? (this.lookups.users || []).find(u => u.login === defaultAgentLogin)
|
|
: null;
|
|
|
|
if (savedAgentId && (this.lookups.users || []).some(u => String(u.id) === String(savedAgentId))) {
|
|
select.value = savedAgentId;
|
|
} else if (defaultAgent) {
|
|
select.value = defaultAgent.id;
|
|
localStorage.setItem('activeAgentId', select.value);
|
|
} 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}`);
|
|
this.updateDailyTimer();
|
|
this.route();
|
|
});
|
|
|
|
// Initial update
|
|
this.updateDailyTimer();
|
|
} 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;
|
|
},
|
|
|
|
/** Initialize and handle theme selection */
|
|
initTheme() {
|
|
const themeSelect = document.getElementById('theme-select');
|
|
const savedTheme = localStorage.getItem('app-theme') || 'light';
|
|
|
|
// Apply the saved theme class to body
|
|
this.applyThemeClass(savedTheme);
|
|
|
|
if (themeSelect) {
|
|
themeSelect.value = savedTheme;
|
|
themeSelect.addEventListener('change', () => {
|
|
const selectedTheme = themeSelect.value;
|
|
this.applyThemeClass(selectedTheme);
|
|
localStorage.setItem('app-theme', selectedTheme);
|
|
Toast.success(`Tema cambiato in: ${themeSelect.options[themeSelect.selectedIndex].text}`);
|
|
});
|
|
}
|
|
},
|
|
|
|
/** Helper to apply theme classes to document.body */
|
|
applyThemeClass(theme) {
|
|
// Remove any existing theme- classes
|
|
document.body.className = document.body.className
|
|
.split(' ')
|
|
.filter(c => !c.startsWith('theme-'))
|
|
.join(' ');
|
|
|
|
if (theme !== 'light') {
|
|
document.body.classList.add(`theme-${theme}`);
|
|
}
|
|
},
|
|
|
|
/** Load demotivational phrases from SQLite cache */
|
|
async loadDemotivationalPhrases() {
|
|
try {
|
|
const res = await fetch('/api/dashboard/phrases?tipo=demotivational');
|
|
if (res.ok) {
|
|
this.demotivationalPhrases = await res.json();
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load demotivational phrases:', e);
|
|
}
|
|
},
|
|
|
|
/** Load motivational phrases from SQLite cache */
|
|
async loadMotivationalPhrases() {
|
|
try {
|
|
const res = await fetch('/api/dashboard/phrases?tipo=motivational');
|
|
if (res.ok) {
|
|
this.motivationalPhrases = await res.json();
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load motivational phrases:', e);
|
|
}
|
|
},
|
|
|
|
/** Fetch daily time accounting and update sidebar display */
|
|
async updateDailyTimer() {
|
|
const timerEl = document.getElementById('daily-timer');
|
|
if (!timerEl) return;
|
|
|
|
try {
|
|
await this.ensureLookups();
|
|
const targetTime = this.lookups.config?.dailyTargetTime || 480;
|
|
|
|
const data = await this.api('/api/users/time-today');
|
|
const todayTime = typeof data.totalToday === 'number' ? data.totalToday : 0;
|
|
const remaining = Math.max(0, targetTime - todayTime);
|
|
const mathematicalPercentage = Math.round((todayTime / targetTime) * 100);
|
|
const hasOverperformance = mathematicalPercentage > 100;
|
|
const percentage = Math.min(100, mathematicalPercentage);
|
|
|
|
const brandEl = document.querySelector('.sidebar-brand');
|
|
if (brandEl) {
|
|
if (hasOverperformance) {
|
|
brandEl.classList.add('glow');
|
|
} else {
|
|
brandEl.classList.remove('glow');
|
|
}
|
|
}
|
|
|
|
if (timerEl) {
|
|
if (hasOverperformance) {
|
|
timerEl.classList.add('overperformance-alarm');
|
|
} else {
|
|
timerEl.classList.remove('overperformance-alarm');
|
|
}
|
|
}
|
|
|
|
const minTimeStr = this.lookups.config?.autoTimeMinHour || '18:00';
|
|
const [minHour, minMin] = minTimeStr.split(':').map(x => parseInt(x, 10));
|
|
|
|
const now = new Date();
|
|
const currentHour = now.getHours();
|
|
const currentMin = now.getMinutes();
|
|
|
|
let isPastTime = false;
|
|
if (currentHour > minHour) {
|
|
isPastTime = true;
|
|
} else if (currentHour === minHour && currentMin >= minMin) {
|
|
isPastTime = true;
|
|
}
|
|
|
|
const triggerAction = async (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const confirmed = await this.confirm(
|
|
'Consuntivazione Automatica',
|
|
`Sei sicuro di voler effettuare la consuntivazione automatica di ${remaining} minuti rimanenti di oggi? Verrà creato un ticket chiuso a tuo carico.`
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
try {
|
|
Toast.success('Consuntivazione in corso...');
|
|
const res = await this.api('/api/tickets/auto-time', { method: 'POST' });
|
|
this.updateDailyTimer();
|
|
this.route();
|
|
await this.alert('Consuntivazione Completata', res.message || 'La consuntivazione automatica è stata completata con successo.');
|
|
} catch (err) {
|
|
Toast.error('Errore consuntivazione automatica: ' + err.message);
|
|
}
|
|
};
|
|
|
|
if (isPastTime && remaining > 0) {
|
|
if (brandEl) {
|
|
brandEl.classList.add('clickable-auto-time');
|
|
brandEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m)`);
|
|
brandEl.onclick = triggerAction;
|
|
}
|
|
|
|
if (timerEl) {
|
|
timerEl.classList.add('clickable-auto-time');
|
|
//timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`);
|
|
timerEl.onclick = triggerAction;
|
|
}
|
|
} else {
|
|
if (brandEl) {
|
|
brandEl.classList.remove('clickable-auto-time');
|
|
brandEl.removeAttribute('title');
|
|
brandEl.onclick = null;
|
|
}
|
|
|
|
if (timerEl) {
|
|
timerEl.classList.remove('clickable-auto-time');
|
|
timerEl.removeAttribute('title');
|
|
timerEl.onclick = null;
|
|
}
|
|
}
|
|
|
|
let phrase = "";
|
|
const phraseThreshold = this.lookups.config?.phraseThreshold || 70;
|
|
const isDemotivational = percentage >= phraseThreshold;
|
|
|
|
if (isDemotivational) {
|
|
if (this.demotivationalPhrases.length > 0) {
|
|
const daySeed = new Date().getDate() + todayTime;
|
|
const idx = Math.floor(Math.abs(Math.sin(daySeed) * this.demotivationalPhrases.length));
|
|
phrase = this.demotivationalPhrases[idx % this.demotivationalPhrases.length];
|
|
} else {
|
|
phrase = "Hai fatto fin troppo lavoro per oggi. Smetti.";
|
|
}
|
|
} else {
|
|
if (this.motivationalPhrases.length > 0) {
|
|
const daySeed = new Date().getDate() + todayTime;
|
|
const idx = Math.floor(Math.abs(Math.sin(daySeed) * this.motivationalPhrases.length));
|
|
phrase = this.motivationalPhrases[idx % this.motivationalPhrases.length];
|
|
} else {
|
|
phrase = "Continua così! Stai andando alla grande.";
|
|
}
|
|
}
|
|
|
|
timerEl.innerHTML = `
|
|
<div class="timer-display" style="display:flex; justify-content:space-between; align-items:center; width:100%; font-size:0.75rem; font-weight:500;">
|
|
<span>⏱ ${todayTime} / ${targetTime} m</span>
|
|
<span style="color:var(--text-muted);">|</span>
|
|
<span>rimanenti: ${remaining} m</span>
|
|
</div>
|
|
<div class="timer-tooltip">
|
|
<div class="timer-tooltip-border"></div>
|
|
<div style="font-size:0.75rem; font-weight:600; color:var(--text-primary); margin-bottom:4px; display:flex; justify-content:space-between;">
|
|
<span>Progresso Giornaliero</span>
|
|
<strong>${mathematicalPercentage}%</strong>
|
|
</div>
|
|
<div style="background:var(--border-light); border-radius:var(--radius-full); height:10px; width:100%; overflow:hidden; border:1px solid var(--border-subtle);">
|
|
<div style="width:${percentage}%; background:linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); height:100%; border-radius:inherit; transition: width 0.3s ease;"></div>
|
|
</div>
|
|
${hasOverperformance ? `
|
|
<div style="font-size:0.72rem; color:var(--error); font-weight:700; line-height:1.3; text-transform:uppercase; margin-top:8px; border-top:1px solid var(--border-subtle); padding-top:8px; text-align:center; animation: pulse 1.5s infinite;">
|
|
⚠️ Rilevata una overperformance allontanarsi dalla postazione immediatamente
|
|
</div>
|
|
` : ''}
|
|
<div style="font-size:0.72rem; color:var(--text-secondary); line-height:1.35; font-style:italic; margin-top:6px; border-top:1px solid var(--border-subtle); padding-top:6px; text-align:center;">
|
|
"${phrase}"
|
|
</div>
|
|
</div>
|
|
`;
|
|
// Update sidebar counts as well
|
|
this.updateSidebarBadges();
|
|
} catch (err) {
|
|
console.warn('Failed to update daily timer:', err);
|
|
}
|
|
},
|
|
|
|
/** Fetch dashboard stats to update sidebar counts */
|
|
async updateSidebarBadges() {
|
|
try {
|
|
const stats = await this.api('/api/dashboard/stats');
|
|
|
|
const badge = document.getElementById('open-ticket-count');
|
|
if (badge) {
|
|
badge.textContent = stats.total_open > 0 ? stats.total_open : '';
|
|
}
|
|
|
|
const myBadge = document.getElementById('my-ticket-count');
|
|
if (myBadge) {
|
|
myBadge.textContent = stats.total_my_open > 0 ? stats.total_my_open : '';
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to update sidebar badges:', e);
|
|
}
|
|
},
|
|
|
|
/** Custom confirm dialog in the center of the screen */
|
|
confirm(title, message, options = {}) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement('div');
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
|
|
overlay.style.backdropFilter = 'blur(4px)';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.zIndex = '99999';
|
|
overlay.style.opacity = '0';
|
|
overlay.style.transition = 'opacity 0.2s ease';
|
|
|
|
const card = document.createElement('div');
|
|
card.style.background = 'var(--bg-card, #1e1e2e)';
|
|
card.style.border = '1px solid var(--border-subtle, #313244)';
|
|
card.style.borderRadius = 'var(--radius-lg, 12px)';
|
|
card.style.padding = 'var(--space-lg, 24px)';
|
|
card.style.width = '100%';
|
|
card.style.maxWidth = '400px';
|
|
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
|
|
card.style.transform = 'scale(0.9)';
|
|
card.style.transition = 'transform 0.2s ease';
|
|
card.className = 'confirm-dialog-card';
|
|
|
|
card.innerHTML = `
|
|
<h3 style="margin-top: 0; margin-bottom: var(--space-xs, 8px); color: var(--text-primary, #cdd6f4); font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
|
<p style="margin-bottom: var(--space-lg, 24px); color: var(--text-muted, #a6adc8); font-size: 0.95rem; line-height: 1.5;">${message}</p>
|
|
<div style="display: flex; gap: var(--space-sm, 12px); justify-content: flex-end;">
|
|
<button id="confirm-btn-cancel" class="btn btn-ghost" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px);">${options.cancelText || 'Annulla'}</button>
|
|
<button id="confirm-btn-ok" class="btn btn-danger" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px); font-weight: 500; cursor: pointer;">${options.confirmText || 'Conferma'}</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(card);
|
|
document.body.appendChild(overlay);
|
|
|
|
// Trigger animations
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '1';
|
|
card.style.transform = 'scale(1)';
|
|
});
|
|
|
|
const cleanUp = (result) => {
|
|
overlay.style.opacity = '0';
|
|
card.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
overlay.remove();
|
|
resolve(result);
|
|
}, 200);
|
|
};
|
|
|
|
const btnCancel = card.querySelector('#confirm-btn-cancel');
|
|
const btnOk = card.querySelector('#confirm-btn-ok');
|
|
|
|
btnCancel.addEventListener('click', () => cleanUp(false));
|
|
btnOk.addEventListener('click', () => cleanUp(true));
|
|
|
|
// Close on backdrop click
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) cleanUp(false);
|
|
});
|
|
});
|
|
},
|
|
|
|
/** Custom alert dialog in the center of the screen */
|
|
alert(title, message, options = {}) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement('div');
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
|
|
overlay.style.backdropFilter = 'blur(4px)';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.zIndex = '99999';
|
|
overlay.style.opacity = '0';
|
|
overlay.style.transition = 'opacity 0.2s ease';
|
|
|
|
const card = document.createElement('div');
|
|
card.style.background = 'var(--bg-card, #1e1e2e)';
|
|
card.style.border = '1px solid var(--border-subtle, #313244)';
|
|
card.style.borderRadius = 'var(--radius-lg, 12px)';
|
|
card.style.padding = 'var(--space-lg, 24px)';
|
|
card.style.width = '100%';
|
|
card.style.maxWidth = '400px';
|
|
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
|
|
card.style.transform = 'scale(0.9)';
|
|
card.style.transition = 'transform 0.2s ease';
|
|
card.className = 'alert-dialog-card';
|
|
|
|
card.innerHTML = `
|
|
<h3 style="margin-top: 0; margin-bottom: var(--space-xs, 8px); color: var(--text-primary, #cdd6f4); font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
|
<p style="margin-bottom: var(--space-lg, 24px); color: var(--text-muted, #a6adc8); font-size: 0.95rem; line-height: 1.5;">${message}</p>
|
|
<div style="display: flex; justify-content: flex-end;">
|
|
<button id="alert-btn-ok" class="btn btn-primary" style="height: 36px; font-size: 0.9rem; padding: 0 20px; border-radius: var(--radius-md, 6px); font-weight: 500; cursor: pointer;">${options.okText || 'OK'}</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(card);
|
|
document.body.appendChild(overlay);
|
|
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '1';
|
|
card.style.transform = 'scale(1)';
|
|
});
|
|
|
|
const cleanUp = () => {
|
|
overlay.style.opacity = '0';
|
|
card.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
overlay.remove();
|
|
resolve();
|
|
}, 200);
|
|
};
|
|
|
|
card.querySelector('#alert-btn-ok').addEventListener('click', cleanUp);
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) cleanUp();
|
|
});
|
|
});
|
|
},
|
|
|
|
/** Custom prompt dialog in the center of the screen */
|
|
prompt(title, message, options = {}) {
|
|
return new Promise((resolve) => {
|
|
const overlay = document.createElement('div');
|
|
overlay.style.position = 'fixed';
|
|
overlay.style.top = '0';
|
|
overlay.style.left = '0';
|
|
overlay.style.width = '100vw';
|
|
overlay.style.height = '100vh';
|
|
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
|
|
overlay.style.backdropFilter = 'blur(4px)';
|
|
overlay.style.display = 'flex';
|
|
overlay.style.alignItems = 'center';
|
|
overlay.style.justifyContent = 'center';
|
|
overlay.style.zIndex = '99999';
|
|
overlay.style.opacity = '0';
|
|
overlay.style.transition = 'opacity 0.2s ease';
|
|
|
|
const card = document.createElement('div');
|
|
card.style.background = 'var(--bg-card, #1e1e2e)';
|
|
card.style.border = '1px solid var(--border-subtle, #313244)';
|
|
card.style.borderRadius = 'var(--radius-lg, 12px)';
|
|
card.style.padding = 'var(--space-lg, 24px)';
|
|
card.style.width = '100%';
|
|
card.style.maxWidth = '400px';
|
|
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
|
|
card.style.transform = 'scale(0.9)';
|
|
card.style.transition = 'transform 0.2s ease';
|
|
card.className = 'prompt-dialog-card';
|
|
|
|
card.innerHTML = `
|
|
<h3 style="margin-top: 0; margin-bottom: var(--space-xs, 8px); color: var(--text-primary, #cdd6f4); font-size: 1.2rem; font-weight: 600;">${title}</h3>
|
|
<p style="margin-bottom: var(--space-sm, 12px); color: var(--text-muted, #a6adc8); font-size: 0.95rem; line-height: 1.5;">${message}</p>
|
|
<input type="text" id="prompt-input-field" class="form-input" value="${options.defaultValue || ''}" placeholder="${options.placeholder || ''}" style="width: 100%; margin-bottom: var(--space-md, 16px); box-sizing: border-box;" />
|
|
<div style="display: flex; gap: var(--space-sm, 12px); justify-content: flex-end;">
|
|
<button id="prompt-btn-cancel" class="btn btn-ghost" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px);">${options.cancelText || 'Annulla'}</button>
|
|
<button id="prompt-btn-ok" class="btn btn-primary" style="height: 36px; font-size: 0.9rem; padding: 0 16px; border-radius: var(--radius-md, 6px); font-weight: 500; cursor: pointer;">${options.confirmText || 'Salva'}</button>
|
|
</div>
|
|
`;
|
|
|
|
overlay.appendChild(card);
|
|
document.body.appendChild(overlay);
|
|
|
|
const input = card.querySelector('#prompt-input-field');
|
|
|
|
requestAnimationFrame(() => {
|
|
overlay.style.opacity = '1';
|
|
card.style.transform = 'scale(1)';
|
|
setTimeout(() => {
|
|
if (input) input.focus();
|
|
}, 50);
|
|
});
|
|
|
|
const cleanUp = (resultValue) => {
|
|
overlay.style.opacity = '0';
|
|
card.style.transform = 'scale(0.9)';
|
|
setTimeout(() => {
|
|
overlay.remove();
|
|
resolve(resultValue);
|
|
}, 200);
|
|
};
|
|
|
|
const btnCancel = card.querySelector('#prompt-btn-cancel');
|
|
const btnOk = card.querySelector('#prompt-btn-ok');
|
|
|
|
btnCancel.addEventListener('click', () => cleanUp(null));
|
|
btnOk.addEventListener('click', () => {
|
|
const val = input ? input.value : '';
|
|
cleanUp(val);
|
|
});
|
|
|
|
if (input) {
|
|
input.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
btnOk.click();
|
|
} else if (e.key === 'Escape') {
|
|
btnCancel.click();
|
|
}
|
|
});
|
|
}
|
|
|
|
overlay.addEventListener('click', (e) => {
|
|
if (e.target === overlay) cleanUp(null);
|
|
});
|
|
});
|
|
},
|
|
};
|
|
|
|
// Start the app when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => App.init());
|