Scrum4Me/docs/plans/PBI-91-pb-screen-state.md
Janpeter Visser 3d5c22382c
feat(PBI-91): expliciete schermstaat + draft-zichtbaarheid PB-page (#210)
* docs(ST-1369): plan PBI-91 — expliciete schermstaat + draft-zichtbaarheid

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): screen-state module — ScreenState + deriveScreenState()

Pure afleidingslaag die de verspreide schermstaat-derivatie van de Product
Backlog page consolideert tot één testbaar ScreenState-model. Nog geen
consumers — die volgen in T-1035/T-1036.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ST-1369): unit-tests voor deriveScreenState()

Dekt alle vier de kinds (NO_SPRINT, DRAFT, ACTIVE, EDITING), de building-flag
en de draft-voorrang boven een actieve sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): SprintSwitcher op deriveScreenState + draft op trigger (G5)

De trigger-knop toont nu de concept-sprint zodra er een sprint-draft loopt,
niet langer alleen de (disabled) dropdown-regel. Schermstaat-afleiding loopt
via de pure deriveScreenState() i.p.v. losse flags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(ST-1369): NewSprintTrigger achter isActiveProduct-gate (G6)

De "Nieuwe sprint"-knop rendert niet langer op een niet-actief product —
een sprint-draft starten daar was verwarrend. page.tsx geeft de bestaande
isActiveProduct-flag door.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(ST-1369): component-tests voor draft-op-trigger (G5) en isActiveProduct-gate (G6)

sprint-switcher: trigger toont concept-sprint bij een pending draft, en geen
concept-label zonder draft. new-sprint-trigger: nieuw testbestand — rendert
niet op een niet-actief product, wel op een actief product zonder draft.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:45:35 +02:00

