// 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 */}
{/* 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 ? ( {member.name} ) : (
{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 && ( )} )} {/* 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;