/**
* 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',
groupByTicket: localStorage.getItem('otrs_ta_group_by_ticket') !== 'false',
groupArticles(articles) {
if (!articles || articles.length === 0) return [];
if (!this.groupByTicket) {
return articles.map(a => ({ type: 'single', article: a }));
}
const groups = [];
let currentGroup = null;
for (const a of articles) {
const ticketId = a.ticket_id;
if (currentGroup && currentGroup.ticket_id === ticketId) {
currentGroup.articles.push(a);
} else {
if (currentGroup) {
groups.push(currentGroup);
}
currentGroup = {
type: 'group',
ticket_id: ticketId,
ticket_tn: a.ticket_tn,
ticket_title: a.ticket_title,
articles: [a]
};
}
}
if (currentGroup) {
groups.push(currentGroup);
}
return groups;
},
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 `
${this.viewMode === 'calendar' ? this.buildCalendarGrid(data) : `
${this.buildDayStrip(data)}
${articles.length === 0 ? `
Nessuna nota o email trovata in questa data per l'agente selezionato.
` : this.groupArticles(articles).map(g => g.type === 'single' ? this.buildArticleCard(g.article) : this.buildGroupCard(g)).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 if (dayMins === 0) {
// Giorno lavorativo senza consuntivazioni: neutro / bianco
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 con consuntivazione (> 0 min)
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 `
`;
},
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 (dayMins === 0) {
// Giorno lavorativo senza ore consuntivate: neutro / non evidenziato in rosso
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 `
${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.a_subject || '(senza oggetto)')}
${bodyPreview || 'Nessun testo'}
${App.escapeHtml(a.channel || a.sender_type || 'Nota')}
⏱️ ${timeMins} min
`;
},
buildGroupCard(group) {
const totalGroupMins = Math.round(group.articles.reduce((acc, a) => acc + (parseFloat(a.time_unit) || 0), 0));
const formattedGroupTime = this.formatPillTime(totalGroupMins);
const hasSelectedArticle = group.articles.some(a => String(a.article_id) === String(this.selectedArticleId));
return `
${group.articles.length} ${group.articles.length === 1 ? 'nota' : 'note'}
⏱️ Totale: ${formattedGroupTime} (${totalGroupMins} m)
${group.articles.map(a => this.buildArticleCard(a)).join('')}
`;
},
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 `
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();
});
// Toggle group by ticket button
document.getElementById('ta-btn-toggle-groupby')?.addEventListener('click', () => {
this.groupByTicket = !this.groupByTicket;
localStorage.setItem('otrs_ta_group_by_ticket', this.groupByTicket);
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 = `
📊
Andamento Consuntivazione Tempo
Confronto e analisi temporale delle consuntivazioni agenti
👥 Agenti (${users.length})
${users.map((u, idx) => {
const uId = String(u.id);
const isBanned = bannedAgentIds.includes(uId);
const isChecked = !isBanned && activeAgentIds.includes(uId);
const color = colorPalette[idx % colorPalette.length];
return `
${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)}
0h
`;
}).join('')}
`;
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();
}
};