// TeamOverviewWidget - Shows team performance metrics for admin function TeamOverviewWidget({ projects, isAdmin }) { const { user } = useAuth(); const [activeTab, setActiveTab] = React.useState('ranking'); // 'ranking', 'reasons' const [expanded, setExpanded] = React.useState(false); // Aggregate team performance data from projects const { teamData, cancellationReasons, projectTotals } = React.useMemo(() => { const userMap = new Map(); const reasonsMap = new Map(); // Track unique project counts for summary const totals = { completed: 0, cancelled: 0, onTime: 0, late: 0 }; projects.forEach(project => { // Determine project outcome once per project (for totals) const isCompleted = project.status === 'completed' || (project.is_archived && !project.is_cancelled); if (project.is_cancelled) { totals.cancelled++; } else if (isCompleted) { totals.completed++; // Check if completed on time 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++; } } // 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); } } // Get assignments based on what's available const assignments = project.assignments || []; // For regular users, also add their own assignment if present if (!isAdmin && project.my_assignment) { const hasCurrentUser = assignments.some(a => a.user_id === user?.id); if (!hasCurrentUser && user) { assignments.push({ user_id: user.id, user_name: user.full_name, user_email: user.email }); } } 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, isCurrentUser: userId === user?.id }); } const userData = userMap.get(userId); userData.totalProjects++; if (project.is_cancelled) { userData.cancelledProjects++; } else if (isCompleted) { userData.completedProjects++; // Check if completed on time (before or on due_date) if (project.due_date) { const dueDate = new Date(project.due_date); // Use archived_at for completion time if available, otherwise updated_at const completedDate = project.archived_at ? new Date(project.archived_at) : (project.updated_at ? new Date(project.updated_at) : new Date()); // Compare only dates, not times dueDate.setHours(23, 59, 59, 999); if (completedDate <= dueDate) { userData.completedOnTime++; } else { userData.completedLate++; } } else { // No due date = considered on time userData.completedOnTime++; } } else { userData.activeProjects++; } }); }); // Sort by completed on time (descending), then by total completed const sortedTeam = Array.from(userMap.values()).sort((a, b) => { // Primary: completed on time if (b.completedOnTime !== a.completedOnTime) { return b.completedOnTime - a.completedOnTime; } // Secondary: total completed if (b.completedProjects !== a.completedProjects) { return b.completedProjects - a.completedProjects; } // Tertiary: fewer cancelled is better return a.cancelledProjects - b.cancelledProjects; }); // Sort cancellation reasons by count const sortedReasons = Array.from(reasonsMap.entries()) .map(([reason, count]) => ({ reason, count })) .sort((a, b) => b.count - a.count); return { teamData: sortedTeam, cancellationReasons: sortedReasons, projectTotals: totals }; }, [projects, user, isAdmin]); // 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]; }; const visibleMembers = expanded ? teamData : teamData.slice(0, 5); const hasMore = teamData.length > 5; if (teamData.length === 0) { return ( Statistiche Team Nessun dato disponibile ); } return ( {/* Header */} Statistiche Team {/* Summary Stats */} {projectTotals.completed} Completate {projectTotals.cancelled} Annullate {/* Tabs */} setActiveTab('ranking')} className={`flex-1 py-1.5 px-2 text-xs font-medium rounded-md transition-colors ${activeTab === 'ranking' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'}`} > Classifica setActiveTab('reasons')} className={`flex-1 py-1.5 px-2 text-xs font-medium rounded-md transition-colors ${activeTab === 'reasons' ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-600 hover:text-slate-900'}`} > Motivi Annullamento {/* Ranking Tab */} {activeTab === 'ranking' && ( <> {visibleMembers.map((member, index) => { const successRate = member.completedProjects > 0 ? Math.round((member.completedOnTime / member.completedProjects) * 100) : 0; return ( {/* Rank */} #{index + 1} {/* Avatar */} {member.avatar ? ( ) : ( {getInitials(member.name)} )} {/* Info */} {member.name} {member.isCurrentUser && ( Tu )} {member.completedProjects} ✓ {member.cancelledProjects} ✗ | {member.completedOnTime} puntuali {/* Success rate */} {member.completedProjects > 0 && ( = 80 ? 'text-green-600' : successRate >= 50 ? 'text-amber-600' : 'text-red-500' }`}> {successRate}% puntualità )} ); })} {hasMore && ( setExpanded(!expanded)} className="w-full mt-3 py-2 text-sm text-brand-600 hover:text-brand-700 font-medium rounded-lg hover:bg-brand-50 transition-colors" > {expanded ? 'Mostra meno' : `Mostra tutti (${teamData.length})`} )} > )} {/* Cancellation Reasons Tab */} {activeTab === 'reasons' && ( {cancellationReasons.length === 0 ? ( Nessuna pratica annullata con motivo specificato ) : ( cancellationReasons.slice(0, 5).map((item, index) => ( {item.count} {item.reason} )) )} {cancellationReasons.length > 5 && ( +{cancellationReasons.length - 5} altri motivi )} )} ); } window.TeamOverviewWidget = TeamOverviewWidget;
Nessun dato disponibile
Nessuna pratica annullata con motivo specificato
{item.reason}
+{cancellationReasons.length - 5} altri motivi