// PerformanceView - Detailed team performance analytics (Admin only) function PerformanceView() { const { user } = useAuth(); const [projects, setProjects] = React.useState([]); const [loading, setLoading] = React.useState(true); const [dateRange, setDateRange] = React.useState('all'); // 'all', '30d', '90d', '1y' const [selectedUser, setSelectedUser] = React.useState(null); // For user detail modal // Load all projects including archived React.useEffect(() => { const loadData = async () => { setLoading(true); try { const [activeProjects, archivedProjects] = await Promise.all([ ApiUtils.fetchProjects(), window.ApiProjects.fetchArchivedProjects() ]); setProjects([...activeProjects, ...archivedProjects]); } catch (err) { console.error('Failed to load projects:', err); } finally { setLoading(false); } }; loadData(); }, []); // Filter projects by date range const filteredProjects = React.useMemo(() => { if (dateRange === 'all') return projects; const now = new Date(); let cutoffDate = new Date(); switch (dateRange) { case '30d': cutoffDate.setDate(now.getDate() - 30); break; case '90d': cutoffDate.setDate(now.getDate() - 90); break; case '1y': cutoffDate.setFullYear(now.getFullYear() - 1); break; default: return projects; } return projects.filter(p => { const projectDate = p.archived_at ? new Date(p.archived_at) : new Date(p.created_at); return projectDate >= cutoffDate; }); }, [projects, dateRange]); // Compute all analytics data const analytics = React.useMemo(() => { const userMap = new Map(); const reasonsMap = new Map(); const monthlyData = new Map(); const totals = { completed: 0, cancelled: 0, active: 0, onTime: 0, late: 0 }; filteredProjects.forEach(project => { const isCompleted = project.status === 'completed' || (project.is_archived && !project.is_cancelled); const isActive = !project.is_archived && !project.is_cancelled; // Project totals if (project.is_cancelled) { totals.cancelled++; } else if (isCompleted) { totals.completed++; if (project.due_date) { const dueDate = new Date(project.due_date); const completedDate = project.archived_at ? new Date(project.archived_at) : (project.updated_at ? new Date(project.updated_at) : new Date()); dueDate.setHours(23, 59, 59, 999); if (completedDate <= dueDate) { totals.onTime++; } else { totals.late++; } } else { totals.onTime++; } } else if (isActive) { totals.active++; } // Monthly trends (based on created_at or archived_at) const trendDate = project.is_archived && project.archived_at ? new Date(project.archived_at) : new Date(project.created_at); const monthKey = `${trendDate.getFullYear()}-${String(trendDate.getMonth() + 1).padStart(2, '0')}`; if (!monthlyData.has(monthKey)) { monthlyData.set(monthKey, { completed: 0, cancelled: 0, created: 0 }); } const monthData = monthlyData.get(monthKey); if (project.is_cancelled) { monthData.cancelled++; } else if (isCompleted) { monthData.completed++; } // Count created projects const createdMonth = new Date(project.created_at); const createdKey = `${createdMonth.getFullYear()}-${String(createdMonth.getMonth() + 1).padStart(2, '0')}`; if (!monthlyData.has(createdKey)) { monthlyData.set(createdKey, { completed: 0, cancelled: 0, created: 0 }); } monthlyData.get(createdKey).created++; // Track cancellation reasons if (project.is_cancelled && project.cancellation_reason) { const reason = project.cancellation_reason.trim(); if (reason) { reasonsMap.set(reason, (reasonsMap.get(reason) || 0) + 1); } } // User performance const assignments = project.assignments || []; assignments.forEach(assignment => { const userId = assignment.user_id; if (!userId) return; if (!userMap.has(userId)) { userMap.set(userId, { id: userId, name: assignment.user_name || assignment.user_email || 'Utente', email: assignment.user_email, avatar: assignment.user_avatar, totalProjects: 0, activeProjects: 0, completedProjects: 0, cancelledProjects: 0, completedOnTime: 0, completedLate: 0, avgCompletionDays: [], }); } const userData = userMap.get(userId); userData.totalProjects++; if (project.is_cancelled) { userData.cancelledProjects++; } else if (isCompleted) { userData.completedProjects++; if (project.due_date) { const dueDate = new Date(project.due_date); const completedDate = project.archived_at ? new Date(project.archived_at) : (project.updated_at ? new Date(project.updated_at) : new Date()); dueDate.setHours(23, 59, 59, 999); if (completedDate <= dueDate) { userData.completedOnTime++; } else { userData.completedLate++; } // Track completion time if (project.start_date) { const startDate = new Date(project.start_date); const days = Math.ceil((completedDate - startDate) / (1000 * 60 * 60 * 24)); if (days > 0) userData.avgCompletionDays.push(days); } } else { userData.completedOnTime++; } } else { userData.activeProjects++; } }); }); // Calculate averages and sort users const users = Array.from(userMap.values()).map(u => ({ ...u, avgCompletionDays: u.avgCompletionDays.length > 0 ? Math.round(u.avgCompletionDays.reduce((a, b) => a + b, 0) / u.avgCompletionDays.length) : null, successRate: u.completedProjects > 0 ? Math.round((u.completedOnTime / u.completedProjects) * 100) : 0, })).sort((a, b) => { if (b.completedOnTime !== a.completedOnTime) return b.completedOnTime - a.completedOnTime; if (b.completedProjects !== a.completedProjects) return b.completedProjects - a.completedProjects; return a.cancelledProjects - b.cancelledProjects; }); // Sort monthly data const months = Array.from(monthlyData.entries()) .sort((a, b) => a[0].localeCompare(b[0])) .slice(-12); // Last 12 months // Sort cancellation reasons const reasons = Array.from(reasonsMap.entries()) .map(([reason, count]) => ({ reason, count })) .sort((a, b) => b.count - a.count); return { users, totals, months, reasons }; }, [filteredProjects]); // Get initials from name const getInitials = (name) => { if (!name) return '?'; const parts = name.split(' ').filter(Boolean); if (parts.length >= 2) { return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); } return name.substring(0, 2).toUpperCase(); }; // Get avatar color based on name const getAvatarColor = (name) => { const colors = [ 'bg-blue-500', 'bg-green-500', 'bg-amber-500', 'bg-red-500', 'bg-purple-500', 'bg-pink-500', 'bg-indigo-500', 'bg-teal-500' ]; const hash = name?.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) || 0; return colors[hash % colors.length]; }; // Format month label const formatMonth = (monthKey) => { const [year, month] = monthKey.split('-'); const date = new Date(year, parseInt(month) - 1); return date.toLocaleDateString('it-IT', { month: 'short', year: '2-digit' }); }; if (loading) { return (
); } const totalClosed = analytics.totals.completed + analytics.totals.cancelled; const completionRate = totalClosed > 0 ? Math.round((analytics.totals.completed / totalClosed) * 100) : 0; const punctualityRate = analytics.totals.completed > 0 ? Math.round((analytics.totals.onTime / analytics.totals.completed) * 100) : 0; return (
{/* Header with filters */}

Performance Team

Analisi dettagliata delle prestazioni del team

Periodo:
{/* KPI Cards */}
Totale Pratiche
{filteredProjects.length}
Completate
{analytics.totals.completed}
Annullate
{analytics.totals.cancelled}
In Corso
{analytics.totals.active}
Tasso Completamento
{completionRate}%
Puntualita
{punctualityRate}%
{/* Monthly Trends Chart - Full width under KPIs */} {analytics.months.length > 0 && (

Andamento Mensile

{analytics.months.map(([monthKey, data]) => { const maxVal = Math.max( ...analytics.months.map(([_, d]) => Math.max(d.completed, d.cancelled, d.created)) ); const completedHeight = maxVal > 0 ? (data.completed / maxVal) * 100 : 0; const cancelledHeight = maxVal > 0 ? (data.cancelled / maxVal) * 100 : 0; return (
0 ? '4px' : '0' }} title={`Completate: ${data.completed}`} >
0 ? '4px' : '0' }} title={`Annullate: ${data.cancelled}`} >
{formatMonth(monthKey)}
); })}
Completate
Annullate
)} {/* Team Ranking - Full Width */}

