docs(PBI-79): plan-update met implementatie-stand + scope-aanpassing

Documenteert wat er sinds de eerste implementatie-pass is gebeurd:
- Tabel van 14 commits met hun rol.
- Twee bugs die tijdens testen boven kwamen (PBI-rij-klik, cascade-restore).
- Nieuwe feature sprint-switch auto-select (server resolveert single-PBI/
  single-story; user-settings persist).

En kondigt scope-aanpassing aan voor de volgende implementatie-ronde:
- pendingSprintDraft wordt session-only (geen server-persist meer).
- useDirtyCloseGuard wist draft op leave-with-confirm.
- Sprint-switcher krijgt concept-entry zolang er een draft loopt.

De rest van het plan beneden blijft van kracht behalve waar deze sectie
het overruled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-11 18:25:07 +02:00
parent d7d11124e3
commit e4252cad3e

View file

@ -4,6 +4,87 @@
>
> **Review verwerkt:** Dit plan is een herziene versie na de review in [`product-backlog-workflow-plan-review.md`](product-backlog-workflow-plan-review.md). De vier P1-bevindingen zijn allemaal geadresseerd, evenals de vijf P2-punten. Zie de sectie *"Reactie op review"* onderaan voor de mapping.
---
## Implementatie-stand & scope-aanpassingen (post-testing)
> Deze sectie documenteert wat er sinds de eerste implementatie-pass is bijgewerkt op basis van gebruikerstests + nieuwe inzichten. De rest van het plan beneden geldt **behalve waar dit kopje dat overrulet**.
### Gerealiseerde commits (in volgorde)
| # | Commit | Story | Inhoud |
|---|---|---|---|
| 1 | 2af6f24 | ST-1333 | Active-sprint null-contract + clearActiveSprintAction |
| 2 | 56c55e1 | ST-1334 | pendingSprintDraft slot (compacte intent-shape) |
| 3 | b4a515e | ST-1343 | `lib/sprint-conflicts.ts` eligibility helpers |
| 4 | e89fb71 | ST-1335 | Gescoped endpoints (`sprint-membership-summary`, `cross-sprint-blocks`) |
| 5 | 89c2356 | ST-1336 | `sprintMembership`-slice + selectors in product-workspace-store |
| 6 | 947d970 | ST-1337 | State A UI (metadata-dialog + sticky banner + PbiList ombouw) |
| 7 | d21011c | ST-1339 | `createSprintWithSelectionAction` + banner wire-up |
| 8 | 4c6e999 | ST-1340 | `commitSprintMembershipAction` + gerichte client-store patches |
| 9 | 117616f | ST-1338 | State B vinkjes-UI + "Sprint opslaan"-knop |
| 10 | b91d92a | ST-1341+1342 | `SprintEditDialog` + multi-OPEN sprints |
| 11 | 0c36f4e | ST-1344 | `updateSprintAction` regression tests |
| 12 | 8d6fbdf | bugfix | PBI-rij weer klikbaar voor selectie; vinkje als aparte trigger |
| 13 | 35c6404 | bugfix | Cascade-restore alleen wanneer hint-story bij nieuwe PBI hoort |
| 14 | d7d1112 | feat | Sprint-switch auto-select PBI/story + user-settings persist (3 keys) |
### Bugs gevonden tijdens testen (afgehandeld)
1. **Hele PBI-rij was de toggle in selectionMode.** Gevolg: rij-klik bulk-toggled stories en update de teller, maar PBI werd niet als focus geselecteerd → story-kolom bleef leeg.
*Fix (8d6fbdf):* in `SortablePbiRow` selectionMode-branch wordt onClick weer `onSelect`; het tri-state icoon zit in een eigen `<button>` met `stopPropagation`.
2. **Cascade-restore overschrijft PBI-switch.** Bij wisselen naar een andere PBI bleef de oude story (en dus zijn taken) zichtbaar omdat `setActivePbi`'s async hint-restore de vorige story-id terugzette zonder PBI-validatie.
*Fix (35c6404):* hint wordt alleen toegepast als `storiesById[hint].pbi_id === pbiId`.
3. **Tooltip-API mismatch.** `TooltipTrigger` van base-ui accepteert geen `asChild`; geprobeerd via render-prop maar uiteindelijk de hele knop in selectionMode in de Tooltip gewikkeld.
### Nieuwe feature (na implementatie toegevoegd) — sprint-switch auto-select
Bij wisselen van sprint via de switcher wordt **server-side** de inhoud van de sprint geresolved en als deze precies één PBI heeft (en die PBI exact één story binnen de sprint), worden beide automatisch geselecteerd. Alle drie selectie-velden worden atomair in user-settings weggeschreven zodat cross-device-restore klopt.
- Schema: `layout.activePbis` + `layout.activeStories` per product (beide nullable).
- Helper: `setActiveSelectionInSettings(userId, productId, { sprintId, pbiId?, storyId? })`.
- Server-action: `switchActiveSprintAction(productId, sprintId)` doet de auto-select-resolutie en returnt het tripel.
- Sprint-switcher: roept de nieuwe action aan en synchroniseert de client-store gelijk (geen flash).
- `ActiveSelectionHydrator` (nieuw): client-side effect dat user-settings-activePbi/activeStory naar de workspace-store spiegelt; wint van de bestaande localStorage hint-restore.
### Scope-aanpassing — pendingSprintDraft wordt **session-only**
**Was:** de draft (sprint-doel + per-PBI intent + per-PBI overrides) staat persistent in `user-settings.workflow.pendingSprintDraft` zodat de gebruiker na navigatie kan hervatten.
**Wordt:** de draft leeft alleen in de Zustand-store van de sessie. Bij wegnavigeren krijgt de gebruiker een `useDirtyCloseGuard`-confirm; bij doorgaan wordt de draft **weggegooid** (niet hervat-baar). Reden: de user geeft expliciet aan dat ongeslagen sprints geen rest-state mogen achterlaten in de DB.
Concrete wijzigingen:
- `lib/user-settings.ts`: `workflow.pendingSprintDraft` kan blijven bestaan voor type-compatibiliteit maar wordt niet meer geschreven door de UI.
- Actions `setPendingSprintDraftAction` + `clearPendingSprintDraftAction` worden gedeprecieerd (of behouden voor migratie van eventueel oude entries) maar **niet meer aangeroepen** door de UI.
- Store `useUserSettingsStore.setPendingSprintDraft` / `upsertPbiIntent` / `upsertStoryOverride` blijven bestaan maar de server-roundtrip eruit; lokale state-only.
- `useDirtyCloseGuard` op het banner-niveau triggert een confirm bij browser-back / route-wissel; bevestigen → `clearPendingSprintDraftAction` (om eventuele oude DB-entries op te ruimen) **+** lokale state-reset.
### Nieuwe feature — draft-sprint zichtbaar in sprint-switcher
Tijdens state A (er is een draft) toont de sprint-switcher de **draft-naam** (= `draft.goal`, ingekort) als extra entry bovenaan de dropdown met markering "Concept" of italic-styling. Hij is niet selecteerbaar als "actieve" sprint (want geen sprintId); klikken erop opent de banner-actie of doet niets bijzonders. Doel: visueel feedback geven dat er een onafgemaakte sprint loopt zonder die in de DB op te slaan.
Concreet:
- Sprint-switcher krijgt prop `pendingDraftGoal?: string | null` (server-side leesbaar via user-settings store na hydration, of via `useUserSettingsStore` in de switcher-component).
- Render bovenaan de dropdown (boven "— Geen actieve sprint —") wanneer aanwezig: *"⚙ Concept — [goal-prefix]"*.
### Wat blijft staan uit de oorspronkelijke ontwerpkeuzes
- Schema `layout.activeSprints` blijft nullable (key+null = bewust geen sprint).
- Drie-states-model (A / A / B) blijft.
- Tri-state PBI-vinkje, story-binair-vinkje, cross-sprint disabled blijven.
- "Sprint opslaan"-knop met teller (state B) blijft.
- Eligibility-filter + status-mutaties in dezelfde transactie blijven.
- Endpoints gescoped op `pbiIds` blijven.
- Multi-OPEN sprints toegestaan blijft.
### Wat nog te doen (na deze plan-update)
1. **Implementeer scope-aanpassing**: maak pendingSprintDraft session-only. UI ondervangt server-roundtrip; bestaande server-actions blijven voor cleanup-doeleinden.
2. **Sprint-switcher concept-entry**: render draft-goal in dropdown.
3. **Verifieer**: A → A → leave-with-confirm pad → geen DB-entry achtergelaten.
---
## Context
De Product Backlog-pagina (`/products/[id]`) is het hart van Scrum4Me. De **lazy-load-basis bestaat al** (filter-first/background-remaining-PBI's + lazy stories/tasks per klik via [lib/product-backlog-pbis.ts](lib/product-backlog-pbis.ts), `ensurePbiLoaded`, `ensureStoryLoaded`). Dit plan bouwt daarop voort, het herontwerpt dat fundament niet.