Scrum4Me/docs/plans/M9-active-product-backlog.md
Janpeter Visser 88dca4102c
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>
2026-04-27 20:25:13 +02:00

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