# Project Gantt / Timeline View

**Date:** 2026-06-01
**Status:** Draft
**Spec:** 173
**Tier:** Business+
**Depends on:** `projects-module`, `tasks-detail-communication`, `project-milestones`, `foundation-auth-rbac`
**Referenced by:** `projects-module`

---

## Overview

An interactive Gantt chart view for project tasks and milestones. Provides a timeline-based visual of task dependencies, duration, and progress, complementing the existing Kanban board view in the tasks module.

---

## Route

`/projects/:id/gantt` — Gantt view toggle alongside board/list views. Persists last-used view to `user_preferences.dashboard_widgets` (`project_view_mode: 'board' | 'list' | 'gantt'`).

---

## UI Layout

```
┌──────────────────────────────────────────────────────────────────────────────┐
│  Acme Redesign Project     [Board] [List] [Gantt ●]    [Zoom: Month ▾]      │
├──────────────────────────────────────────────────────────────────────────────┤
│  Task / Milestone        │  Jun 2026                │  Jul 2026              │
│                          │  W1  W2  W3  W4          │  W1  W2  W3  W4       │
│  ────────────────────────┼───────────────────────────┼──────────────────────  │
│  📍 Kickoff              │  ◇                        │                       │
│  ▼ Phase 1: Discovery    │  [■■■■■■■]                │                       │
│    Research              │    [■■■]                  │                       │
│    Interviews            │        [■■■]              │                       │
│    ──────────────────────│──────────────────────     │                       │
│    📍 Phase 1 Complete   │               ◇           │                       │
│  ▼ Phase 2: Design       │                           │  [■■■■■■■■■■]        │
│    Wireframes            │                           │    [■■■■]             │
│    UI Design             │                           │         [■■■■■]      │
│  ▼ Phase 3: Development  │                           │                       │
│    Frontend              │                           │            (unscheduled│
│    Backend               │                           │            tasks)     │
└──────────────────────────────────────────────────────────────────────────────┘
```

**Timeline elements:**
- Task bars: colored by status (`oklch(0.75 0.15 250)` for active, `oklch(0.6 0.05 0)` for blocked)
- Milestone markers: `◇` diamond on the date line
- Today indicator: vertical line in `oklch(0.6 0.18 30)` (accent)
- Dependency arrows: thin line from predecessor end to successor start
- Progress overlay: darker fill proportional to task `completion_pct`

**Zoom levels:** Day (6 weeks), Week (3 months), Month (12 months), Quarter (2 years)

---

## Task Data Requirements

Gantt requires tasks to have start and end dates. Tasks without dates appear in an "Unscheduled" bucket at the bottom.

### Schema delta

```sql
-- tasks.start_date is owned by project-templates (wave 6) and already exists — NOT re-added here.
ALTER TABLE tasks ADD COLUMN end_date   DATE;    -- Gantt bar end
-- Both nullable; if only due_date set, Gantt uses due_date as single-day marker
ALTER TABLE tasks ADD COLUMN parent_task_id UUID REFERENCES tasks(id) ON DELETE SET NULL;
-- For task hierarchy (sub-tasks); parent bars span min(children.start) to max(children.end)

ALTER TABLE tasks ADD COLUMN depends_on_task_ids UUID[] DEFAULT '{}';
-- Array of task IDs this task depends on; used for dependency arrows + critical path
-- Stored as array for simplicity; validated to prevent cycles at API level

-- Reverse lookup ("which tasks depend on :id?") is needed on task delete and to render
-- dependency arrows. Without a GIN index, `:id = ANY(depends_on_task_ids)` is a seq scan.
CREATE INDEX idx_tasks_depends_on ON tasks USING GIN (depends_on_task_ids);
```

### Milestones

A task with `is_milestone = true` renders as a diamond marker (◇) rather than a bar. Milestones have a single `due_date` (start_date = end_date).

```sql
-- tasks.is_milestone is owned by project-templates (wave 6) and already exists — NOT re-added here.
```

### Payment / deliverable milestones overlay (`project_milestones`)

`tasks.is_milestone` covers *schedule* milestones only. Fixed-fee projects also have **payment/deliverable milestones** in the separate `project_milestones` table (spec 132 `project-milestones`) with their own `due_date`, `amount`, and `completed_at`. These were previously invisible on the Gantt. The Gantt overlays them as a distinct marker so the financial timeline is visible alongside task scheduling:

- For `billing_type = 'fixed'` projects, `project_milestones` rows render as **filled diamonds (◆)** on their `due_date` line, visually distinct from task milestone markers (`◇` hollow). They occupy a dedicated **"Payments"** lane pinned at the top of the chart (not interleaved with task rows).
- Marker label shows milestone name + amount (e.g. "◆ Design approval · ₪2,000"); `aria-label="{name}, payment milestone, due {date}, {amount}, {completed|pending}"`.
- Completed milestones (`completed_at IS NOT NULL`) render filled/green; pending render outlined. Invoiced milestones (`invoice_id` set) carry an invoice-link affordance in the tooltip.
- Payment milestones are **read-only on the Gantt** — drag/resize/dependency interactions do not apply; editing happens on the project Milestones tab (spec 132). Clicking a marker opens that milestone in the Milestones tab.
- Non-fixed projects (hourly/retainer) have no `project_milestones`; the Payments lane is hidden.

---

## Interactions

### Drag to reschedule

Drag a task bar horizontally to change `start_date` + `end_date` (preserving duration). Fires `PATCH /api/tasks/:id` with new dates.

### Resize to extend/shorten

Drag right edge of bar to change `end_date` only.

### Click to open task

Click anywhere on the bar (except drag handles) → opens task detail side panel (same as in board view).

### Add dependency

Hover over task bar → dependency connector appears on right edge → drag to target task bar → creates dependency link.

`POST /api/tasks/:id/dependencies` with `{ depends_on_task_id }`.

### Create task from Gantt

Click on empty timeline area → inline task creation with pre-filled `start_date` from click position.

### Keyboard accessibility (WCAG 2.1.1 — drag is not the only way)

Every pointer interaction above has a keyboard-operable equivalent; the Gantt is not pointer-only.

- Task bars are focusable (`tabindex`, `role="button"`, `aria-label="{title}, {start}–{end}, {status}"`). Tab/Shift+Tab move between bars in row order.
- On a focused bar: **←/→** move the bar by one grid step (preserving duration); **Shift+←/→** resize `end_date`; **Enter/Space** open the task panel. Each commits the same `PATCH /api/tasks/:id` as drag.
- The task detail panel always exposes plain `start_date`/`end_date`/dependency inputs as the documented non-pointer fallback for rescheduling and dependency editing.
- After a keyboard move, an `aria-live="polite"` status region announces the new range ("Research moved to Jun 3 – Jun 9").

---

## API Extensions

```
GET  /api/projects/:id/gantt
     → tasks formatted for Gantt rendering
       Response: {
         tasks: [{
           id, title, status, start_date, end_date, due_date,
           is_milestone, completion_pct, parent_task_id,
           depends_on_task_ids, assignees, is_blocked
         }],
         milestones: [...],  -- schedule milestones: tasks where is_milestone = true
         payment_milestones: [{   -- spec 132 project_milestones (fixed-fee only)
           id, name, amount, due_date, completed_at, invoice_id
         }],
         project: { start_date, end_date, billing_type }
       }
       Requires: projects:read

PATCH /api/tasks/:id
      (existing endpoint — extended to accept start_date, end_date)

POST  /api/tasks/:id/dependencies
      body: { depends_on_task_id: string }
      Action: adds to depends_on_task_ids array; validates no cycle (DFS)
      Returns 409 if cycle detected
      Requires: tasks:write

DELETE /api/tasks/:id/dependencies/:dependencyTaskId
       Requires: tasks:write
```

---

## Critical Path Highlighting (optional display)

When enabled (toggle in Gantt toolbar), the critical path is highlighted in amber (`oklch(0.75 0.2 80)`). The critical path = the longest chain of dependent tasks from project start to end. Computed client-side using the task dependency graph; no server-side computation needed at v1 data scales.

**Accessibility (do not encode meaning by color/SVG alone — WCAG 1.4.1 / 1.3.1):**
- Dependency arrows are decorative SVG; the relationship is also exposed programmatically — each task bar carries `aria-describedby` pointing to a visually-hidden list of predecessor task names ("Depends on: Research, Interviews").
- Critical-path membership is not amber-only: critical bars also get `aria-label` suffix "— on critical path" and a non-color affordance (e.g. a bold left border), so the information survives for color-blind and screen-reader users.

---

## Architecture Decisions

| Decision | Choice | Reason |
|----------|--------|--------|
| `depends_on_task_ids UUID[]` | Not dependency join table | Array is simpler for most queries (render arrows, validate cycles); a join table would be needed for indexing at scale but is YAGNI for v1 |
| Client-side Gantt rendering | Not server-side chart | Interactive drag/drop requires DOM access; react-gantt or custom SVG/Canvas rendering; server provides data only |
| start_date / end_date nullable | Not required | Tasks can exist without Gantt scheduling; board/list view is the default; Gantt is opt-in per project; forcing dates on all tasks would break the existing quick-add flow |
| Business+ tier only | Not Freelancer | Gantt is a project management feature for multi-person teams; single freelancer use case is adequately served by the task list; tier gate prevents feature bloat at the free tier |
| Cycle detection at API level | Not DB constraint | ARRAY FK constraints don't support cycle validation; API-level DFS is sufficient at v1 scale; denormalized array read is fast for cycle detection |
