/** * 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, 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, }); const 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; } container.innerHTML = this.buildHtml(data); this.bindEvents(); // 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.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 7 days before and 7 days after selected date (15 days total) const days = []; for (let offset = -7; offset <= 7; offset++) { const d = new Date(selected); d.setDate(d.getDate() + offset); 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; // Filter out non-working days if toggle is off, unless it's currently selected if (!isWorkDay && !this.showNonWorkingDays && !isSelected) { continue; } 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('')}
`; }, 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() { // 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); const container = document.getElementById('view-container'); container.innerHTML = this.buildHtml(this.summaryData); this.bindEvents(); }); // Day pill clicks document.querySelectorAll('.ta-day-pill[data-date]').forEach(pill => { pill.addEventListener('click', () => { this.selectedDate = pill.dataset.date; this.loadData(); }); }); // Previous day button document.getElementById('ta-day-prev')?.addEventListener('click', () => { const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 1, parts[2]); d.setDate(d.getDate() - 1); this.selectedDate = this.formatLocalISO(d); this.loadData(); }); // Next day button document.getElementById('ta-day-next')?.addEventListener('click', () => { const parts = this.selectedDate.split('-').map(Number); const d = new Date(parts[0], parts[1] - 1, parts[2]); d.setDate(d.getDate() + 1); 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 container = document.getElementById('view-container'); container.innerHTML = this.buildHtml(this.summaryData); this.bindEvents(); }); }); // 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; } }); } } };