Scrum4Me/docs/old/plans/M9-active-product-backlog.md
Janpeter Visser 2bef1a4c20
fix(ci): docs:check-links groen — exclude docs/old/ + archiveer stale plans (#193)
CI faalde sinds #191 (docs cleanup) op pre-existing broken links:
- docs/old/ bevat archief-docs met by-design stale paden
- docs/plans/PBI-79*, M9*, M11* hadden geprojecteerde paden naar
  ../backlog/index.md (verplaatst naar docs/old/backlog/) en naar
  app-bestanden die nooit met de juiste relatieve prefix waren geschreven
- docs/adr/0000* verwees naar docs-restructure-ai-lookup.md (verplaatst)
- docs/glossary.md verwees naar /docs/backlog/index.md (verplaatst)

Fixes:
- scripts/check-doc-links.mjs: skip docs/old/ recursief
- Move docs/plans/{PBI-79,M9,M11}*.md → docs/old/plans/ (allemaal merged PBIs;
  plans waren historisch)
- docs/adr/0000-record-architecture-decisions.md: update pad naar archief
- docs/glossary.md: verwijder dode "backlog index"-link

Verificatie: `npm run docs:check-links` → ✓ All doc links valid (105 files)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 21:33:47 +02:00

8 KiB

title status audience language last_updated applies_to
M9 — Actief Product Backlog active
maintainer
contributor
nl 2026-05-03
M9

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 backlog.md § M9.


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 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.tsgetLastProductCookie / 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:
      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:

    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.