docs(T-1015): PB-workflow doc — as-is workflow-states, transitions en diagram

Tweede laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363):
de zeven impliciete workflow-states met preconditie en UI-gedrag, de
transition-tabel, en een Mermaid stateDiagram-v2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-14 22:18:43 +02:00
parent cb5150c2ae
commit bf42609d6d

View file

@ -92,3 +92,78 @@ Spiegelbeeld voor de sprint-board: stories/taken *binnen* één sprint. Zelfde s
Gebruikersvoorkeuren + cross-tab sync. Voor dit scherm cruciaal: **`workflow.pendingSprintDraft[productId]`** — de concept-sprint die ontstaat bij "Nieuwe sprint". Sinds PBI-79 is die draft **session-only**: `setPendingSprintDraft` / `clearPendingSprintDraft` schrijven alleen lokaal (geen server-roundtrip), en `hydrate()` stript legacy DB-entries weg zodat de draft niet "spookt". `upsertPbiIntent` / `upsertStoryOverride` muteren de draft-selectie.
Slice-details van het workspace-patroon (`ensure*Loaded`, selectors, optimistic mutations, gotchas) staan in [workspace-store.md](../patterns/workspace-store.md) — hier niet gedupliceerd.
## Workflow-states (as-is)
Het scherm kent vandaag **geen expliciete `screenState`**. De zichtbare toestand wordt per render ad-hoc afgeleid uit SSR-flags in `app/(app)/products/[id]/page.tsx` (`isActiveProduct`, `hasOpenSprint`, `isDemo`, plus `sprintItems` / `activeSprintItem` / `buildingSprintIds` uit `getSprintSwitcherData`) en store-flags (`pendingSprintDraft` uit `user-settings`, `sprintMembership.pending` uit `product-workspace`). Daaruit zijn **zeven** herkenbare states te destilleren.
### 1. `PRODUCT_NOT_ACTIVE`
**Preconditie:** `isActiveProduct === false` (`user.active_product_id !== id`).
**UI:** `SprintSwitcher` wordt niet gerenderd (`{isActiveProduct && …}`); `ActivateProductButton` zichtbaar. De PBI/Story/Task-panes werken normaal. `NewSprintTrigger` is wél zichtbaar (alleen demo-gated, geen `isActiveProduct`-gate); `SaveSprintButton` niet.
### 2. `PRODUCT_ACTIVE_NO_SPRINTS`
**Preconditie:** `isActiveProduct === true` && `sprintItems.length === 0`.
**UI:** `SprintSwitcher` rendert maar valt terug op de "Geen sprints"-tooltip (early return in `sprint-switcher.tsx`). `NewSprintTrigger` enabled. Geen `SaveSprintButton`, geen "Sprint actief →"-link (`hasOpenSprint === false`).
### 3. `SPRINT_DRAFT_PENDING`
**Preconditie:** `user-settings.workflow.pendingSprintDraft[productId]` is truthy (session-only).
**UI:** sticky `SprintDefinitionBanner` onder de header (`sprint-draft-banner.tsx`); `NewSprintTrigger` verbergt zichzelf (`if (hasDraft) return null`); `SprintSwitcher` toont een *disabled* "⚙ Concept — [goal]"-item; `SprintDraftLeaveGuard` registreert een `beforeunload`-waarschuwing. De cherrypick-checkboxes in de PBI/Story-panes schakelen naar draft-selectie-modus.
### 4. `ACTIVE_SPRINT_CLEAN`
**Preconditie:** `isActiveProduct` && `activeSprintItem !== null` && `selectIsDirty === false`.
**UI:** `SprintSwitcher` toont de actieve sprint-code + status; `SaveSprintButton` zichtbaar maar **disabled** (`disabled={!isDirty || …}`), label "Sprint opslaan"; "Sprint actief →"-link zichtbaar zolang er een open sprint is.
### 5. `ACTIVE_SPRINT_DIRTY`
**Preconditie:** als 4, maar `selectIsDirty === true``sprintMembership.pending.adds`/`removes` is niet leeg.
**UI:** als 4, maar `SaveSprintButton` **enabled** met teller — label `Sprint opslaan (N)`.
### 6. `ACTIVE_SPRINT_BUILDING`
**Preconditie:** `activeSprintItem` && `buildingSprintIds.includes(activeSprintItem.id)` — er loopt een `SprintRun` met status `QUEUED`/`RUNNING` (`getSprintSwitcherData`).
**UI:** als 4/5, maar `SprintSwitcher` toont een gele **"BUILDING"**-badge i.p.v. de sprint-status. Cosmetisch — er worden geen knoppen geblokkeerd.
### 7. `DEMO_MODE`
**Preconditie:** `session.isDemo === true`.
**Strikt genomen geen state** maar een **cross-cutting constraint** over alle bovenstaande states: schrijf-knoppen (`ActivateProductButton`, `SaveSprintButton`, `NewSprintTrigger`, `EditProductButton`) zijn verborgen of `disabled`; de `SprintSwitcher` navigeert i.p.v. een server-action aan te roepen; en de server-actions zelf returnen vroeg met `{ error: 'Niet beschikbaar in demo-modus' }`.
## Transitions (as-is)
Er is geen centrale overgangs-tabel; elke transitie is een server-action of store-mutatie gevolgd door een re-render:
| Van → naar | Trigger |
|---|---|
| `PRODUCT_NOT_ACTIVE``PRODUCT_ACTIVE_*` | `setActiveProductAction` (`actions/active-product.ts`) → `revalidatePath('/', 'layout')` |
| `PRODUCT_ACTIVE_NO_SPRINTS` / `ACTIVE_SPRINT_*``SPRINT_DRAFT_PENDING` | `NewSprintTrigger``NewSprintMetadataDialog``setPendingSprintDraft` (`user-settings` store, session-only) |
| `SPRINT_DRAFT_PENDING` → vorige state | `SprintDefinitionBanner` "Annuleren" → `clearPendingSprintDraft` |
| `SPRINT_DRAFT_PENDING``ACTIVE_SPRINT_CLEAN` | `SprintDefinitionBanner` "Sprint aanmaken" → `createSprintWithSelectionAction` (`actions/sprints.ts`): maakt de sprint, koppelt de geselecteerde stories, en de nieuwe sprint wordt de actieve sprint |
| `ACTIVE_SPRINT_CLEAN``ACTIVE_SPRINT_DIRTY` | `toggleStorySprintMembership` (`product-workspace` store) maakt dirty; `commitSprintMembershipAction` via `SaveSprintButton``applyMembershipCommitResult` maakt weer clean |
| `ACTIVE_SPRINT_*` → andere `ACTIVE_SPRINT_*` | `SprintSwitcher` dropdown → `switchActiveSprintAction` (`actions/active-sprint.ts`) |
| `ACTIVE_SPRINT_*``PRODUCT_ACTIVE_NO_SPRINTS` (geen actieve sprint) | `SprintSwitcher` "— Geen actieve sprint —" → `clearActiveSprintAction` |
| `ACTIVE_SPRINT_CLEAN/DIRTY``ACTIVE_SPRINT_BUILDING` | extern: een `SprintRun` gaat naar `QUEUED`/`RUNNING` resp. rondt af — zichtbaar bij de volgende SSR-render |
Server-actions roepen `revalidatePath` aan; de client doet daarnaast `router.refresh()` (bv. na sprint-creatie). De realtime-laag (SSE → `applyRealtimeEvent`) dekt *externe* wijzigingen — zie de sectie Architectuur-lagen hierboven.
## State-diagram
```mermaid
stateDiagram-v2
[*] --> PRODUCT_NOT_ACTIVE
PRODUCT_NOT_ACTIVE --> PRODUCT_ACTIVE_NO_SPRINTS: setActiveProductAction
PRODUCT_ACTIVE_NO_SPRINTS --> SPRINT_DRAFT_PENDING: Nieuwe sprint / setPendingSprintDraft
SPRINT_DRAFT_PENDING --> PRODUCT_ACTIVE_NO_SPRINTS: Annuleren / clearPendingSprintDraft
SPRINT_DRAFT_PENDING --> ACTIVE_SPRINT: Sprint aanmaken / createSprintWithSelectionAction
state ACTIVE_SPRINT {
[*] --> CLEAN
CLEAN --> DIRTY: toggleStorySprintMembership
DIRTY --> CLEAN: commitSprintMembershipAction
CLEAN --> BUILDING: SprintRun QUEUED/RUNNING
BUILDING --> CLEAN: SprintRun klaar
}
ACTIVE_SPRINT --> SPRINT_DRAFT_PENDING: Nieuwe sprint / setPendingSprintDraft
ACTIVE_SPRINT --> ACTIVE_SPRINT: switchActiveSprintAction
ACTIVE_SPRINT --> PRODUCT_ACTIVE_NO_SPRINTS: clearActiveSprintAction
```
`DEMO_MODE` staat bewust niet in het diagram: het is geen knoop in de graaf maar een read-only constraint die over álle states heen ligt.