'use server' import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' import { getIronSession } from 'iron-session' import { prisma } from '@/lib/prisma' import { SessionData, sessionOptions } from '@/lib/session' import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access' import { createSprintSchema, updateSprintDatesSchema, updateSprintGoalSchema, } from '@/lib/schemas/sprint' async function getSession() { return getIronSession(await cookies(), sessionOptions) } function hasDuplicateIds(ids: string[]) { return new Set(ids).size !== ids.length } type SprintFieldErrors = Record export async function createSprintAction(_prevState: unknown, formData: FormData) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd', code: 403 } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 } const parsed = createSprintSchema.safeParse({ productId: formData.get('productId'), sprint_goal: formData.get('sprint_goal'), start_date: formData.get('start_date'), end_date: formData.get('end_date'), }) if (!parsed.success) { return { error: 'Validatie mislukt', code: 422, fieldErrors: parsed.error.flatten().fieldErrors as SprintFieldErrors, } } const product = await getAccessibleProduct(parsed.data.productId, session.userId) if (!product) return { error: 'Product niet gevonden', code: 403 } const existing = await prisma.sprint.findFirst({ where: { product_id: parsed.data.productId, status: 'ACTIVE' }, }) if (existing) return { error: 'Er is al een actieve Sprint voor dit product', sprintId: existing.id, code: 422 } const sprint = await prisma.sprint.create({ data: { product_id: parsed.data.productId, sprint_goal: parsed.data.sprint_goal, status: 'ACTIVE', start_date: parsed.data.start_date, end_date: parsed.data.end_date, }, }) revalidatePath(`/products/${parsed.data.productId}`) return { success: true, sprintId: sprint.id } } export async function updateSprintDatesAction(_prevState: unknown, formData: FormData) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd', code: 403 } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 } const parsed = updateSprintDatesSchema.safeParse({ id: formData.get('id'), start_date: formData.get('start_date'), end_date: formData.get('end_date'), }) if (!parsed.success) { return { error: 'Validatie mislukt', code: 422, fieldErrors: parsed.error.flatten().fieldErrors as SprintFieldErrors, } } const sprint = await prisma.sprint.findFirst({ where: { id: parsed.data.id, product: productAccessFilter(session.userId) }, }) if (!sprint) return { error: 'Sprint niet gevonden', code: 403 } await prisma.sprint.update({ where: { id: parsed.data.id }, data: { start_date: parsed.data.start_date, end_date: parsed.data.end_date }, }) revalidatePath(`/products/${sprint.product_id}/sprint`) return { success: true } } export async function updateSprintGoalAction(_prevState: unknown, formData: FormData) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd', code: 403 } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 } const parsed = updateSprintGoalSchema.safeParse({ id: formData.get('id'), sprint_goal: formData.get('sprint_goal'), }) if (!parsed.success) { return { error: 'Validatie mislukt', code: 422, fieldErrors: parsed.error.flatten().fieldErrors as SprintFieldErrors, } } const sprint = await prisma.sprint.findFirst({ where: { id: parsed.data.id, product: productAccessFilter(session.userId) }, }) if (!sprint) return { error: 'Sprint niet gevonden', code: 403 } await prisma.sprint.update({ where: { id: parsed.data.id }, data: { sprint_goal: parsed.data.sprint_goal } }) revalidatePath(`/products/${sprint.product_id}/sprint`) return { success: true } } export async function addStoryToSprintAction(sprintId: string, storyId: string) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const sprint = await prisma.sprint.findFirst({ where: { id: sprintId, product: productAccessFilter(session.userId) }, }) if (!sprint) return { error: 'Sprint niet gevonden' } const story = await prisma.story.findFirst({ where: { id: storyId, product: productAccessFilter(session.userId) }, }) if (!story) return { error: 'Story niet gevonden' } if (story.product_id !== sprint.product_id) return { error: 'Story hoort niet bij deze Sprint' } const last = await prisma.story.findFirst({ where: { sprint_id: sprintId }, orderBy: { sort_order: 'desc' }, }) await prisma.story.update({ where: { id: storyId }, data: { sprint_id: sprintId, status: 'IN_SPRINT', sort_order: (last?.sort_order ?? 0) + 1.0 }, }) revalidatePath(`/products/${sprint.product_id}/sprint`) revalidatePath(`/products/${sprint.product_id}`) return { success: true } } export async function removeStoryFromSprintAction(storyId: string) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const story = await prisma.story.findFirst({ where: { id: storyId, product: productAccessFilter(session.userId) }, include: { sprint: true }, }) if (!story) return { error: 'Story niet gevonden' } await prisma.story.update({ where: { id: storyId }, data: { sprint_id: null, status: 'OPEN' }, }) revalidatePath(`/products/${story.product_id}/sprint`) revalidatePath(`/products/${story.product_id}`) return { success: true } } export async function reorderSprintStoriesAction(sprintId: string, orderedIds: string[]) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } if (hasDuplicateIds(orderedIds)) return { error: 'Ongeldige Sprint Backlog-volgorde' } const sprint = await prisma.sprint.findFirst({ where: { id: sprintId, product: productAccessFilter(session.userId) }, }) if (!sprint) return { error: 'Sprint niet gevonden' } const stories = await prisma.story.findMany({ where: { id: { in: orderedIds }, sprint_id: sprintId, product_id: sprint.product_id }, select: { id: true }, }) if (stories.length !== orderedIds.length) return { error: 'Ongeldige Sprint Backlog-volgorde' } await prisma.$transaction( orderedIds.map((id, i) => prisma.story.update({ where: { id }, data: { sort_order: i + 1.0 } }) ) ) revalidatePath(`/products/${sprint.product_id}/sprint`) return { success: true } } export async function completeSprintAction( sprintId: string, decisions: Record ) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const sprint = await prisma.sprint.findFirst({ where: { id: sprintId, product: productAccessFilter(session.userId) }, }) if (!sprint) return { error: 'Sprint niet gevonden' } const entries = Object.entries(decisions) if (entries.some(([, status]) => status !== 'DONE' && status !== 'OPEN')) { return { error: 'Ongeldige Sprint-afronding' } } const storyIds = entries.map(([storyId]) => storyId) if (hasDuplicateIds(storyIds)) return { error: 'Ongeldige Sprint-afronding' } const stories = await prisma.story.findMany({ where: { id: { in: storyIds }, sprint_id: sprintId, product_id: sprint.product_id }, select: { id: true, pbi_id: true }, }) if (stories.length !== storyIds.length) return { error: 'Ongeldige Sprint-afronding' } const affectedPbiIds = [...new Set(stories.map((s) => s.pbi_id))] const candidatePbis = await prisma.pbi.findMany({ where: { id: { in: affectedPbiIds }, status: { not: 'DONE' } }, select: { id: true, stories: { select: { id: true, status: true } } }, }) const decisionByStoryId = new Map(entries) const pbiIdsToMarkDone = candidatePbis .filter( (pbi) => pbi.stories.length > 0 && pbi.stories.every((s) => { const next = decisionByStoryId.get(s.id) ?? s.status return next === 'DONE' }) ) .map((p) => p.id) await prisma.$transaction([ ...entries.map(([storyId, status]) => prisma.story.update({ where: { id: storyId }, data: { status, sprint_id: status === 'OPEN' ? null : undefined, }, }) ), ...pbiIdsToMarkDone.map((id) => prisma.pbi.update({ where: { id }, data: { status: 'DONE' } }) ), prisma.sprint.update({ where: { id: sprintId }, data: { status: 'COMPLETED', completed_at: new Date() }, }), ]) revalidatePath(`/products/${sprint.product_id}`) revalidatePath(`/products/${sprint.product_id}/sprint`) return { success: true } }