/** * TimeAccountingView — Storico consuntivazione * Shows a summary of notes/emails entered by the selected agent on the selected day, * with options on the right side to edit consumed time or backdate the article. * Includes interactive day pills strip, configurable WORK_DAYS, and show/hide non-working days toggle. */ const TimeAccountingView = { selectedAgentId: null, selectedDate: null, selectedArticleId: null, summaryData: null, viewMode: 'list', // 'list' | 'calendar' showAllTime: false, showNonWorkingDays: localStorage.getItem('otrs_ta_show_non_working_days') !== 'false', formatLocalISO(d) { if (!d) return ''; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}-${m}-${day}`; }, async render() { const container = document.getElementById('view-container'); container.innerHTML = '

Caricamento consuntivazione...

'; try { await App.ensureLookups(); if (!this.selectedAgentId) { this.selectedAgentId = App.currentAgentId; } if (!this.selectedDate) { this.selectedDate = this.formatLocalISO(new Date()); } await this.loadData(); } catch (err) { container.innerHTML = `

Errore caricamento: ${App.escapeHtml(err.message)}

`; } }, async loadData() { const container = document.getElementById('view-container'); const params = new URLSearchParams({ agent_id: this.selectedAgentId, date: this.selectedDate, }); if (this.showAllTime) { params.set('range', 'all'); } else if (this.viewMode === 'calendar') { params.set('range', 'month'); } let data = await App.api(`/api/tickets/time-accounting/summary?${params}`); // If non-working days are hidden, ensure selectedDate is a valid working day if (!this.showNonWorkingDays) { const workDays = (data && data.work_days) || [1, 2, 3, 4, 5]; const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 1, parts[2]); let shifted = false; while (workDays.length > 0 && !workDays.includes(d.getDay())) { d.setDate(d.getDate() - 1); shifted = true; } if (shifted) { this.selectedDate = this.formatLocalISO(d); params.set('date', this.selectedDate); data = await App.api(`/api/tickets/time-accounting/summary?${params}`); } } this.summaryData = data; // Auto-select first article if current selection is invalid const articles = data.articles || []; if (!articles.find(a => String(a.article_id) === String(this.selectedArticleId))) { this.selectedArticleId = articles.length > 0 ? articles[0].article_id : null; } const currentScrollX = window.scrollX; const currentScrollY = window.scrollY; container.innerHTML = this.buildHtml(data); this.bindEvents(); window.scrollTo(currentScrollX, currentScrollY); // Smooth scroll active day pill into center view setTimeout(() => { const activePill = container.querySelector('.ta-day-pill.active'); if (activePill) { activePill.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } }, 50); }, formatPillTime(mins) { if (!mins || mins <= 0) return '0m'; const h = Math.floor(mins / 60); const m = Math.round(mins % 60); if (h > 0 && m > 0) return `${h}h ${m}m`; if (h > 0) return `${h}h`; return `${m}m`; }, buildHtml(data) { const articles = data.articles || []; const totalMins = Math.round(data.total_minutes || 0); const targetMins = data.target_minutes || 480; const workDays = data.work_days || [1, 2, 3, 4, 5]; const hours = Math.floor(totalMins / 60); const mins = totalMins % 60; const formattedTotal = hours > 0 ? `${hours}h ${mins}m (${totalMins} min)` : `${totalMins} min`; const selectedParts = this.selectedDate.split('-').map(Number); const selectedDateObj = new Date(selectedParts[0], selectedParts[1] - 1, selectedParts[2]); const dayOfWeek = selectedDateObj.getDay(); const isWorkDay = workDays.includes(dayOfWeek); const isTargetReached = totalMins >= targetMins; let headerBadgeBg = 'var(--accent-primary)'; let headerBadgeText = `Totale: ${formattedTotal}`; if (isWorkDay) { if (isTargetReached) { headerBadgeBg = '#15803d'; headerBadgeText = `Totale: ${formattedTotal} — ✓ Soglia raggiunta`; } else { const diff = targetMins - totalMins; const diffH = Math.floor(diff / 60); const diffM = Math.round(diff % 60); const diffStr = diffH > 0 ? `${diffH}h ${diffM}m` : `${diffM}m`; headerBadgeBg = '#b91c1c'; headerBadgeText = `Totale: ${formattedTotal} — Mancano ${diffStr}`; } } const users = App.lookups.users || []; const selectedArticle = articles.find(a => String(a.article_id) === String(this.selectedArticleId)); return `
Storico consuntivazione ${headerBadgeText}
${this.viewMode === 'calendar' ? this.buildCalendarGrid(data) : ` ${this.buildDayStrip(data)}
${articles.length === 0 ? `

Nessuna nota o email trovata in questa data per l'agente selezionato.

` : articles.map(a => this.buildArticleCard(a)).join('')}
${selectedArticle ? this.buildDetailPanel(selectedArticle) : `
Seleziona una nota o email a sinistra per gestirne il tempo o retrodatare.
`}
`} `; }, buildDayStrip(data) { const selectedParts = this.selectedDate.split('-').map(Number); const selected = new Date(selectedParts[0], selectedParts[1] - 1, selectedParts[2]); const todayStr = this.formatLocalISO(new Date()); const weekdays = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; const months = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']; const dailyTotals = (data && data.daily_totals) || {}; const targetMins = (data && data.target_minutes) || 480; const workDays = (data && data.work_days) || [1, 2, 3, 4, 5]; // Generate 2 full weeks of days (15 calendar days if showing all, or 10-11 working days if hiding non-working days) const dateList = []; if (this.showNonWorkingDays) { for (let offset = -7; offset <= 7; offset++) { const d = new Date(selected); d.setDate(d.getDate() + offset); dateList.push(d); } } else { const before = []; let dPrev = new Date(selected); while (before.length < 5) { dPrev.setDate(dPrev.getDate() - 1); if (workDays.includes(dPrev.getDay())) { before.unshift(new Date(dPrev)); } } const current = new Date(selected); const after = []; let dNext = new Date(selected); while (after.length < 5) { dNext.setDate(dNext.getDate() + 1); if (workDays.includes(dNext.getDay())) { after.push(new Date(dNext)); } } dateList.push(...before, current, ...after); } const days = []; for (const d of dateList) { const isoStr = this.formatLocalISO(d); const dayOfWeek = d.getDay(); // 0 = Sun, 1 = Mon ... 6 = Sat const isWorkDay = workDays.includes(dayOfWeek); const isSelected = isoStr === this.selectedDate; const isToday = isoStr === todayStr; const dayNum = d.getDate(); const weekdayName = weekdays[dayOfWeek]; const monthName = months[d.getMonth()]; const dayMins = Math.round(dailyTotals[isoStr] || 0); const formattedTime = this.formatPillTime(dayMins); const isTargetReached = dayMins >= targetMins; // Color coding computation let bgStyle = 'var(--bg-card)'; let borderStyle = '1px solid var(--border-subtle)'; let textColor = 'var(--text-primary)'; let badgeBg = 'var(--bg-tertiary)'; let badgeColor = 'var(--text-muted)'; if (!isWorkDay) { // Non-working days (e.g., weekends) if (isSelected) { bgStyle = 'var(--accent-primary)'; borderStyle = '1px solid var(--accent-primary)'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.25)'; badgeColor = '#ffffff'; } else if (isToday) { bgStyle = 'rgba(79, 70, 229, 0.12)'; borderStyle = '1px solid var(--border-subtle)'; textColor = 'var(--accent-primary)'; } } else { // Working days (configured in WORK_DAYS) if (isTargetReached) { // Verdino (green) if (isSelected) { bgStyle = '#15803d'; borderStyle = '1px solid #166534'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.28)'; badgeColor = '#ffffff'; } else { bgStyle = 'rgba(34, 197, 94, 0.16)'; borderStyle = '1px solid rgba(34, 197, 94, 0.5)'; textColor = '#15803d'; badgeBg = 'rgba(34, 197, 94, 0.25)'; badgeColor = '#15803d'; } } else { // Rosso (red) if (isSelected) { bgStyle = '#b91c1c'; borderStyle = '1px solid #991b1b'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.28)'; badgeColor = '#ffffff'; } else { bgStyle = 'rgba(239, 68, 68, 0.14)'; borderStyle = '1px solid rgba(239, 68, 68, 0.4)'; textColor = '#b91c1c'; badgeBg = 'rgba(239, 68, 68, 0.22)'; badgeColor = '#b91c1c'; } } } days.push(` `); } return `
${days.join('')}
`; }, buildCalendarGrid(data) { const todayStr = this.formatLocalISO(new Date()); const dailyTotals = (data && data.daily_totals) || {}; const targetMins = (data && data.target_minutes) || 480; const workDays = (data && data.work_days) || [1, 2, 3, 4, 5]; const monthNames = ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre']; const headerDays = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom']; // If showAllTime is true, render all months present in dailyTotals or at least current month down to earliest month let monthsToRender = []; if (this.showAllTime) { const dates = Object.keys(dailyTotals).sort(); let startYear, startMonth; if (dates.length > 0) { const [y, m] = dates[0].split('-').map(Number); startYear = y; startMonth = m - 1; } else { const now = new Date(); startYear = now.getFullYear(); startMonth = now.getMonth(); } const now = new Date(); const endYear = now.getFullYear(); const endMonth = now.getMonth(); // Loop month by month from earliest to current/selected let curr = new Date(startYear, startMonth, 1); const end = new Date(endYear, endMonth, 1); while (curr <= end) { monthsToRender.push({ year: curr.getFullYear(), month: curr.getMonth() }); curr.setMonth(curr.getMonth() + 1); } // Reverse so newest months appear first monthsToRender.reverse(); } else { const selectedParts = this.selectedDate.split('-').map(Number); monthsToRender.push({ year: selectedParts[0], month: selectedParts[1] - 1 }); } const renderSingleMonthGrid = (mYear, mMonth) => { const firstDay = new Date(mYear, mMonth, 1); const lastDay = new Date(mYear, mMonth + 1, 0); let startDayOfWeek = firstDay.getDay() - 1; if (startDayOfWeek === -1) startDayOfWeek = 6; const cells = []; for (let i = 0; i < startDayOfWeek; i++) { cells.push(`
`); } const daysInMonth = lastDay.getDate(); for (let dayNum = 1; dayNum <= daysInMonth; dayNum++) { const d = new Date(mYear, mMonth, dayNum); const isoStr = this.formatLocalISO(d); const dayOfWeek = d.getDay(); const isWorkDay = workDays.includes(dayOfWeek); const isSelected = isoStr === this.selectedDate; const isToday = isoStr === todayStr; const dayMins = Math.round(dailyTotals[isoStr] || 0); const formattedTime = this.formatPillTime(dayMins); const isTargetReached = dayMins >= targetMins; let bgStyle = 'var(--bg-card)'; let borderStyle = '1px solid var(--border-subtle)'; let textColor = 'var(--text-primary)'; let badgeBg = 'var(--bg-tertiary)'; let badgeColor = 'var(--text-muted)'; if (!isWorkDay) { if (isSelected) { bgStyle = 'var(--accent-primary)'; borderStyle = '2px solid var(--accent-primary)'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.25)'; badgeColor = '#ffffff'; } else if (isToday) { bgStyle = 'rgba(79, 70, 229, 0.12)'; textColor = 'var(--accent-primary)'; } } else { if (isTargetReached) { if (isSelected) { bgStyle = '#15803d'; borderStyle = '2px solid #166534'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.28)'; badgeColor = '#ffffff'; } else { bgStyle = 'rgba(34, 197, 94, 0.16)'; borderStyle = '1px solid rgba(34, 197, 94, 0.5)'; textColor = '#15803d'; badgeBg = 'rgba(34, 197, 94, 0.25)'; badgeColor = '#15803d'; } } else { if (isSelected) { bgStyle = '#b91c1c'; borderStyle = '2px solid #991b1b'; textColor = '#ffffff'; badgeBg = 'rgba(255,255,255,0.28)'; badgeColor = '#ffffff'; } else { bgStyle = 'rgba(239, 68, 68, 0.14)'; borderStyle = '1px solid rgba(239, 68, 68, 0.4)'; textColor = '#b91c1c'; badgeBg = 'rgba(239, 68, 68, 0.22)'; badgeColor = '#b91c1c'; } } } const weekdays = ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab']; const weekdayName = weekdays[dayOfWeek]; cells.push(` `); } const monthTitle = `${monthNames[mMonth]} ${mYear}`; return `

📅 ${monthTitle}

${headerDays.map(h => `
${h}
`).join('')}
${cells.join('')}
`; }; const selectedParts = this.selectedDate.split('-').map(Number); const selectedYear = selectedParts[0]; const selectedMonth = selectedParts[1] - 1; const currentMonthName = `${monthNames[selectedMonth]} ${selectedYear}`; return `
${this.showAllTime ? 'Tutto lo Storico Consuntivato' : currentMonthName}
${monthsToRender.map(m => renderSingleMonthGrid(m.year, m.month)).join('')}
`; }, buildArticleCard(a) { const isSelected = String(a.article_id) === String(this.selectedArticleId); const dt = a.create_time ? new Date(a.create_time).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }) : '—'; const bodyPreview = App.escapeHtml((a.a_body || '').replace(/<[^>]*>/g, ' ').substring(0, 150)) + ((a.a_body || '').length > 150 ? '...' : ''); const timeMins = parseFloat(a.time_unit || 0); return `
#${App.escapeHtml(a.ticket_tn || a.ticket_id)} ${App.escapeHtml(a.ticket_title || '')}
${dt}
${App.escapeHtml(a.a_subject || '(senza oggetto)')}
${bodyPreview || 'Nessun testo'}
${App.escapeHtml(a.channel || a.sender_type || 'Nota')} ⏱️ ${timeMins} min
`; }, buildDetailPanel(a) { const timeMins = parseFloat(a.time_unit || 0); const localISO = (dateStr) => { if (!dateStr) return ''; const d = new Date(dateStr); return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16); }; return `
Ticket Collegato
#${App.escapeHtml(a.ticket_tn || a.ticket_id)} — ${App.escapeHtml(a.ticket_title || '')}
Oggetto Nota/Mail
${App.escapeHtml(a.a_subject || '(senza oggetto)')}
Contenuto
${App.escapeHtml(a.a_body || '')}

`; }, bindEvents() { // Toggle View Mode (List <-> Calendar Grid) document.getElementById('ta-toggle-view-mode')?.addEventListener('click', () => { this.viewMode = this.viewMode === 'list' ? 'calendar' : 'list'; this.loadData(); }); // Toggle Show All Time history document.getElementById('ta-cal-toggle-all-time')?.addEventListener('click', () => { this.showAllTime = !this.showAllTime; Toast.info(this.showAllTime ? 'Caricamento dell\'intero storico in corso...' : 'Filtro intervallo standard ripristinato'); this.loadData(); }); // Calendar month navigation document.getElementById('ta-cal-month-prev')?.addEventListener('click', () => { const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 2, 1); this.selectedDate = this.formatLocalISO(d); this.loadData(); }); document.getElementById('ta-cal-month-next')?.addEventListener('click', () => { const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1], 1); this.selectedDate = this.formatLocalISO(d); this.loadData(); }); // Agent change document.getElementById('ta-agent-select')?.addEventListener('change', (e) => { this.selectedAgentId = e.target.value; this.loadData(); }); // Date change (Calendar picker) document.getElementById('ta-date-input')?.addEventListener('change', (e) => { this.selectedDate = e.target.value; this.loadData(); }); // Today button document.getElementById('ta-btn-today')?.addEventListener('click', () => { this.selectedDate = this.formatLocalISO(new Date()); this.loadData(); }); // Toggle non-working days button document.getElementById('ta-btn-toggle-nonwork')?.addEventListener('click', () => { this.showNonWorkingDays = !this.showNonWorkingDays; localStorage.setItem('otrs_ta_show_non_working_days', this.showNonWorkingDays); this.loadData(); }); // Day pill clicks (also handles calendar cell clicks) document.querySelectorAll('.ta-day-pill[data-date]').forEach(pill => { pill.addEventListener('click', () => { const dateClicked = pill.dataset.date; if (this.viewMode === 'calendar') { this.selectedDate = dateClicked; this.viewMode = 'list'; } else { this.selectedDate = dateClicked; } this.loadData(); }); }); // Previous day button document.getElementById('ta-day-prev')?.addEventListener('click', () => { const workDays = (this.summaryData && this.summaryData.work_days) || [1, 2, 3, 4, 5]; const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 1, parts[2]); do { d.setDate(d.getDate() - 1); } while (!this.showNonWorkingDays && workDays.length > 0 && !workDays.includes(d.getDay())); this.selectedDate = this.formatLocalISO(d); this.loadData(); }); // Next day button document.getElementById('ta-day-next')?.addEventListener('click', () => { const workDays = (this.summaryData && this.summaryData.work_days) || [1, 2, 3, 4, 5]; const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 1, parts[2]); do { d.setDate(d.getDate() + 1); } while (!this.showNonWorkingDays && workDays.length > 0 && !workDays.includes(d.getDay())); this.selectedDate = this.formatLocalISO(d); this.loadData(); }); // Card selection click document.querySelectorAll('.ta-article-card').forEach(card => { card.addEventListener('click', () => { this.selectedArticleId = card.dataset.articleId; const currentScrollX = window.scrollX; const currentScrollY = window.scrollY; const container = document.getElementById('view-container'); container.innerHTML = this.buildHtml(this.summaryData); this.bindEvents(); window.scrollTo(currentScrollX, currentScrollY); }); }); // Save time action const saveTimeBtn = document.getElementById('ta-btn-save-time'); if (saveTimeBtn) { saveTimeBtn.addEventListener('click', async () => { const timeInput = document.getElementById('ta-input-time'); const timeVal = parseFloat(timeInput.value); if (isNaN(timeVal) || timeVal < 0) { Toast.warning('Inserisci un tempo valido in minuti'); return; } try { saveTimeBtn.disabled = true; await App.api(`/api/tickets/articles/${this.selectedArticleId}/time`, { method: 'PUT', body: JSON.stringify({ time_unit: timeVal }), }); Toast.success('Tempo consuntivato aggiornato!'); this.loadData(); } catch (err) { Toast.error('Errore aggiornamento tempo: ' + err.message); saveTimeBtn.disabled = false; } }); } // Save retrodate action const saveDateBtn = document.getElementById('ta-btn-save-date'); if (saveDateBtn) { saveDateBtn.addEventListener('click', async () => { const dateInput = document.getElementById('ta-input-date'); const dateVal = dateInput.value; if (!dateVal) { Toast.warning('Scegli una data/ora valida'); return; } try { saveDateBtn.disabled = true; await App.api(`/api/tickets/articles/${this.selectedArticleId}/retrodata-article`, { method: 'POST', body: JSON.stringify({ create_time: dateVal }), }); Toast.success('Articolo retrodatato con successo!'); this.loadData(); } catch (err) { Toast.error('Errore retrodatazione: ' + err.message); saveDateBtn.disabled = false; } }); } // Show Chart Modal action document.getElementById('ta-cal-show-chart')?.addEventListener('click', () => { this.openChartModal(); }); }, async openChartModal() { const existingModal = document.getElementById('ta-chart-modal'); if (existingModal) existingModal.remove(); const users = App.lookups.users || []; // Saved banned agent IDs (semitrasparenti e bloccati) let bannedAgentIds = []; try { const savedBanned = localStorage.getItem('otrs_ta_chart_banned_agents'); if (savedBanned) bannedAgentIds = JSON.parse(savedBanned); } catch (_) {} if (!Array.isArray(bannedAgentIds)) bannedAgentIds = []; // Saved selected agent IDs or default to current agent let activeAgentIds = []; try { const saved = localStorage.getItem('otrs_ta_chart_agents'); if (saved) activeAgentIds = JSON.parse(saved); } catch (_) {} if (!Array.isArray(activeAgentIds) || activeAgentIds.length === 0) { activeAgentIds = [String(this.selectedAgentId || App.currentAgentId)]; } // Filter activeAgentIds to remove any banned agents activeAgentIds = activeAgentIds.filter(id => !bannedAgentIds.includes(id)); // Saved range or default to 'all' let selectedRange = localStorage.getItem('otrs_ta_chart_range') || 'all'; const modal = document.createElement('div'); modal.id = 'ta-chart-modal'; modal.className = 'modal'; modal.style.cssText = 'display:flex; position:fixed; z-index:10000; inset:0; background:rgba(0,0,0,0.65); align-items:center; justify-content:center; padding:20px; backdrop-filter:blur(4px);'; const isDarkMode = document.body.classList.contains('theme-dark'); const textColor = isDarkMode ? '#cbd5e1' : '#334155'; const gridColor = isDarkMode ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.06)'; // Paletta di colori vivaci per gli agenti nel grafico const colorPalette = [ '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#f97316', '#6366f1', '#14b8a6', '#a855f7' ]; modal.innerHTML = ` `; document.body.appendChild(modal); const closeModal = () => { if (this.chartInstance) { this.chartInstance.destroy(); this.chartInstance = null; } modal.remove(); }; modal.querySelector('#ta-chart-modal-close').onclick = closeModal; modal.onclick = (e) => { if (e.target === modal) closeModal(); }; // Function to fetch data and refresh chart instance const refreshChartData = async () => { const loadingEl = modal.querySelector('#ta-chart-loading'); if (loadingEl) loadingEl.style.display = 'flex'; const checkboxes = modal.querySelectorAll('.ta-agent-checkbox:checked'); const selectedIds = Array.from(checkboxes).map(c => c.value); // Save state to localStorage localStorage.setItem('otrs_ta_chart_agents', JSON.stringify(selectedIds)); localStorage.setItem('otrs_ta_chart_range', selectedRange); const countEl = modal.querySelector('#ta-chart-agents-count'); if (countEl) countEl.textContent = selectedIds.length; if (selectedIds.length === 0) { if (this.chartInstance) { this.chartInstance.destroy(); this.chartInstance = null; } if (loadingEl) loadingEl.style.display = 'none'; return; } try { const params = new URLSearchParams({ agent_ids: selectedIds.join(','), date: this.selectedDate, range: selectedRange, }); const data = await App.api(`/api/tickets/time-accounting/summary?${params}`); const dailyTotalsByAgent = data.daily_totals_by_agent || {}; const targetMins = data.target_minutes || 480; const targetHoursVal = +(targetMins / 60).toFixed(2); // Compute set of all dates across selected agents let allDates = []; Object.values(dailyTotalsByAgent).forEach(agentTotals => { Object.keys(agentTotals).forEach(d => { if (!allDates.includes(d)) allDates.push(d); }); }); allDates.sort(); // If numeric range (e.g. last 30, 90, 180, 365 days), slice dates if (selectedRange !== 'all' && !isNaN(parseInt(selectedRange, 10))) { const limitDays = parseInt(selectedRange, 10); const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - limitDays); const cutoffIso = this.formatLocalISO(cutoff); allDates = allDates.filter(d => d >= cutoffIso); } const labels = allDates.map(d => { const parts = d.split('-'); return `${parts[2]}/${parts[1]}`; }); // Update total hours badges for all agents in the sidebar list users.forEach((u) => { const uId = String(u.id); const agentTotals = dailyTotalsByAgent[uId] || {}; let totalMinsForAgent = 0; allDates.forEach(d => { totalMinsForAgent += (agentTotals[d] || 0); }); const totalHoursVal = (totalMinsForAgent / 60).toFixed(1); const badgeEl = modal.querySelector(`.ta-agent-toggle-item[data-id="${uId}"] .ta-agent-total-badge`); if (badgeEl) { badgeEl.textContent = `${totalHoursVal}h`; if (totalMinsForAgent > 0) { badgeEl.style.color = 'var(--accent-primary)'; badgeEl.style.borderColor = 'rgba(59, 130, 246, 0.3)'; } else { badgeEl.style.color = 'var(--text-muted)'; badgeEl.style.borderColor = 'var(--border-subtle)'; } } }); // Build dataset per agent const datasets = selectedIds.map((agId) => { const userObj = users.find(u => String(u.id) === String(agId)); const idx = users.findIndex(u => String(u.id) === String(agId)); const color = colorPalette[idx >= 0 ? idx % colorPalette.length : 0]; const agentTotals = dailyTotalsByAgent[agId] || {}; const hoursData = allDates.map(d => { const mins = agentTotals[d] || 0; return +(mins / 60).toFixed(2); }); return { label: userObj ? `${userObj.first_name} ${userObj.last_name}` : `Agente #${agId}`, data: hoursData, borderColor: color, backgroundColor: color + '22', borderWidth: 2, fill: false, tension: 0.25, pointBackgroundColor: color, pointRadius: 1.5, pointHoverRadius: 5, }; }); // Add Target reference line datasets.push({ label: 'Soglia Target (Ore)', data: allDates.map(() => targetHoursVal), borderColor: '#ef4444', borderDash: [5, 5], borderWidth: 2, fill: false, pointRadius: 0, }); if (this.chartInstance) { this.chartInstance.destroy(); } if (window.Chart) { const ctx = modal.querySelector('#ta-chart-canvas').getContext('2d'); this.chartInstance = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets, }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false, }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { const val = context.raw || 0; const mins = Math.round(val * 60); const h = Math.floor(mins / 60); const m = mins % 60; const timeFormatted = h > 0 ? `${h}h ${m}m` : `${m}m`; return `${context.dataset.label}: ${timeFormatted} (${val}h)`; } } } }, scales: { x: { grid: { color: gridColor }, ticks: { color: textColor, font: { family: 'Inter', size: 10 } } }, y: { grid: { color: gridColor }, beginAtZero: true, ticks: { color: textColor, font: { family: 'Inter', size: 10 }, callback: function(value) { return value + 'h'; } } } } } }); } } catch (err) { console.error('Error loading chart data:', err); } finally { if (loadingEl) loadingEl.style.display = 'none'; } }; // Range selector change event const rangeSelect = modal.querySelector('#ta-chart-range-select'); if (rangeSelect) { rangeSelect.onchange = (e) => { selectedRange = e.target.value; refreshChartData(); }; } // Agent checkboxes toggle events modal.querySelectorAll('.ta-agent-checkbox').forEach(chk => { chk.onchange = (e) => { const itemLabel = e.target.closest('.ta-agent-toggle-item'); if (itemLabel) { itemLabel.style.background = e.target.checked ? 'var(--bg-tertiary)' : 'transparent'; itemLabel.style.borderColor = e.target.checked ? 'var(--border-subtle)' : 'transparent'; } refreshChartData(); }; }); // Agent Ban/Unban toggle button events 🚫 modal.querySelectorAll('.ta-agent-ban-btn').forEach(btn => { btn.onclick = (e) => { e.stopPropagation(); const uId = String(btn.dataset.id); const itemLabel = btn.closest('.ta-agent-toggle-item'); const chk = itemLabel ? itemLabel.querySelector('.ta-agent-checkbox') : null; const isCurrentlyBanned = bannedAgentIds.includes(uId); if (isCurrentlyBanned) { bannedAgentIds = bannedAgentIds.filter(id => id !== uId); if (itemLabel) { itemLabel.classList.remove('banned'); itemLabel.style.opacity = '1'; itemLabel.style.pointerEvents = 'auto'; } if (chk) { chk.disabled = false; } btn.title = 'Blocca agente (Escludi dal grafico e dal pulsante Tutti)'; btn.style.opacity = '0.5'; } else { bannedAgentIds.push(uId); if (itemLabel) { itemLabel.classList.add('banned'); itemLabel.style.opacity = '0.4'; itemLabel.style.pointerEvents = 'none'; itemLabel.style.background = 'transparent'; itemLabel.style.borderColor = 'transparent'; } if (chk) { chk.checked = false; chk.disabled = true; } btn.title = 'Sblocca agente'; btn.style.opacity = '1'; } localStorage.setItem('otrs_ta_chart_banned_agents', JSON.stringify(bannedAgentIds)); refreshChartData(); }; }); // Select All agents button (ignores banned agents) const selectAllBtn = modal.querySelector('#ta-chart-select-all'); if (selectAllBtn) { selectAllBtn.onclick = () => { // Only select/unselect checkboxes that are not disabled/banned const activeCheckboxes = modal.querySelectorAll('.ta-agent-checkbox:not(:disabled)'); const allChecked = Array.from(activeCheckboxes).every(c => c.checked); activeCheckboxes.forEach(chk => { chk.checked = !allChecked; const itemLabel = chk.closest('.ta-agent-toggle-item'); if (itemLabel) { itemLabel.style.background = chk.checked ? 'var(--bg-tertiary)' : 'transparent'; itemLabel.style.borderColor = chk.checked ? 'var(--border-subtle)' : 'transparent'; } }); refreshChartData(); }; } // Initial render refreshChartData(); } };