feat: contatore tempo allocato, pagina miei ticket, unione ticket (da db)
This commit is contained in:
+192
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user