feat: voeg geselecteerde PBI automatisch toe aan nieuwe sprint

Bij sprint-aanmaak wordt de pbi_id uit de selection-store als hidden
form-field meegestuurd. Server-side worden alle stories van die PBI
(zonder sprint) en hun taken aan de nieuwe sprint gekoppeld; stories
krijgen status IN_SPRINT met incrementele sort_order.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-08 02:09:55 +02:00
parent b55a929fd8
commit 1ecb2d6f4d
3 changed files with 38 additions and 0 deletions

View file

@ -40,6 +40,7 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
sprint_goal: formData.get('sprint_goal'),
start_date: formData.get('start_date'),
end_date: formData.get('end_date'),
pbi_id: formData.get('pbi_id'),
})
if (!parsed.success) {
return {
@ -72,6 +73,35 @@ export async function createSprintAction(_prevState: unknown, formData: FormData
}),
)
if (parsed.data.pbi_id) {
const pbi = await prisma.pbi.findFirst({
where: { id: parsed.data.pbi_id, product_id: parsed.data.productId },
select: { id: true },
})
if (pbi) {
const stories = await prisma.story.findMany({
where: { pbi_id: pbi.id, sprint_id: null },
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
select: { id: true },
})
if (stories.length > 0) {
const storyIds = stories.map(s => s.id)
await prisma.$transaction([
...stories.map((s, i) =>
prisma.story.update({
where: { id: s.id },
data: { sprint_id: sprint.id, status: 'IN_SPRINT', sort_order: i + 1 },
}),
),
prisma.task.updateMany({
where: { story_id: { in: storyIds }, sprint_id: null },
data: { sprint_id: sprint.id },
}),
])
}
}
}
await setActiveSprintCookie(parsed.data.productId, sprint.id)
revalidatePath(`/products/${parsed.data.productId}`)
return { success: true, sprintId: sprint.id }

View file

@ -21,6 +21,7 @@ import {
entityDialogHeaderClasses,
} from '@/components/shared/entity-dialog-layout'
import { createSprintAction } from '@/actions/sprints'
import { useSelectionStore } from '@/stores/selection-store'
interface StartSprintButtonProps {
productId: string
@ -44,6 +45,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
const [dirty, setDirty] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const router = useRouter()
const selectedPbiId = useSelectionStore((s) => s.selectedPbiId)
const [state, formAction, pending] = useActionState<ActionResult | undefined, FormData>(
async (_prev, fd) => {
@ -92,6 +94,7 @@ export function StartSprintButton({ productId, isDemo = false }: StartSprintButt
className="flex-1 overflow-y-auto px-6 py-6 space-y-6"
>
<input type="hidden" name="productId" value={productId} />
{selectedPbiId && <input type="hidden" name="pbi_id" value={selectedPbiId} />}
<div className="space-y-1.5">
<label className="text-sm font-medium text-foreground">

View file

@ -17,6 +17,11 @@ export const createSprintSchema = z
sprint_goal: z.string().min(1, 'Sprint Goal is verplicht').max(500),
start_date: dateField,
end_date: dateField,
pbi_id: z
.string()
.nullable()
.optional()
.transform(v => (v && v.trim() !== '' ? v : null)),
})
.superRefine(validateDateOrder)