gantt

Gantt chart with quarter roadmap and rollups

Loading preview…

src/gantt/c-gantt-3.vue

<!--
  ПОРТ c-gantt-3 ("Gantt chart with quarter roadmap and rollups").

  Оригинал берёт императивный доступ через `apiRef`; наш `Gantt.vue` его не
  реализует (`defineExpose` не добавлен, см. заголовок Gantt.vue). Тот же
  доступ получается напрямую: `AddInitiativeButton` — крошечный локальный
  компонент, смонтированный ВНУТРИ `<Gantt>` (в слоте `GanttToolbar`), поэтому
  его `useGantt()` находит provide/inject-контекст так же, как `GanttNav`/
  `GanttToolbar` уже делают. Родительские workstream-строки не получают
  своих баров — `summaryBars` (по умолчанию `true`) сворачивает дочерние
  initiative-бары в сводную полосу на родительской строке, как в оригинале.
  Дата фиксирована явно в UTC (никаких `new Date()`), тот же якорь, что и в
  `ui-gantt`.
-->
<script setup lang="ts">
import { defineComponent, h, ref } from "vue"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Gantt, GanttNav, GanttToolbar, GanttView, useGantt } from "@/components/reui/gantt"
import type { GanttEvent, GanttResource } from "@/components/reui/gantt"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

const ANCHOR = new Date(Date.UTC(2024, 0, 8, 0, 0, 0))

const RESOURCES: GanttResource[] = [
  {
    id: "platform",
    title: "Platform",
    children: [
      { id: "auth-revamp", title: "Auth Revamp" },
      { id: "api-v2", title: "API v2" },
    ],
  },
  {
    id: "growth",
    title: "Growth",
    children: [
      { id: "onboarding", title: "Onboarding Flow" },
      { id: "referrals", title: "Referral Program" },
    ],
  },
  {
    id: "design-system",
    title: "Design System",
    children: [
      { id: "tokens", title: "Design Tokens" },
      { id: "components", title: "Component Library" },
    ],
  },
  {
    id: "backlog",
    title: "Backlog",
    children: [
      { id: "search", title: "Search Revamp" },
      { id: "billing", title: "Billing v2" },
      { id: "mobile", title: "Mobile App" },
    ],
  },
]

const BACKLOG: Array<{ id: string; title: string; color: string }> = [
  { id: "search", title: "Search Revamp", color: "var(--color-rose-500)" },
  { id: "billing", title: "Billing v2", color: "var(--color-amber-500)" },
  { id: "mobile", title: "Mobile App", color: "var(--color-cyan-500)" },
]

function buildBars(anchor: Date): GanttEvent[] {
  const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
  const day = (dayOffset: number) => addDays(week, dayOffset)
  const bar = (resourceId: string, title: string, startOffset: number, days: number, color: string, progress?: number): GanttEvent => ({
    id: `bar-${resourceId}`,
    title,
    start: day(startOffset),
    end: day(startOffset + days),
    allDay: true,
    color,
    resourceId,
    progress,
  })

  return [
    bar("auth-revamp", "Auth Revamp", -30, 60, "var(--color-blue-500)", 100),
    bar("api-v2", "API v2", 20, 90, "var(--color-sky-500)", 30),
    bar("onboarding", "Onboarding Flow", -20, 60, "var(--color-emerald-500)", 80),
    bar("referrals", "Referral Program", 50, 90, "var(--color-teal-500)", 0),
    bar("tokens", "Design Tokens", -60, 70, "var(--color-violet-500)", 100),
    bar("components", "Component Library", 0, 120, "var(--color-purple-500)", 45),
  ]
}

const bars = buildBars(ANCHOR)
const scheduled = ref(0)

const AddInitiativeButton = defineComponent({
  setup() {
    const instance = useGantt<unknown>()
    const addInitiative = () => {
      const scheduledIds = new Set(instance.api.getEvents().map((event) => event.id))
      const index = BACKLOG.findIndex((item) => !scheduledIds.has(`bar-${item.id}`))
      if (index === -1) return
      const item = BACKLOG[index]!
      const week = startOfWeek(startOfDay(ANCHOR), { weekStartsOn: 0 })
      const start = addDays(week, 12 + index * 20)
      instance.api.addEvent({
        id: `bar-${item.id}`,
        title: item.title,
        start,
        end: addDays(start, 24),
        allDay: true,
        color: item.color,
        resourceId: item.id,
      })
      scheduled.value++
    }
    return () =>
      h(
        Button,
        { variant: "outline", size: "sm", disabled: scheduled.value >= BACKLOG.length, onClick: addInitiative },
        () => [
          h(
            "svg",
            {
              xmlns: "http://www.w3.org/2000/svg",
              width: "24",
              height: "24",
              viewBox: "0 0 24 24",
              fill: "none",
              stroke: "currentColor",
              "stroke-width": "2",
              "stroke-linecap": "round",
              "stroke-linejoin": "round",
              class: "size-4",
              "aria-hidden": "true",
            },
            [h("path", { d: "M5 12h14" }), h("path", { d: "M12 5v14" })]
          ),
          "Add to roadmap",
        ]
      )
  },
})
</script>

<template>
  <div class="w-full p-4">
    <Card class="w-full py-0">
      <CardContent class="p-0">
        <Gantt :date="ANCHOR"
          initial-center="anchor" :default-events="bars" :resources="RESOURCES" default-scale="quarter" :infinite-scroll="false" :tree-panel="{ width: 200 }" class="h-[480px] w-full">
          <div class="flex flex-wrap items-center gap-2 border-b pe-3">
            <GanttNav class="min-w-0 flex-1 border-b-0" />
            <GanttToolbar>
              <AddInitiativeButton />
            </GanttToolbar>
          </div>
          <GanttView />
        </Gantt>
      </CardContent>
    </Card>
  </div>
</template>

Install

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-gantt-3.json

Registry dependencies

npm dependencies

  • date-fns

Source: ported from ReUI (Keenthemes, MIT)