feat(PBI-71): UX-fix 'lege sprint' + sprint-switch data-refresh (#175)

- StartSprintButton dialog toont 3-state banner: info met accurate vrije-
  stories count + PBI-context, of waarschuwing als geen PBI geselecteerd
  is, of waarschuwing als de geselecteerde PBI 0 vrije stories heeft
- Voeg sprint_id toe aan BacklogStory/Story/SprintStory + select in PB-
  pagina's en sprint-board mappings, zodat de banner accuraat kan tellen
- createSprintAction: revalidatePath met 'layout' flag voor consistency
  met createSprintWithPbisAction (top-nav 'Sprint' link ververst direct)

Sprint-switch data-refresh op alle relevante pagina's:

- BacklogHydrationWrapper: fingerprint-based re-hydratie zodat PB-data
  na router.refresh opnieuw uit nieuwe initialData komt (was: useEffect
  met lege deps draaide alleen 1x)
- SprintBoardClient: key={sprint.id} forceert remount bij sprint-switch
  zodat lokale sprintStories/sprintStoryIds-state vers ge-init wordt
- Solo (desktop + mobile): gebruik resolveActiveSprint(id) ipv eerste
  OPEN-sprint, plus key={sprint.id} op SoloBoard voor remount

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-09 16:27:24 +02:00 committed by GitHub
parent 35e37dac09
commit 71319e629d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 72 additions and 14 deletions

View file

@ -61,6 +61,7 @@ export default async function ProductBacklogPage({ params, searchParams }: Props
priority: true,
status: true,
pbi_id: true,
sprint_id: true,
created_at: true,
},
}),

View file

@ -2,6 +2,7 @@ import { notFound, redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getAccessibleProduct } from '@/lib/product-access'
import { prisma } from '@/lib/prisma'
import { resolveActiveSprint } from '@/lib/active-sprint'
import { getSprintSwitcherData } from '@/lib/sprint-switcher-data'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
@ -21,9 +22,10 @@ export default async function SoloProductPage({ params }: Props) {
const product = await getAccessibleProduct(id, session.userId)
if (!product) notFound()
const sprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'OPEN' },
})
const active = await resolveActiveSprint(id)
const sprint = active
? await prisma.sprint.findFirst({ where: { id: active.id, product_id: id } })
: null
const switcherData = await getSprintSwitcherData(id, { activeSprintId: sprint?.id ?? null })
@ -126,6 +128,7 @@ export default async function SoloProductPage({ params }: Props) {
{switcherBar}
<div className="flex-1 min-h-0">
<SoloBoard
key={sprint.id}
productId={id}
sprintGoal={sprint.sprint_goal}
tasks={tasks}

View file

@ -94,6 +94,7 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
description: s.description,
acceptance_criteria: s.acceptance_criteria,
pbi_id: s.pbi_id,
sprint_id: s.sprint_id,
created_at: s.created_at,
priority: s.priority,
status: s.status,
@ -144,6 +145,7 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
description: s.description,
acceptance_criteria: s.acceptance_criteria,
pbi_id: s.pbi_id,
sprint_id: s.sprint_id,
created_at: s.created_at,
priority: s.priority,
status: s.status,
@ -191,6 +193,7 @@ export default async function SprintBoardPage({ params, searchParams }: Props) {
<div className="flex-1 overflow-hidden">
<SprintBoardClient
key={sprint.id}
productId={id}
sprintId={sprint.id}
stories={sprintStoryItems}

View file

@ -51,6 +51,7 @@ export default async function MobileProductBacklogPage({ params, searchParams }:
priority: true,
status: true,
pbi_id: true,
sprint_id: true,
created_at: true,
},
}),

View file

@ -8,6 +8,7 @@ import { notFound } from 'next/navigation'
import { getAccessibleProduct } from '@/lib/product-access'
import { prisma } from '@/lib/prisma'
import { requireSession } from '@/lib/auth-guard'
import { resolveActiveSprint } from '@/lib/active-sprint'
import { SoloBoard } from '@/components/solo/solo-board'
import { NoActiveSprint } from '@/components/solo/no-active-sprint'
import type { SoloTask } from '@/components/solo/solo-board'
@ -24,9 +25,10 @@ export default async function MobileSoloProductPage({ params }: Props) {
const product = await getAccessibleProduct(id, session.userId)
if (!product) notFound()
const sprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'OPEN' },
})
const active = await resolveActiveSprint(id)
const sprint = active
? await prisma.sprint.findFirst({ where: { id: active.id, product_id: id } })
: null
if (!sprint) {
return (
@ -112,6 +114,7 @@ export default async function MobileSoloProductPage({ params }: Props) {
return (
<SoloBoard
key={sprint.id}
productId={id}
sprintGoal={sprint.sprint_goal}
tasks={tasks}