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:
parent
ae901edd75
commit
450aea5974
1 changed files with 193 additions and 0 deletions
|
|
@ -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.
|
||||
Loading…
Reference in a new issue