feat: contatore tempo allocato, pagina miei ticket, unione ticket (da db)

This commit is contained in:
2026-07-07 00:18:22 +02:00
parent e55cccbaa5
commit 0f012816ea
15 changed files with 1133 additions and 185 deletions
+192 -3
View File
@@ -11,9 +11,14 @@ const App = {
types: [],
},
lookupsLoaded: false,
demotivationalPhrases: [],
motivationalPhrases: [],
/** Initialize the application */
init() {
this.initTheme();
this.loadDemotivationalPhrases();
this.loadMotivationalPhrases();
Toast.init();
// Hash-based SPA router
@@ -21,9 +26,17 @@ const App = {
// 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;
@@ -49,6 +62,24 @@ const App = {
}
}
});
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
@@ -92,6 +123,11 @@ const App = {
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';
@@ -154,15 +190,16 @@ const App = {
}
try {
const [queues, states, priorities, users, types] = await Promise.all([
const [queues, states, priorities, users, types, config] = 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.lookups = { queues, states, priorities, users, types };
this.lookups = { queues, states, priorities, users, types, config };
localStorage.setItem('otrs_lookups', JSON.stringify(this.lookups));
this.lookupsLoaded = true;
} catch (err) {
@@ -266,10 +303,18 @@ const App = {
`<option value="${u.id}">${u.first_name} ${u.last_name} (${u.login})</option>`
).join('');
// Load saved agent ID or default to the first available
// 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);
@@ -279,7 +324,12 @@ const App = {
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);
}
@@ -296,6 +346,145 @@ const App = {
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 txt file */
async loadDemotivationalPhrases() {
try {
const res = await fetch('/demotivational.txt');
if (res.ok) {
const text = await res.text();
this.demotivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
}
} catch (e) {
console.warn('Failed to load demotivational phrases:', e);
}
},
/** Load motivational phrases from txt file */
async loadMotivationalPhrases() {
try {
const res = await fetch('/motivational.txt');
if (res.ok) {
const text = await res.text();
this.motivationalPhrases = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
}
} 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 percentage = Math.min(100, Math.round((todayTime / targetTime) * 100));
let phrase = "";
const isDemotivational = percentage >= 70;
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>${percentage}%</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>
<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);
}
},
};
// Start the app when DOM is ready