Classifica Team

{analytics.users.length === 0 ? (

Nessun dato disponibile

) : (
{analytics.users.map((member, index) => ( setSelectedUser(member)} > ))}
Pos. Utente Completate Annullate In Tempo In Ritardo Puntualita Tempo Medio
#{index + 1}
{member.avatar ? ( {member.name} ) : (
{getInitials(member.name)}
)}
{member.name}
{member.email}
{member.completedProjects} {member.cancelledProjects} {member.completedOnTime} {member.completedLate} = 80 ? 'bg-green-100 text-green-700' : member.successRate >= 50 ? 'bg-amber-100 text-amber-700' : 'bg-red-100 text-red-700' }`}> {member.successRate}% {member.avgCompletionDays !== null ? `${member.avgCompletionDays}g` : '-'}
)}
{/* Bottom Row - Esito Pratiche and Motivi Annullamento side by side */}
{/* Completion vs Cancellation Donut */}

Esito Pratiche

{/* Background circle */} {/* Completed arc */} {totalClosed > 0 && ( )}
{completionRate}% successo
Completate ({analytics.totals.completed})
Annullate ({analytics.totals.cancelled})
{/* Cancellation Reasons - Horizontal Bar Chart */}

Motivi Annullamento

{analytics.reasons.length === 0 ? (

Nessuna pratica annullata con motivo specificato

) : (
{(() => { const maxCount = Math.max(...analytics.reasons.slice(0, 6).map(r => r.count)); return analytics.reasons.slice(0, 6).map((item, index) => { const barWidth = maxCount > 0 ? (item.count / maxCount) * 100 : 0; return (

{item.reason.length > 30 ? item.reason.substring(0, 30) + '...' : item.reason}

{item.count}
); }); })()} {analytics.reasons.length > 6 && (

+{analytics.reasons.length - 6} altri motivi

)}
)}
{/* User Detail Modal */} {selectedUser && (
{selectedUser.avatar ? ( {selectedUser.name} ) : (
{getInitials(selectedUser.name)}
)}

{selectedUser.name}

{selectedUser.email}

{/* Stats Grid */}
{selectedUser.totalProjects}
Totale Pratiche
{selectedUser.completedProjects}
Completate
{selectedUser.cancelledProjects}
Annullate
{selectedUser.activeProjects}
In Corso
{/* Performance Metrics */}

Metriche di Performance

Puntualita {selectedUser.successRate}%
= 80 ? 'bg-green-500' : selectedUser.successRate >= 50 ? 'bg-amber-500' : 'bg-red-500' }`} style={{ width: `${selectedUser.successRate}%` }} >
In Tempo {selectedUser.completedOnTime}
In Ritardo {selectedUser.completedLate}
{selectedUser.avgCompletionDays !== null && (
Tempo Medio Completamento {selectedUser.avgCompletionDays} giorni
)}
)}
); } window.PerformanceView = PerformanceView;