98 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan — Expliciete schermstaat + draft-zichtbaarheid op de Product Backlog page
> **Status:** goedgekeurd 2026-05-15 · gematerialiseerd via Scrum4Me-MCP.
> **PBI-91** · Story **ST-1369** (OPEN — in de productbacklog, geen sprint) · Taken **T-1033 t/m T-1037** (uitvoervolgorde = sort_order 15).
>
> Vervolg op **PBI-88** ("Product Backlog page workflow & states", PR #208) — implementeert 3 van de 4 niet-bindende aanbevelingen uit `docs/architecture/product-backlog-workflow.md`. G3 uitgesteld.
## Context
PBI-88 leverde een as-is/to-be analyse op: [docs/architecture/product-backlog-workflow.md](../architecture/product-backlog-workflow.md). Dat doc sluit af met vier **niet-bindende** aanbevelingen (G1, G3, G5, G6) als "input voor latere PBI's".
Deze PBI implementeert er **drie** van. **G3** (expliciete ERROR-schermstaat) wordt **uitgesteld** — het doc zegt zelf "alleen oppakken als falende commits of SSE-verlies een echt UX-probleem blijken", en het is meer een ontwerpkeuze dan een heldere implementatie.
De directe aanleiding voor PBI-88 was een **bug**: de concept-sprint (`pendingSprintDraft`) is niet zichtbaar op de SprintSwitcher-trigger-knop — alleen in de (disabled) dropdown. Dat is **G5** en wordt hier opgelost.
**Scope:**
- **G1** — `deriveScreenState()`: één pure functie die de vandaag verspreide schermstaat-afleiding consolideert.
- **G5** — draft-status zichtbaar op de SprintSwitcher-trigger (de oorspronkelijke bug).
- **G6** — `NewSprintTrigger` achter een `isActiveProduct`-gate.
- **G3** — *uitgesteld*, vastgelegd als follow-up.
## Aanpak
### Nieuwe bestanden
**`stores/product-workspace/screen-state.ts`** — pure module, géén React, spiegelt `selectors.ts`:
```ts
export type ScreenState =
| { kind: 'NO_SPRINT' }
| { kind: 'DRAFT' }
| { kind: 'ACTIVE'; building: boolean }
| { kind: 'EDITING'; building: boolean }
export interface ScreenStateInput {
activeSprintItem: { id: string } | null // SSR-prop uit page.tsx
buildingSprintIds: string[] // SSR-prop uit page.tsx
hasPendingDraft: boolean // user-settings store
pendingAdds: string[] // product-workspace store
pendingRemoves: string[] // product-workspace store
}
export function deriveScreenState(i: ScreenStateInput): ScreenState {
if (i.hasPendingDraft) return { kind: 'DRAFT' } // draft wint van alles
if (i.activeSprintItem) {
const building = i.buildingSprintIds.includes(i.activeSprintItem.id)
const dirty = i.pendingAdds.length > 0 || i.pendingRemoves.length > 0
return dirty ? { kind: 'EDITING', building } : { kind: 'ACTIVE', building }
}
return { kind: 'NO_SPRINT' }
}
```
Bewust **geen** `useScreenState`-hook: consumers roepen `deriveScreenState()` inline aan met store-slices + SSR-props. `ScreenStateInput` is daar precies voor ontworpen. Hook-extractie is "straks" als meer componenten meedoen — niet nu. `PRODUCT_NOT_ACTIVE` en `DEMO_MODE` blijven **buiten** `ScreenState` (gates, geen knopen — conform het doc).
**`__tests__/stores/product-workspace/screen-state.test.ts`** — pure input→output tests, patroon van [__tests__/lib/product-switch-path.test.ts](../../__tests__/lib/product-switch-path.test.ts) (vitest, geen mocks).
### Te wijzigen bestanden
| Bestand | Wijziging |
|---|---|
| [components/shared/sprint-switcher.tsx](../../components/shared/sprint-switcher.tsx) | Leest `pendingAdds`/`pendingRemoves` uit `useProductWorkspaceStore`; `hasPendingDraft` = bestaand `draftGoal !== null` (regel 57). Roept `deriveScreenState()` aan. Trigger-label (regel 142-156) vertakt op `screenState.kind`: bij `DRAFT` toont de **trigger** `⚙ Concept — {draftGoal}` i.p.v. "Selecteer sprint"; status/BUILDING-badge verborgen in `DRAFT`. **(G1 + G5)** |
| [components/backlog/new-sprint-trigger.tsx](../../components/backlog/new-sprint-trigger.tsx) | `isActiveProduct: boolean` toevoegen aan props; `if (!isActiveProduct) return null` — spiegelt het bestaande `if (hasDraft) return null` patroon (regel 25). **(G6)** |
| [app/(app)/products/[id]/page.tsx](../../app/(app)/products/%5Bid%5D/page.tsx) | Regel 134: `isActiveProduct={isActiveProduct}` doorgeven aan `NewSprintTrigger` (`isActiveProduct` bestaat al op regel 49). **(G6)** |
| [__tests__/components/shared/sprint-switcher.test.tsx](../../__tests__/components/shared/sprint-switcher.test.tsx) | Uitbreiden: trigger toont "⚙ Concept" als er een draft is. **(G5)** |
| `__tests__/components/backlog/new-sprint-trigger.test.tsx` | Toevoegen of uitbreiden: component returnt `null` bij `isActiveProduct={false}`. **(G6)** |
### Hergebruik (niets nieuws bouwen)
- Pure-selector-patroon: `selectIsDirty` / `selectPendingCount` — [stores/product-workspace/selectors.ts:166](../../stores/product-workspace/selectors.ts:166)
- Bestaande `draftGoal`-selector — [components/shared/sprint-switcher.tsx:57](../../components/shared/sprint-switcher.tsx:57)
- Bestaande `buildingSet`-logica voor dropdown-items — [components/shared/sprint-switcher.tsx:51](../../components/shared/sprint-switcher.tsx:51)
- `sprintMembership.pending` shape `{ adds, removes }` — bestaande store-slice
- Test-patroon pure functie — [__tests__/lib/product-switch-path.test.ts](../../__tests__/lib/product-switch-path.test.ts)
## Taken (Story ST-1369)
| Code | Taak | Kern |
|---|---|---|
| T-1033 | `screen-state.ts``ScreenState` type + pure `deriveScreenState()` | Nieuw bestand, pure module |
| T-1034 | Unit tests `screen-state.test.ts` | 4 kinds + `building`-flag + precedence (draft wint) |
| T-1035 | `SprintSwitcher` op `deriveScreenState()` + G5: draft op de trigger-knop | G1-wiring + G5 |
| T-1036 | `NewSprintTrigger` achter `isActiveProduct`-gate (component + `page.tsx`) | G6 |
| T-1037 | Component-tests: sprint-switcher (G5) + new-sprint-trigger (G6) | Regressie-dekking |
## Verificatie
- `npm run verify` — lint + typecheck + test (de lokale gate; `npm run build` kan in een worktree falen op ontbrekende `DATABASE_URL`)
- `npm test -- screen-state` — de nieuwe pure-functie-tests geïsoleerd
- `npm run dev` + browser:
- **G5**: start een nieuwe sprint-draft → de SprintSwitcher-**trigger** toont "⚙ Concept — [goal]" (niet alleen de dropdown)
- **G5**: annuleer de draft → trigger valt terug op sprint-code / "Selecteer sprint"
- **G6**: open een **niet-actief** product → de "Nieuwe sprint"-knop is afwezig; activeer het product → knop verschijnt
- regressie: actieve sprint zonder draft toont gewoon code + status/BUILDING-badge
## Uitgesteld (follow-up)
**G3 — expliciete ERROR-schermstaat.** Vandaag: server-action-fout → `toast.error`, scherm blijft in huidige state. Reden uitstel: het doc adviseert dit alleen op te pakken als falende commits of SSE-verlies een aantoonbaar UX-probleem blijken; ERROR past bovendien niet natuurlijk in de pure `deriveScreenState()` (een fout is geen afgeleide van de input-flags). Vereist eerst een aparte ontwerpkeuze.