feat(PBI-79/ST-1339): createSprintWithSelectionAction + banner wire-up

actions/sprints.ts:
- Nieuwe createSprintWithSelectionAction(productId, metadata, pbiIntent,
  storyOverrides).
- Server-side intent-resolve:
  1. Voor elke PBI met intent='all': fetch child-story-IDs minus
     storyOverrides[pbi].remove.
  2. Plus storyOverrides[*].add (cross-PBI cherrypick toegestaan).
- Eligibility-filter via partitionByEligibility (sprint_id IS NULL + status
  != DONE; stories in andere OPEN sprint → conflicts.crossSprint).
- Transactie wrapt sprint.create + story.updateMany (status='IN_SPRINT') +
  task.updateMany (sprint_id cascade) — alles atomair.
- setActiveSprintInSettings na success.
- Return: { success, sprintId, affectedStoryIds, affectedPbiIds,
  affectedTaskIds, conflicts: { notEligible, crossSprint } } of error.

components/backlog/sprint-definition-banner.tsx:
- 'Sprint aanmaken'-knop sluit aan op createSprintWithSelectionAction;
  toast bij conflicts, success-toast anders, router.refresh() voor SSR
  cycle. Pending draft wordt door de action zelf nog niet expliciet gewist
  — dat gebeurt via revalidatePath en kan in ST-1340 finetuned worden.

Tests: __tests__/actions/create-sprint-with-selection.test.ts (6 cases)
dekken intent-resolve, override-respect, cross-sprint conflict, transactie-
binding van story.status + task.sprint_id, return-shape, en error-pad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-11 16:58:15 +02:00
parent 947d970231
commit d21011cdfa
3 changed files with 480 additions and 4 deletions

View file

@ -1,6 +1,7 @@
'use client'
import { useMemo, useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import {
@ -16,6 +17,7 @@ import {
import { useUserSettingsStore } from '@/stores/user-settings/store'
import { useProductWorkspaceStore } from '@/stores/product-workspace/store'
import type { PendingSprintDraft } from '@/lib/user-settings'
import { createSprintWithSelectionAction } from '@/actions/sprints'
import { debugProps } from '@/lib/debug'
interface SprintDefinitionBannerProps {
@ -74,6 +76,7 @@ export function SprintDefinitionBanner({
(s) => s.clearPendingSprintDraft,
)
const pbiSummary = useProductWorkspaceStore((s) => s.sprintMembership.pbiSummary)
const router = useRouter()
const [isPending, startTransition] = useTransition()
const [confirmCancel, setConfirmCancel] = useState(false)
@ -100,10 +103,33 @@ export function SprintDefinitionBanner({
}
function handleCreate() {
// PBI-79 ST-1339 wires de createSprintWithSelectionAction in.
toast.info(
'Sprint aanmaken is nog niet aangesloten (wordt afgerond in ST-1339).',
)
startTransition(async () => {
const result = await createSprintWithSelectionAction({
productId,
metadata: {
goal: draft.goal,
startAt: draft.startAt,
endAt: draft.endAt,
},
pbiIntent: draft.pbiIntent,
storyOverrides: draft.storyOverrides,
})
if ('error' in result) {
toast.error(result.error)
return
}
const { conflicts } = result
if (conflicts.notEligible.length > 0) {
toast.warning(
`${conflicts.notEligible.length} stor${
conflicts.notEligible.length === 1 ? 'y is' : 'ies zijn'
} overgeslagen (al in een andere sprint of afgerond).`,
)
} else {
toast.success('Sprint aangemaakt')
}
router.refresh()
})
}
const storyLabel = counts.hasUnknownTotal