admin / Apex
publicBid Management and Orchestration Tool with AI Capability
Apex / Synapse-Apexv2 / frontend / src / components / Gantt.tsx
3062 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | interface Task { id: number; name: string; kind: string; status: string; start: string | null; due: string | null; } const DAY = 86400000; export default function Gantt({ tasks }: { tasks: Task[] }) { const dated = tasks.filter((t) => t.due); if (dated.length === 0) return null; const times: number[] = []; dated.forEach((t) => { if (t.start) times.push(new Date(t.start).getTime()); if (t.due) times.push(new Date(t.due).getTime()); }); let min = Math.min(...times); let max = Math.max(...times); if (max - min < DAY) { max = min + DAY; } // avoid zero span const span = max - min; const today = Date.now(); const labelW = 190; const rightPad = 20; const chartW = 680; const timelineW = chartW - labelW - rightPad; const rowH = 26; const headerH = 28; const H = headerH + dated.length * rowH + 12; const x = (t: number) => labelW + ((t - min) / span) * timelineW; // month gridlines const ticks: { x: number; label: string }[] = []; const d = new Date(min); d.setDate(1); while (d.getTime() <= max) { const tx = x(Math.max(d.getTime(), min)); ticks.push({ x: tx, label: d.toLocaleDateString(undefined, { month: "short", year: "2-digit" }) }); d.setMonth(d.getMonth() + 1); } function colour(t: Task) { if (t.status === "done") return "#1e8449"; if (t.due && new Date(t.due).getTime() < today && t.status !== "done") return "#c0392b"; return t.kind === "milestone" ? "#f4b942" : "#27357e"; } return ( <div style={{ overflowX: "auto" }}> <svg width="100%" viewBox={`0 0 ${chartW} ${H}`} role="img" style={{ maxWidth: "100%" }}> <title>Bid plan Gantt chart</title> {ticks.map((tk, i) => ( <g key={i}> <line x1={tk.x} y1={headerH - 6} x2={tk.x} y2={H} stroke="#e2e5ee" strokeWidth={0.5} /> <text x={tk.x + 2} y={12} fontSize={11} fill="#6b7280">{tk.label}</text> </g> ))} {/* today marker */} {today >= min && today <= max && ( <line x1={x(today)} y1={headerH - 6} x2={x(today)} y2={H} stroke="#c0392b" strokeWidth={1} strokeDasharray="3 3" /> )} {dated.map((t, i) => { const y = headerH + i * rowH; const s = t.start ? new Date(t.start).getTime() : new Date(t.due!).getTime(); const e = new Date(t.due!).getTime(); const bx = x(s); const bw = Math.max(x(e) - bx, t.kind === "milestone" ? 0 : 6); const c = colour(t); return ( <g key={t.id}> <text x={0} y={y + rowH / 2 + 4} fontSize={12} fill="#1c2333"> {t.name.length > 30 ? t.name.slice(0, 29) + "…" : t.name} </text> {t.kind === "milestone" ? ( <path d={`M ${x(e)} ${y + rowH / 2 - 6} l 6 6 l -6 6 l -6 -6 z`} fill={c} /> ) : ( <rect x={bx} y={y + 6} width={bw} height={rowH - 12} rx={4} fill={c} opacity={0.9} /> )} </g> ); })} </svg> </div> ); } |