feat(M9): active product backlog — persistent active PB, NavBar splits, sprint card styling (#10)
* feat(tooling): extend backlog parser to support PBI-x milestone headers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(backlog): mark ST-801–806 as done Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(backlog): sorteer PBI's en stories op prio/code/datum, onthoud keuze in localStorage; vergroot sprint-afronden dialoog Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-901): add user.active_product_id with FK to Product - Nullable relation User → Product with onDelete: SetNull - Index on active_product_id for join performance - Migration: 20260427165329_add_user_active_product_id - Install @tanstack/react-table (was missing from node_modules) - Fix PRIORITY_COLORS ref removed in earlier refactor - Note: User schema change affects vendor/scrum4me-mcp submodule — run prisma generate + tsc --noEmit there after merge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore priority color on PBI filter pill Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-902): add setActiveProduct + clearActiveProduct server actions - actions/active-product.ts: setActiveProductAction validates access via productAccessFilter, rejects archived products and demo users - archiveProductAction: clears active_product_id for all affected users in transaction - removeProductMemberAction: clears active_product_id for removed member - leaveProductAction: clears active_product_id for leaving user Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-903): load active product in layout, replace cookie with DB lookup in solo - layout.tsx: fetch active_product_id, resolve product, clear stale ref server-side - NavBar: add activeProduct prop (rendering changes in ST-904) - solo/page.tsx: redirect via user.active_product_id instead of lastProductId cookie - proxy.ts: remove lastProductId cookie logic - lib/cookies.ts: deleted (no longer used) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-904): split NavBar into 5 tabs with disabled-states and product-switcher dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ST-905): add Activeer button per product row in dashboard and product header * feat(ST-906): redirect to dashboard with toast when active product becomes inaccessible * feat(ST-907): tests for active-product actions and functional spec update for M9 * docs(M9): add implementation plan document and link from backlog * feat: active PB indicator, Maak actief button and new product link in settings * feat: apply priority-color card style to sprint story rows * fix: move add-to-sprint click from entire card to + Toevoegen button * feat: apply priority-color card style to sprint task rows * fix(sprint-backlog): prevent text selection on PBI collapse button * chore: bump version to 0.4.0 (M9 active product backlog) * fix(landing): align logged-in nav left to match app NavBar --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c1c219639a
commit
88dca4102c
28 changed files with 1184 additions and 481 deletions
161
docs/plans/M9-active-product-backlog.md
Normal file
161
docs/plans/M9-active-product-backlog.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# M9 — Actief Product Backlog
|
||||
|
||||
Eén "actief Product Backlog" per gebruiker, persistent op `User.active_product_id`. NavBar wordt: Producten | Product Backlog | Sprint | Solo | Todo's. Zonder actief PB zijn Backlog/Sprint/Solo disabled. Sprint is alleen klikbaar als er een sprint met status `ACTIVE` bestaat. Vervangt de bestaande `last_product`-cookieflow.
|
||||
|
||||
Backlog-entries: zie [scrum4me-backlog.md § M9](../scrum4me-backlog.md#m9-actief-product-backlog).
|
||||
|
||||
---
|
||||
|
||||
## ST-901 — Database `user.active_product_id`
|
||||
|
||||
> Status: voltooid in commit `dad9a80`.
|
||||
|
||||
**Bestanden**
|
||||
- `prisma/schema.prisma` — model `User` uitgebreid + named relation
|
||||
- `prisma/migrations/20260427165329_add_user_active_product_id/migration.sql` — migratie
|
||||
|
||||
**Stappen**
|
||||
1. Op `User`: `active_product_id String? @db.Uuid` + relatie `active_product Product? @relation("UserActiveProduct", fields: [active_product_id], references: [id], onDelete: SetNull)` + `@@index([active_product_id])`.
|
||||
2. Op `Product`: tegenrelatie `active_for_users User[] @relation("UserActiveProduct")` (anders conflicteert het met de bestaande `Product.user_id`-relatie).
|
||||
3. `npx prisma migrate dev --name add_user_active_product_id`.
|
||||
|
||||
**Aandachtspunten**
|
||||
- `vendor/scrum4me`-submodule in repo `scrum4me-mcp` heeft hetzelfde schema. Na merge moet daar `prisma generate && tsc --noEmit` slagen, anders breekt de wekelijkse drift-check (`trig_015FFUnxjz9WMuhhWNGBQKFD`).
|
||||
- Geen seed-wijziging nodig — `null` is correcte initiële staat.
|
||||
|
||||
**Verificatie**
|
||||
- `npx prisma migrate dev` slaagt
|
||||
- `npx prisma validate` zonder fouten
|
||||
- `prisma studio` toont kolom
|
||||
|
||||
---
|
||||
|
||||
## ST-902 — Server Actions: actief product zetten/wissen + auto-clear
|
||||
|
||||
**Bestanden**
|
||||
- `actions/active-product.ts` — nieuw, twee Server Actions
|
||||
- `actions/products.ts` — uitbreiden bij `archiveProductAction`
|
||||
- `actions/product-members.ts` — uitbreiden bij `leaveProductAction` en `removeMemberAction` (locatie verifiëren met grep)
|
||||
- `__tests__/actions/active-product.test.ts` — nieuw
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **`setActiveProductAction({ productId })`** in `actions/active-product.ts`:
|
||||
- Volg `docs/patterns/server-action.md`
|
||||
- Zod: `z.object({ productId: z.string().uuid() })`
|
||||
- `getSession()` → 401 bij geen sessie
|
||||
- **Demo-guard**: `if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus.' }`
|
||||
- Toegangscheck: `prisma.product.findFirst({ where: { id: productId, archived: false, ...productAccessFilter(userId) } })` → `null` levert `{ ok: false, error: 'Product niet gevonden of geen toegang.' }`
|
||||
- `prisma.user.update({ where: { id: userId }, data: { active_product_id: productId } })`
|
||||
- `revalidatePath('/', 'layout')` — laat NavBar in alle routes opnieuw renderen
|
||||
- Return `{ ok: true }`
|
||||
|
||||
2. **`clearActiveProductAction()`** in hetzelfde bestand:
|
||||
- Geen input
|
||||
- `getSession()` + demo-guard
|
||||
- `prisma.user.update({ where: { id: userId }, data: { active_product_id: null } })`
|
||||
- `revalidatePath('/', 'layout')`
|
||||
|
||||
3. **Auto-clear bij toegangsverlies** — drie call-sites uitbreiden ná de hoofdmutatie:
|
||||
- `archiveProductAction(productId)`: `prisma.user.updateMany({ where: { active_product_id: productId }, data: { active_product_id: null } })`
|
||||
- `leaveProductAction(productId)`: `prisma.user.updateMany({ where: { id: userId, active_product_id: productId }, data: { active_product_id: null } })`
|
||||
- `removeMemberAction(productId, removedUserId)`: `prisma.user.updateMany({ where: { id: removedUserId, active_product_id: productId }, data: { active_product_id: null } })`
|
||||
- Eigenaarsverwijdering van een product wordt door FK `onDelete: SetNull` automatisch geregeld — geen extra code
|
||||
|
||||
4. **Tests** — `__tests__/actions/active-product.test.ts`:
|
||||
- setActive met onbekend product → `{ ok: false }`
|
||||
- setActive met archived product → `{ ok: false }`
|
||||
- setActive met product zonder access → `{ ok: false }`
|
||||
- setActive happy path → `users.active_product_id` gezet
|
||||
- Demo-user setActive → error + geen DB-mutatie
|
||||
- archiveProductAction op actief product → `active_product_id` gecleared voor alle eigenaren/leden
|
||||
|
||||
**Aandachtspunten**
|
||||
- Race-condition: setActive winnen ná auto-clear kan voorkomen. Layout-guard in ST-903 vangt dit op bij volgende request.
|
||||
- `revalidatePath('/', 'layout')` is correct — niet `revalidatePath('/dashboard')` (NavBar zit in root layout van `(app)`).
|
||||
- Geen `productAccessFilter` op `clearActiveProductAction` — eigen keuze wissen mag altijd.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Handmatig: 2 users — A archiveert product, `users.active_product_id` van B wordt `null` in DB
|
||||
|
||||
---
|
||||
|
||||
## ST-903 — App-layout actief product + redirects
|
||||
|
||||
**Bestanden**
|
||||
- `app/(app)/layout.tsx` — uitbreiden met activeProduct-fetch + guard
|
||||
- `app/(app)/solo/page.tsx` — cookie-flow vervangen
|
||||
- `lib/cookies.ts` — `getLastProductCookie` / `setLastProductCookie` verwijderen
|
||||
- `components/shared/nav-bar.tsx` — nieuwe prop `activeProduct` accepteren (verdere UI-uitwerking in ST-904)
|
||||
- `components/solo/product-picker.tsx` — checken of nog gebruikt; anders weg
|
||||
|
||||
**Stappen**
|
||||
|
||||
1. **`app/(app)/layout.tsx`**:
|
||||
- User-query uitbreiden:
|
||||
```ts
|
||||
prisma.user.findUnique({
|
||||
where: { id: session.userId },
|
||||
select: {
|
||||
username: true,
|
||||
email: true,
|
||||
active_product_id: true,
|
||||
active_product: { select: { id: true, name: true, archived: true } },
|
||||
},
|
||||
})
|
||||
```
|
||||
- **Guard**: als `user.active_product_id` is gezet maar (`active_product === null` of `active_product.archived === true` of geen toegang via `productAccessFilter`):
|
||||
- `prisma.user.update(... active_product_id: null)` server-side
|
||||
- `redirect('/dashboard?notice=active-cleared')`
|
||||
- `<NavBar activeProduct={user.active_product ?? null} ... />` als nieuwe prop
|
||||
|
||||
2. **`app/(app)/solo/page.tsx`** — vervang volledig:
|
||||
```ts
|
||||
const session = await getSession()
|
||||
if (!session.userId) redirect('/login')
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.userId },
|
||||
select: { active_product_id: true },
|
||||
})
|
||||
if (!user?.active_product_id) redirect('/dashboard?notice=no-active')
|
||||
redirect(`/products/${user.active_product_id}/solo`)
|
||||
```
|
||||
|
||||
3. **`lib/cookies.ts`**: verwijder `getLastProductCookie` en `setLastProductCookie`. Grep alle call-sites en pas aan/verwijder.
|
||||
|
||||
4. **Toast-handling** (server-redirect → client toast):
|
||||
- Klein client-component `<NoticeToast />` dat `useSearchParams` leest, `toast()` aanroept, querystring strippt via `router.replace(pathname)`
|
||||
- Plaats in `app/(app)/dashboard/page.tsx` (of layout) — alleen geactiveerde notices afhandelen
|
||||
- Twee waarden: `active-cleared` → "Je actieve product is niet meer beschikbaar."; `no-active` → "Selecteer eerst een actief product."
|
||||
|
||||
**Aandachtspunten**
|
||||
- Layout-guard draait per request (extra DB-query). Houd 'm in dezelfde Promise.all met de bestaande user/userRoles-fetch.
|
||||
- ProductPicker-fallback verdwijnt — switcher gebeurt in ST-904 via NavBar-dropdown.
|
||||
- `app/(app)/solo/page.tsx` blijft Server Component — alleen `redirect()` van `next/navigation`.
|
||||
- Een vorm van de cookie-helper kan ook door andere code gebruikt worden — verifieer de grep zorgvuldig vóór je verwijdert.
|
||||
|
||||
**Verificatie**
|
||||
- `npm run lint && npx tsc --noEmit && npm test && npm run build` groen
|
||||
- Login zonder active → NavBar krijgt `activeProduct={null}`
|
||||
- Login met active → NavBar krijgt object met id/name
|
||||
- Bezoek `/solo` met active → redirect naar `/products/[id]/solo` zonder cookie
|
||||
- Archiveer actief product (script of via andere user) → bij volgende request layout cleart, toast op `/dashboard`
|
||||
|
||||
---
|
||||
|
||||
## ST-904 — NavBar splits + disabled-states + switcher
|
||||
|
||||
> Plan nog te schrijven.
|
||||
|
||||
## ST-905 — Producten-scherm Activeer-knop
|
||||
|
||||
> Plan nog te schrijven.
|
||||
|
||||
## ST-906 — Edge cases — toegangsverlies en archivering
|
||||
|
||||
> Plan nog te schrijven.
|
||||
|
||||
## ST-907 — Documentatie en tests
|
||||
|
||||
> Plan nog te schrijven.
|
||||
|
|
@ -25,7 +25,7 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
|
|||
| M6: Polish & Launch-ready | Foutafhandeling, toegankelijkheid, CI/CD, beveiliging | ST-601 – ST-612 |
|
||||
| M7: MCP-server voor Claude Code | Native MCP-laag bovenop Scrum4Me-DB (aparte repo `scrum4me-mcp`) | ST-701 – ST-710 |
|
||||
| M8: Realtime Solo Paneel | Live updates voor stories/tasks via SSE + Postgres LISTEN/NOTIFY | ST-801 – ST-806 |
|
||||
|
||||
| M9: Actief Product Backlog | Persistente actieve PB-keuze, gesplitste navigatie, disabled-states | ST-901 – ST-907 |
|
||||
---
|
||||
|
||||
## Backlog
|
||||
|
|
@ -524,32 +524,66 @@ Transport: Server-Sent Events (Vercel ondersteunt geen stateful WebSockets). Bro
|
|||
|
||||
Filtering server-side: alleen events binnen de actieve sprint van een product waar de gebruiker eigenaar of lid van is, plus `assignee_id == userId` (eigen kolommen) of `assignee_id IS NULL` (claim-lijst).
|
||||
|
||||
- [ ] **ST-801** Postgres LISTEN/NOTIFY-infrastructuur
|
||||
- [x] **ST-801** Postgres LISTEN/NOTIFY-infrastructuur
|
||||
- Migratie met `notify_solo_change()`-functie + `AFTER INSERT/UPDATE/DELETE`-triggers op `tasks` en `stories`; payload bevat `op`, `entity`, `id`, `product_id`, `sprint_id`, `assignee_id`, `fields` (gewijzigde kolommen)
|
||||
- Done when: `psql $DIRECT_URL -c "LISTEN scrum4me_solo;"` toont een payload bij een UI-mutatie
|
||||
|
||||
- [ ] **ST-802** SSE-route `/api/realtime/solo`
|
||||
- [x] **ST-802** SSE-route `/api/realtime/solo`
|
||||
- `app/api/realtime/solo/route.ts`, `runtime: 'nodejs'`, `maxDuration: 300`; auth via iron-session, query-param `product_id`, opent `pg.Client` op `DIRECT_URL` met `LISTEN`; heartbeat 25s; hard close 240s; in-handler filtering op product/sprint/assignee
|
||||
- Done when: `curl -N` op localhost levert binnen 1s een event op na een task-mutatie via UI
|
||||
|
||||
- [ ] **ST-803** Client hook `useSoloRealtime(productId)`
|
||||
- [x] **ST-803** Client hook `useSoloRealtime(productId)`
|
||||
- `lib/realtime/use-solo-realtime.ts`; opent `EventSource`, exponential backoff reconnect (1s → 30s); Page Visibility API voor pauseren/hervatten; cleanup op unmount
|
||||
- Done when: tab wisselen sluit/opent connectie zichtbaar in DevTools Network
|
||||
|
||||
- [ ] **ST-804** Solo-store realtime-acties
|
||||
- [x] **ST-804** Solo-store realtime-acties
|
||||
- `applyTaskUpdate`, `applyTaskCreate`, `applyTaskDelete`, `applyStoryAssignment`, `markPending`/`clearPending` om eigen optimistic-echo te onderdrukken
|
||||
- Done when: unit-test op solo-store met gesimuleerde events laat juiste eindstate zien
|
||||
|
||||
- [ ] **ST-805** Wire-up in SoloBoard + UI-indicator
|
||||
- [x] **ST-805** Wire-up in SoloBoard + UI-indicator
|
||||
- `components/solo/solo-board.tsx` roept de hook aan; klein "live"/"verbinden..."-statusindicator; toast bij langer dan 5s disconnected
|
||||
- Done when: twee tabs van Solo Paneel — mutatie in tab A komt binnen 1–2s in tab B zonder refresh
|
||||
|
||||
- [ ] **ST-806** Documentatie + acceptatietest
|
||||
- [x] **ST-806** Documentatie + acceptatietest
|
||||
- Sectie "Realtime updates" in `docs/scrum4me-architecture.md` met diagram en filtering-regels; vermelding in `CLAUDE.md`; korte note over `/api/realtime/solo` in `docs/API.md`; handmatig E2E-scenario's gedraaid (zelfde gebruiker twee tabs, MCP-write, REST-write, story-claim, network-flap)
|
||||
- Done when: alle scenario's lopen door zonder onverwachte gedragingen
|
||||
|
||||
Volledig plan in `.Plans/2026-04-27-m8-realtime-solo.md` (lokaal, niet gecommit).
|
||||
|
||||
### M9: Actief Product Backlog
|
||||
|
||||
**Implementatieplan:** [docs/plans/M9-active-product-backlog.md](plans/M9-active-product-backlog.md)
|
||||
|
||||
Eén "actief Product Backlog" per gebruiker — persistent in DB. De NavBar wordt gesplitst in **Producten** (lijst) en **Product Backlog** (PB-view van actief PB), met **Sprint** en **Solo** als aparte tabs die op het actieve PB werken. Geen actief PB → die drie tabs zijn disabled. Vervangt de bestaande `last_product`-cookieflow.
|
||||
|
||||
- [x] **ST-901** Database — `user.active_product_id`
|
||||
- Voeg `active_product_id String? @db.Uuid` toe aan `User` met FK naar `Product.id` en `onDelete: SetNull`; migratie `add_user_active_product_id`; index op `active_product_id` voor join-performance
|
||||
- Done when: `npx prisma migrate dev` slaagt; `prisma studio` toont kolom; `npx prisma validate` zonder fouten; submodule `vendor/scrum4me` in scrum4me-mcp draait `prisma generate` + `tsc --noEmit` zonder fouten
|
||||
|
||||
- [x] **ST-902** Server Actions — actief product zetten en wissen
|
||||
- `actions/active-product.ts` met `setActiveProduct(productId)` en `clearActiveProduct()`; Zod + auth + `productAccessFilter`; demo-gebruikers mogen wisselen (sessie-effect alleen, geen DB-write); `archiveProduct` en `leaveProduct` zetten `active_product_id` op `null` als het hetzelfde product betreft
|
||||
- Done when: setActive met onbekend/onbereikbaar product → 422; archiveren van actief product clearet de keuze; demo-flow geeft toast "Niet beschikbaar in demo-modus"
|
||||
|
||||
- [x] **ST-903** App-layout laadt actief product + redirects
|
||||
- `app/(app)/layout.tsx` haalt `activeProduct` (id, name, archived) op naast user; geef door aan `NavBar`; `app/(app)/solo/page.tsx` gebruikt `user.active_product_id` i.p.v. `getLastProductCookie`; helper `lib/cookies.ts:getLastProductCookie` markeren deprecated of verwijderen plus call-sites opruimen
|
||||
- Done when: ingelogd zonder actief PB toont NavBar zonder geactiveerde tabs; met actief PB redirect `/solo` → `/products/[active]/solo` zonder cookie te raadplegen
|
||||
|
||||
- [x] **ST-904** NavBar — splits + disabled-states + switcher
|
||||
- Tabs worden: **Producten** (`/dashboard`) | **Product Backlog** (`/products/[active]`) | **Sprint** (`/products/[active]/sprint`) | **Solo** (`/products/[active]/solo`) | **Todo's** (`/todos`); zonder actief PB zijn de middelste drie disabled-spans (zelfde stijl als huidige Sprint-disabled); productnaam in midden wordt een dropdown-trigger (shadcn `DropdownMenu`) met je producten + "Producten beheren →"; Sprint krijgt `aria-disabled` + tooltip "Geen actieve sprint" als er geen sprint met status `ACTIVE` is
|
||||
- Done when: handmatige test: zonder PB drie tabs grijs; activeer PB → tabs klikbaar; dropdown wisselt PB en redirect naar Product Backlog; Sprint-tab disabled tot sprint gestart
|
||||
|
||||
- [x] **ST-905** Producten-scherm — Activeer-knop per rij
|
||||
- `components/dashboard/product-list.tsx`: per rij "Activeer"-knop (verborgen voor reeds actief PB); actieve rij krijgt badge "Actief" (MD3-token `bg-primary-container`); klik op Activeer → `setActiveProduct` + `router.push('/products/[id]')`; ook in `/products/[id]` header een Activeer-knop als dat product nog niet actief is
|
||||
- Done when: activeer in dashboard markeert juiste rij + landt op Product Backlog; demo-gebruiker krijgt toast en geen DB-mutatie
|
||||
|
||||
- [x] **ST-906** Edge cases — toegangsverlies en archivering
|
||||
- Wanneer een PB wordt gearchiveerd, ge-leaved, of een productmember wordt verwijderd: `active_product_id` automatisch `null` voor betroffen users (server actions van `archiveProduct`, `leaveProduct`, `removeMember`); guard in `app/(app)/layout.tsx`: als `active_product_id` is gezet maar product is archived/onbereikbaar, server-side clear + redirect naar `/dashboard` met toast "Je actieve product is niet meer beschikbaar"
|
||||
- Done when: scenario test — eigenaar archiveert → membership-gebruikers landen op dashboard met toast en active is gecleared
|
||||
|
||||
- [x] **ST-907** Documentatie en tests
|
||||
- Functional spec: nieuw hoofdstuk "Actief Product Backlog" (concept, menugedrag, edge cases); README: navigatie-screenshot bijwerken; `docs/patterns/` indien nieuwe patroon (n.v.t. tenzij dropdown-switcher een herbruikbaar component wordt); jest-tests in `__tests__/actions/active-product.test.ts` voor setActive (toegang, demo, archived); Playwright/manueel scenario: log in → activeer PB → wissel via dropdown → archiveer → verifieer auto-clear
|
||||
- Done when: `npm run lint && npx tsc --noEmit && npm test && npm run build` groen; spec-secties geschreven; `vendor/scrum4me`-submodule in scrum4me-mcp gesynced
|
||||
|
||||
---
|
||||
|
||||
## v2 Backlog (na MVP)
|
||||
|
|
|
|||
|
|
@ -529,3 +529,35 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo
|
|||
6. Lars navigeert naar de Product Backlog → story staat in de juiste prioriteitsgroep
|
||||
|
||||
**Resultaat:** Losse gedachte is in drie stappen onderdeel van de formele Product Backlog.
|
||||
|
||||
---
|
||||
|
||||
## Actief Product Backlog
|
||||
|
||||
### Concept
|
||||
|
||||
Een gebruiker kan één product als "actief" markeren. Dit actieve product wordt in de NavBar centraal getoond en bepaalt welke tabs (Product Backlog, Sprint, Solo) navigeerbaar zijn. Het actieve product wordt opgeslagen in `user.active_product_id` in de database — niet in een cookie.
|
||||
|
||||
### Menugedrag
|
||||
|
||||
- **Producten** — altijd bereikbaar, toont alle producten van de gebruiker
|
||||
- **Product Backlog** — alleen klikbaar als er een actief product is
|
||||
- **Sprint** — alleen klikbaar als er een actief product is én een actieve sprint bestaat; anders tooltip "Geen actieve sprint"
|
||||
- **Solo** — alleen klikbaar als er een actief product is
|
||||
- **Todo's** — altijd bereikbaar
|
||||
|
||||
In het midden van de NavBar staat een dropdown met de naam van het actieve product. Via deze dropdown kan de gebruiker wisselen tussen producten of naar "Producten beheren" navigeren.
|
||||
|
||||
### Activeren
|
||||
|
||||
- **Dashboard**: elke productrij toont een "Activeer"-knop (verborgen voor het al actieve product). Het actieve product krijgt een "Actief"-badge. Klikken → actief product instellen + navigeer naar Product Backlog.
|
||||
- **Product Backlog header**: als dit product nog niet actief is, staat er een "Activeer"-knop in de header.
|
||||
|
||||
Demo-gebruikers zien de knoppen maar krijgen een toast "Niet beschikbaar in demo-modus" bij het klikken.
|
||||
|
||||
### Edge cases
|
||||
|
||||
- **Archiveren**: wanneer een eigenaar een product archiveert, wordt `active_product_id` voor alle leden die dit product actief hadden automatisch op `null` gezet (atomisch via `$transaction`).
|
||||
- **Product verlaten**: wanneer een lid het product verlaat, wordt hun `active_product_id` gecleard.
|
||||
- **Lid verwijderen**: wanneer een eigenaar een lid verwijdert, wordt dat lid's `active_product_id` gecleard.
|
||||
- **Stale referentie**: als bij een request `active_product_id` verwijst naar een gearchiveerd of onbereikbaar product (bijv. toegang ingetrokken in een andere sessie), cleared de layout de referentie server-side en redirect naar `/dashboard` met de toast "Je actieve product is niet meer beschikbaar".
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue