// 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 (
Analisi dettagliata delle prestazioni del team
Nessun dato disponibile
) : (| Pos. | Utente | Completate | Annullate | In Tempo | In Ritardo | Puntualita | Tempo Medio |
|---|---|---|---|---|---|---|---|
| #{index + 1} |
{member.avatar ? (
{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` : '-'} |
Nessuna pratica annullata con motivo specificato
) : ({item.reason.length > 30 ? item.reason.substring(0, 30) + '...' : item.reason}
{item.count}+{analytics.reasons.length - 6} altri motivi
)}{selectedUser.email}