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:
parent
b55a929fd8
commit
1ecb2d6f4d
3 changed files with 38 additions and 0 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue