docs(plan): Phase 10 spec — Gantt redesign for /plan

Redesign the planning page as a project-grouped Gantt:
parent rows are cards (start_date..target_date), indented
children are subtasks (gaining optional start_date and target_date).
No new collections. cards gains an optional assignee text field.
Replaces the Phase 5 Puzzle + Timeline tabs at /plan.

Out of scope: drag-to-reschedule, subtask dates edited on the
card page, connector lines, status/priority filters.
This commit is contained in:
Dave 2026-09-21 15:43:40 +02:00
parent ae901edd75
commit 450aea5974

View file

@ -0,0 +1,193 @@
# Phase 10 — Gantt plan redesign
**Date:** 2026-09-21
**Status:** Draft for review
**Phase:** 10
**Depends on:** Phase 5 (planning and roadmap view), the existing `cards` / `subtasks` schema
**Supersedes:** The Puzzle + Timeline tabs of the Phase 5 `/plan` page. The data they read (`start_date`, `target_date` on cards) is reused; only the view and a small schema add ship here.
---
## 1. Purpose
Replace the current `/plan` screen — its Now/Next/Later grouping and its client-grouped timeline do not work for the developer — with a real Gantt where each row is one of the developer's own work items, indented subtasks sit under it, and every bar represents a duration.
The inspiration is a reference Gantt (dark navy palette, monthly header above weekly ticks, parent bars in a saturated tone with their subtasks in a lighter tint). The reference is treated as **layout, colour direction, and shape** only — none of its domain language ("Planning / Packaging / Marketing") is carried over. What ships is keyed to the existing `projects → cards → subtasks` tree.
The result answers two questions the current page answers badly:
- *"How long will this take, and how does it sit against the rest of the work?"* — every card bar shows duration.
- *"What does this card actually involve?"* — the subtasks under it are visible without leaving the planning page.
## 2. Data — the only schema add
Two new fields, both optional and idempotent through `ensureXxxFields`-style helpers:
**`subtasks.start_date` (date, optional)**
**`subtasks.target_date` (date, optional)**
The existing `subtasks` collection is currently `{ card, title, done, position, created, updated }` and acts as the checklist on the card detail page. The new date fields extend that record so the same row can both be a checklist item **and** appear on the Gantt when it has dates. A subtask with neither date set is invisible on the Gantt, exactly like the Phase 5 timeline's "undated lane" treatment — except here we count them once per project section instead of giving them a row.
**`cards.assignee` (text, optional, max ~120)**
A free-text label rendered above the parent bar. Empty by default; nothing in the rest of the app reads it. The Phase 5 timeline never had this and the reference image's "Bob Johnson / Henry Leblanc" labels read as human assignments, so the field exists to support that look. One developer means it will usually be empty in practice; that is intentional, not underbuilt.
Everything else — `cards.start_date`, `cards.target_date`, `cards.title`, `cards.project`, the existing `subtasks.card` relation, the existing `(card, position)` index — is reused without change.
No new collections. No new rules. The Phase 1 read rules already scope a client manager to their own client's work; that carries over as-is.
## 3. The new `/plan` page
### 3.1 Vertical structure
For every active project, in client-then-project alphabetical order, render one **project section** with this shape:
```
┌──────────────────────────────────────────────────────────────────────┐
│ ● Project Name Client · 12 cards │ ← section header
├──────────────────────────────────────────────────────────────────────┤
│ MONTHS January February │ ← month row
│ WEEKS W1 W2 W3 W4 W1 W2 W3 W4 W1 W2 W3 W4 │ ← week row
├───────────────────────────────────┬──────────────────────────────────┤
│ Card title │ ████████ │ ← card row
│ Subtask one │ ████ │ ← subtask row
│ Subtask two │ ████ │ ← subtask row
│ Card title without dates │ — not scheduled — │ ← undated card row
│ Card title │ ████ │ ← card row
├───────────────────────────────────┴──────────────────────────────────┤
│ 2 cards not scheduled · 3 subtasks not scheduled [Add dates] │ ← footer
└──────────────────────────────────────────────────────────────────────┘
```
Two or more project sections stack vertically. A horizontal divider separates them.
### 3.2 Inside one project section
One flat row array drives both the left **label column** and the right **timeline track**, indexed identically. That is the same invariant the existing `TimelineView` already relies on (one row array, two views) — it guarantees a row's label and its bar cannot drift, and it makes subtask indentation a matter of CSS padding rather than a parallel data structure. The array entries are:
- `{ kind: "card", card, datedSubtasks: Subtask[] }` — renders the parent bar from `card.start_date → card.target_date` and one indented subtask bar per dated subtask.
- `{ kind: "undated", card }` — renders the undated card row. Position below the dated rows within the same project section.
`datedSubtasks` is pre-filtered at loader time to subtasks where `start_date` or `target_date` is set; subtasks without dates are counted into the section footer instead.
### 3.3 The bars
- Parent card bar: `bg-client-brand` with the client's brand colour (already on `clients.brand_color`, today rendered as the row dot). Rounded ends, same height across all cards (12 px). If the card has no assignee, no label is drawn above the bar.
- Subtask bars: same colour, **opacity 0.5** (a single Tailwind opacity modifier against the parent's coloured background; avoids a second CSS variable per project). Inset to the right of the parent bar by `padding-left: 1.5rem`.
- Card bar without dates: not drawn. The label column shows an em-dash and the text "not scheduled".
### 3.4 Header
The header above the timeline reuses `monthTicks` and `weekTicks` from `src/lib/roadmap/timeline.ts`. Month label sits centred above the weeks it spans. Each `W1..W4` segment is one quarter-month. The today marker (vertical line + "Today" pill) is reused unchanged.
### 3.5 Section header
The project section header is a single row above the timeline header inside the same panel:
- A 2 px coloured bar at the top edge in the project client's brand colour (this becomes the section's only visual brand cue and removes the need for a separate icon column).
- The project name in larger type on the left.
- The client name + active card count on the right.
### 3.6 Empty and sparse cases
- **No active projects at all** — the page shows the existing "No active projects" tile. Same wording as before so it doesn't read as a new feature.
- **No dated cards and no dated subtasks** — the page shows a single "Nothing scheduled yet" panel with the same copy as Phase 5 §4.2. We do not invent an undated lane for the Gantt.
- **A section with only undated cards** — section still renders, timeline area collapses to a single row per undated card with "not scheduled" text. The section footer counts them.
### 3.7 Interactions (this phase)
Read-only.
- Clicking a card bar → opens `/p/[client]/[project]/c/[id]` (existing).
- Clicking a subtask bar → opens the parent card; the existing card detail page renders subtasks, and the subtask with `start_date`/`target_date` set gets a small "scheduled" indicator next to its checkbox (text only, no Gantt duplicated on the card page).
- Hovering a bar shows a tooltip with the card title, the client, and the explicit date range (`"5 Mar → 19 Mar"`).
- Keyboard: every bar is a `role="button"` with `tabIndex=0`, focusable. Enter / Space opens the card. The focus ring uses the existing `--brand` token so it reads against the dark background.
Editing dates happens on the card detail page or the subtask detail panel, as today. **Drag-to-reschedule is out of scope** for this phase — see §6.
## 4. Loader
`loadRoadmap` is replaced by a new `loadPlan` returning the structure the view needs in one server-side pass:
```ts
type PlanSection = {
project: { id: string; name: string; slug: string };
client: { id: string; name: string; slug: string; brandColor: string };
cards: PlanCard[];
};
type PlanCard = {
id: string;
title: string;
priority: CardPriority;
startDate: string | null;
targetDate: string | null;
assignee: string;
href: string;
datedSubtasks: PlanSubtask[];
undatedSubtaskCount: number;
};
type PlanSubtask = {
id: string;
title: string;
done: boolean;
startDate: string | null;
targetDate: string | null;
};
```
Implementation notes:
- One PocketBase trip per collection (`projects`, `clients`, `cards`, `subtasks`), in the same shape as `loadRoadmap` today.
- Sort: cards within a project by `start_date` ASC NULLS LAST, then by `target_date` ASC NULLS LAST, then by title. Subtasks within a card by `position` ASC.
- Scope: read with the caller's own PocketBase token. The Phase 1 read rules already constrain a client manager to their own client's projects.
- `datedSubtasks` is filtered to rows where either date is present; the rest are counted.
The existing `RoadmapCard` type and `src/lib/roadmap/band.ts` are dropped from the `/plan` page but kept (used by the dashboard tile if applicable — to confirm before deletion in the plan). The new loader does not need `band`.
## 5. Files touched
- `scripts/setup-remote.ts` — add `ensureSubtaskDateFields` (adds `start_date`, `target_date`) and `ensureCardAssigneeField` (adds `assignee`). Both follow the `ensureXxxFields` idempotent pattern (`Phase 8c` background-md, `Phase 9a` proposal fields).
- `src/lib/roadmap/load.ts` — add `loadPlan()` alongside `loadRoadmap`, or rename `loadRoadmap` → `loadPlan` and update callers if the dashboard tile does not need the legacy band/shape. Choice deferred to the implementation plan.
- `src/app/plan/page.tsx` — replace the `RoadmapTabs` body with `GanttPlanView`. Keep the `TopBar` title "Planning".
- `src/components/roadmap/gantt-plan-view.tsx` — new. Single section component; does the heavy lifting (row array, row alignment, month/week ticks via existing helpers).
- `src/components/roadmap/project-section.tsx` — new. One project's section, including its header.
- `src/components/roadmap/gantt-bar.tsx` — new. One bar (card or subtask variant).
- Tests: `src/lib/roadmap/load.test.ts` for the loader; `tests/plan.test.ts` for the page integration.
## 6. Out of scope
- **Drag-to-reschedule.** The Phase 5 audit-log shape (owner moving a client-owned `target_date`) applies here too; adding drag to a denser widget without the activity log in place would be a regression. A future phase can wire drag with the same audit-log shape as Phase 5 §5.
- **Subtask editing on the Gantt.** Dates are edited on the card page or subtask panel, as today.
- **Filtering by client, priority, or status.** Not in the request.
- **Connector lines** between parent and child bars (the reference Gantt has them; the indented alignment reads cleanly without them, and the dark palette makes 1px rules tricky to get right).
- **Hover-only tooltips that pop out of the bar** (we use a native `title` attribute for now).
- **Removing the legacy `RoadmapTabs` / `TimelineView` / `PuzzleView` / `band-view` files.** If the dashboard still uses them, leave them. If not, the plan may delete them. This keeps the diff small and reversible.
## 7. Migration
`ensureSubtaskDateFields` and `ensureCardAssigneeField` are idempotent. Re-running `npm run setup-remote` over the live instance adds the fields without affecting existing rows. No backfill: existing rows simply show as unscheduled on the new page, exactly the way Phase 5's "undated lane" handled the same case.
## 8. Acceptance criteria
1. The `/plan` page loads for the owner and shows one section per active project, sorted client-then-project alphabetical.
2. A client manager sees only their own client's project sections.
3. Within a section, dated cards render as parent bars coloured with the client's brand colour. Their dated subtasks render as indented lighter-tint bars below them.
4. A card with an `assignee` shows that label above its bar; a card without one shows nothing above the bar.
5. Cards without dates show as "not scheduled" rows in the same section, below the dated rows, and are counted in the section footer.
6. Subtasks without dates are invisible on the Gantt but counted in the section footer.
7. The header shows month labels and `W1..W4` segments using the existing `monthTicks` / `weekTicks` helpers. A vertical "Today" line + pill marks the current date.
8. Clicking a card or subtask bar opens the existing card detail page.
9. Every bar is keyboard-focusable and activates on Enter / Space.
10. With no dated cards or subtasks across any project, the page shows a single "Nothing scheduled yet" panel.
11. With no active projects at all, the page shows the existing "No active projects" tile.
12. The schema additions survive a re-run of `scripts/setup-remote.ts` without duplicating fields.
13. The existing `/p/[client]/[project]/c/[id]` page still renders subtasks correctly when a subtask has dates set.
14. Unit tests cover the new loader shape. Integration tests cover the page rendering against the demo data set.
## 9. Risks
- **Background colour contrast.** Subtasks use `opacity-50` against the project's brand colour, which is already opaque. If the brand colour is light (e.g. `#ffffff` or close to it), the subtask bar may be hard to read. Mitigation: a follow-up phase can clamp luminance or use a derived darker tint per project; not worth blocking Phase 10 on.
- **Wide projects.** A project with many dated cards and subtasks becomes a long vertical list. Mitigated by the existing `overflow-x-auto` on the timeline track; the label column stays the same width. No change in this phase.
- **Subtasks as both checklist and planning.** A subtask with `done = true` but a `target_date` in the future shows as a completed-but-scheduled row. We draw it with a strikethrough on its title. No rule changes; the user can choose to ignore either signal.