Catalyst / admin/Apex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Apex

public

Bid Management and Orchestration Tool with AI Capability

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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>
  );
}