// ProjectGanttChart - Gantt chart visualization for active projects function ProjectGanttChart({ projects, onProjectClick }) { const [viewMode, setViewMode] = React.useState('month'); // 'week', 'month', 'quarter' const [collapsed, setCollapsed] = React.useState(false); const containerRef = React.useRef(null); const today = new Date(); today.setHours(0, 0, 0, 0); // Filter projects that have at least a start_date const projectsWithDates = projects.filter(p => p.start_date); if (projectsWithDates.length === 0) { return null; } // Calculate date range based on all projects const calculateDateRange = () => { let minDate = new Date(); let maxDate = new Date(); projectsWithDates.forEach(project => { const startDate = new Date(project.start_date); const endDate = project.due_date ? new Date(project.due_date) : new Date(startDate.getTime() + 30 * 24 * 60 * 60 * 1000); if (startDate < minDate) minDate = startDate; if (endDate > maxDate) maxDate = endDate; }); // Add padding minDate.setDate(minDate.getDate() - 7); maxDate.setDate(maxDate.getDate() + 14); return { minDate, maxDate }; }; const { minDate, maxDate } = calculateDateRange(); // Generate time periods based on view mode const generatePeriods = () => { const periods = []; let current = new Date(minDate); if (viewMode === 'week') { current.setDate(current.getDate() - current.getDay() + 1); while (current <= maxDate) { const weekStart = new Date(current); const weekEnd = new Date(current); weekEnd.setDate(weekEnd.getDate() + 6); periods.push({ start: weekStart, end: weekEnd, label: `${weekStart.getDate()} ${weekStart.toLocaleDateString('it-IT', { month: 'short' })}` }); current.setDate(current.getDate() + 7); } } else if (viewMode === 'month') { current.setDate(1); while (current <= maxDate) { const monthStart = new Date(current); const monthEnd = new Date(current.getFullYear(), current.getMonth() + 1, 0); periods.push({ start: monthStart, end: monthEnd, label: current.toLocaleDateString('it-IT', { month: 'short', year: '2-digit' }) }); current.setMonth(current.getMonth() + 1); } } else { current.setDate(1); current.setMonth(Math.floor(current.getMonth() / 3) * 3); while (current <= maxDate) { const quarterStart = new Date(current); const quarterEnd = new Date(current.getFullYear(), current.getMonth() + 3, 0); const quarterNum = Math.floor(current.getMonth() / 3) + 1; periods.push({ start: quarterStart, end: quarterEnd, label: `Q${quarterNum} ${current.getFullYear()}` }); current.setMonth(current.getMonth() + 3); } } return periods; }; const periods = generatePeriods(); const totalDays = Math.ceil((maxDate - minDate) / (1000 * 60 * 60 * 24)); const getProjectBar = (project) => { const startDate = new Date(project.start_date); const endDate = project.due_date ? new Date(project.due_date) : new Date(startDate.getTime() + 14 * 24 * 60 * 60 * 1000); const startOffset = Math.max(0, Math.ceil((startDate - minDate) / (1000 * 60 * 60 * 24))); const duration = Math.max(1, Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24))); const leftPercent = (startOffset / totalDays) * 100; const widthPercent = (duration / totalDays) * 100; const isOverdue = project.due_date && new Date(project.due_date) < today && project.status !== 'completed'; const daysUntilDue = project.due_date ? Math.ceil((new Date(project.due_date) - today) / (1000 * 60 * 60 * 24)) : null; const isNearingDeadline = daysUntilDue !== null && daysUntilDue > 0 && daysUntilDue <= 7; return { leftPercent, widthPercent, isOverdue, isNearingDeadline, daysUntilDue }; }; const getBarColor = (project, isOverdue, isNearingDeadline) => { if (project.status === 'completed') return 'bg-green-500'; if (isOverdue) return 'bg-red-500'; if (isNearingDeadline) return 'bg-amber-500'; // Normal projects use priority-based colors (distinct from warning colors) switch (project.priority) { case 'high': return 'bg-purple-500'; case 'medium': return 'bg-blue-500'; default: return 'bg-slate-400'; } }; const todayOffset = Math.ceil((today - minDate) / (1000 * 60 * 60 * 24)); const todayPercent = (todayOffset / totalDays) * 100; const sortedProjects = [...projectsWithDates].sort((a, b) => new Date(a.start_date) - new Date(b.start_date) ); return (
setCollapsed(!collapsed)} >

Timeline Pratiche

{projectsWithDates.length} progetti
{!collapsed && (
e.stopPropagation()}>
Alta Media Bassa
{['week', 'month', 'quarter'].map(mode => ( ))}
)}
{!collapsed && (
{/* Sticky header row */}
Pratica
Assegnati
{periods.map((period, idx) => (
{period.label}
))}
{/* Scrollable project rows container - max height of ~10 rows (400px) */}
{/* Today marker - spans full height of content */} {todayPercent >= 0 && todayPercent <= 100 && (
Oggi
)} {sortedProjects.map((project, idx) => { const { leftPercent, widthPercent, isOverdue, isNearingDeadline, daysUntilDue } = getProjectBar(project); const barColor = getBarColor(project, isOverdue, isNearingDeadline); const assignees = project.assignments || []; return (
onProjectClick && onProjectClick(project.id)} >
{project.name}
{/* Assignees Column */}
{assignees.length > 0 ? (
{assignees.slice(0, 3).map((assignment, i) => { const name = assignment.user_name || assignment.user_email || '?'; const initials = name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); return (
{initials}
); })} {assignees.length > 3 && (
a.user_name || a.user_email).join(', ')} > +{assignees.length - 3}
)}
) : ( - )}
{periods.map((_, idx) => (
))}
onProjectClick && onProjectClick(project.id)} title={`${project.name}\nInizio: ${new Date(project.start_date).toLocaleDateString('it-IT')}\n${project.due_date ? 'Scadenza: ' + new Date(project.due_date).toLocaleDateString('it-IT') : 'Scadenza non impostata'}`} > {widthPercent > 8 && ( {project.name.length > 15 ? project.name.substring(0, 15) + '...' : project.name} )} {(isOverdue || isNearingDeadline) && ( {isOverdue ? ( ! ) : ( {daysUntilDue} )} )}
); })}
)}
); } window.ProjectGanttChart = ProjectGanttChart;