Scrum4Me/docs/plans/M9-active-product-backlog.md
Janpeter Visser e10f8f81bc
Phase 2 — Normalize file naming (#59)
* docs(naming): drop scrum4me- prefix from doc filenames

Rename 10 docs/scrum4me-*.md files to unprefixed kebab-case names.
Update every internal link in docs/, CLAUDE.md, AGENTS.md, README.md.

* docs(naming): lowercase API.md and MD3 filenames

Rename docs/API.md → docs/api.md and
docs/MD3_Color_Scheme_Documentation.md → docs/md3-color-scheme.md.
Update all internal links across 7 files.

* docs(naming): rename plan file to kebab-case ASCII

Rename "docs/plans/Tweede Claude Agent — Planning Agent.md"
→ docs/plans/tweede-claude-agent-planning.md. No external links needed updating.

* docs(naming): rename middleware.md to proxy.md (next 16)

docs/patterns/middleware.md → docs/patterns/proxy.md following
the Next.js 16 proxy.ts rename. Update link in CLAUDE.md.

* docs(naming): polish CLAUDE.md doc-index after renames

Fix doubled scrum4me-scrum4me-mcp repo references (cascade from
prior sed) in CLAUDE.md, docs/architecture.md, backlog.md,
agent-instruction-audit.md, and plans/ST-1109. Update
'Middleware' label to 'Proxy middleware' in patterns table.
2026-05-03 03:00:47 +02:00

7.8 KiB

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 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.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.