import React, { useMemo } from 'react'; import { Flag, Calendar, Clock, Briefcase, CheckCircle2, AlertCircle, TrendingUp, Plus, Coins, } from 'lucide-react'; import { Project, Milestone, Activity, ApprovalStatus, Priority } from '../../types'; import { formatDate, formatZAR, getApprovalStatusBadge, getPriorityBadge } from '../../lib/formatters'; interface ProjectTimelineGanttProps { project: Project; milestones: Milestone[]; activities: Activity[]; approvedBudgetTotal?: number; approvedBudgetsCount?: number; onManageMilestones: () => void; onManageActivities: () => void; } export const ProjectTimelineGantt: React.FC = ({ project, milestones, activities, approvedBudgetTotal = 0, approvedBudgetsCount = 0, onManageMilestones, onManageActivities, }) => { // Compute timeline boundaries and items const timelineData = useMemo(() => { const dates: number[] = []; if (project.startedAt) dates.push(new Date(project.startedAt).getTime()); if (project.endedAt) dates.push(new Date(project.endedAt).getTime()); milestones.forEach((m) => { if (m.dueAt) dates.push(new Date(m.dueAt).getTime()); }); activities.forEach((a) => { if (a.performedAt) dates.push(new Date(a.performedAt).getTime()); }); const now = Date.now(); dates.push(now); let startMs: number; let endMs: number; if (dates.length > 0 && Math.min(...dates) !== Math.max(...dates)) { startMs = Math.min(...dates); endMs = Math.max(...dates); // Add a slight buffer (5% on each side) for aesthetic padding const buffer = Math.max((endMs - startMs) * 0.05, 7 * 24 * 60 * 60 * 1000); startMs -= buffer; endMs += buffer; } else { // Default: current quarter +/- 3 months startMs = now - 90 * 24 * 60 * 60 * 1000; endMs = now + 180 * 24 * 60 * 60 * 1000; } const totalDurationMs = endMs - startMs; // Helper to calculate percentage offset along timeline const getPercent = (timeMs: number): number => { const clamped = Math.max(startMs, Math.min(endMs, timeMs)); return ((clamped - startMs) / totalDurationMs) * 100; }; // Generate monthly ticks for ruler const ticks: { label: string; percent: number; isStartOfYear: boolean }[] = []; const currDate = new Date(startMs); currDate.setDate(1); currDate.setHours(0, 0, 0, 0); while (currDate.getTime() <= endMs) { if (currDate.getTime() >= startMs) { ticks.push({ label: currDate.toLocaleDateString('en-GB', { month: 'short', year: '2-digit' }), percent: getPercent(currDate.getTime()), isStartOfYear: currDate.getMonth() === 0, }); } currDate.setMonth(currDate.getMonth() + 1); } // Sort milestones & activities by date const sortedMilestones = [...milestones].sort( (a, b) => new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime() ); const sortedActivities = [...activities].sort( (a, b) => new Date(a.performedAt || 0).getTime() - new Date(b.performedAt || 0).getTime() ); const todayPercent = getPercent(now); const isTodayInRange = todayPercent >= 0 && todayPercent <= 100; return { startMs, endMs, totalDurationDays: Math.round(totalDurationMs / (1000 * 60 * 60 * 24)), getPercent, ticks, sortedMilestones, sortedActivities, todayPercent, isTodayInRange, }; }, [project, milestones, activities]); const verifiedMilestonesCount = milestones.filter((m) => m.status === ApprovalStatus.Approved).length; const milestoneProgress = milestones.length > 0 ? Math.round((verifiedMilestonesCount / milestones.length) * 100) : 0; const totalActivityBudget = activities.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); return (
{/* Timeline Header & Key Stats */}

Implementation Schedule & Timeline ({timelineData.totalDurationDays} Days Span)

Chronological alignment of contractual deliverables and operational activities.

{/* Quick Legend & Actions */}
Verified Deliverable Pending / Review Work Activity
{/* Main Gantt Timeline Track Container */}
{/* Month Scale / Ruler */}
{timelineData.ticks.map((tick, idx) => (
{tick.label}
))}
{/* Timeline Visualization Surface */}
{/* Background vertical grid lines */}
{timelineData.ticks.map((tick, idx) => (
))} {/* Today marker */} {timelineData.isTodayInRange && (
Today
)}
{/* Layer 1: Milestones Track */}
Contractual Deliverables & Milestones ({milestones.length})
{timelineData.sortedMilestones.length === 0 ? (
No contractual milestones defined yet. Click "Add / Manage Milestones" to establish deliverables.
) : (
{/* Milestone connecting base line */}
{/* Milestone Pins */} {timelineData.sortedMilestones.map((m, idx) => { const mTime = new Date(m.dueAt || 0).getTime(); const pos = timelineData.getPercent(mTime); const isApproved = m.status === ApprovalStatus.Approved; const isPending = m.status === ApprovalStatus.Pending; const priBadge = getPriorityBadge(m.priority); return (
{/* Flag Marker */}
{/* Brief Pin Label */}
{m.name} {formatDate(m.dueAt)}
{/* Tooltip on Hover */}
Milestone #{m.id} {priBadge.label}

{m.name}

{formatDate(m.dueAt)} {isApproved ? 'Verified' : 'Pending'}
); })}
)}
{/* Layer 2: Activities Track */}
Work Breakdown Activities ({activities.length})
{timelineData.sortedActivities.length === 0 ? (
No activities recorded in work breakdown structure.
) : (
{timelineData.sortedActivities.slice(0, 4).map((act, idx) => { const aTime = new Date(act.performedAt || project.startedAt || Date.now()).getTime(); const pos = timelineData.getPercent(aTime); return (
{act.title} {act.category || 'Operations'}
{formatZAR(act.amount)}
{formatDate(act.performedAt)}
); })} {activities.length > 4 && (
)}
)}
{/* Bottom Summary KPI Bar */}
Project Duration {timelineData.totalDurationDays} Days {formatDate(project.startedAt)} → {formatDate(project.endedAt)}
Milestone Progress
{milestoneProgress}% ({verifiedMilestonesCount}/{milestones.length})
Work Breakdown Cost {formatZAR(totalActivityBudget)} {activities.length} total tasks
Governance State
{(() => { const b = getApprovalStatusBadge(project.status); return ( {b.label} ); })()}
Statutory Clearance
{/* Total Value of Approved Project Budgets Widget */}
Approved Budget Value {formatZAR(approvedBudgetTotal)} {approvedBudgetsCount > 0 ? `${approvedBudgetsCount} approved budget line${approvedBudgetsCount === 1 ? '' : 's'}` : 'Allocated Project Budget'}
); };