'use server' import { revalidatePath } from 'next/cache' import { cookies } from 'next/headers' import { getIronSession } from 'iron-session' import { z } from 'zod' import { prisma } from '@/lib/prisma' import { SessionData, sessionOptions } from '@/lib/session' import { getAccessibleProduct, productAccessFilter } from '@/lib/product-access' async function getSession() { return getIronSession(await cookies(), sessionOptions) } async function verifyStoryAccess(storyId: string, userId: string) { return prisma.story.findFirst({ where: { id: storyId, product: productAccessFilter(userId) }, include: { product: true }, }) } function hasDuplicateIds(ids: string[]) { return new Set(ids).size !== ids.length } const createStorySchema = z.object({ pbiId: z.string(), productId: z.string(), title: z.string().min(1, 'Titel is verplicht').max(200), priority: z.coerce.number().int().min(1).max(4), }) const updateStorySchema = z.object({ id: z.string(), title: z.string().min(1, 'Titel is verplicht').max(200), description: z.string().max(2000).optional(), acceptance_criteria: z.string().max(2000).optional(), priority: z.coerce.number().int().min(1).max(4), }) export async function createStoryAction(_prevState: unknown, formData: FormData) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const parsed = createStorySchema.safeParse({ pbiId: formData.get('pbiId'), productId: formData.get('productId'), title: formData.get('title'), priority: formData.get('priority') ?? 2, }) if (!parsed.success) return { error: parsed.error.flatten().fieldErrors } const pbi = await prisma.pbi.findFirst({ where: { id: parsed.data.pbiId, product: productAccessFilter(session.userId) }, }) if (!pbi) return { error: 'PBI niet gevonden' } const last = await prisma.story.findFirst({ where: { pbi_id: parsed.data.pbiId, priority: parsed.data.priority }, orderBy: { sort_order: 'desc' }, }) const sort_order = (last?.sort_order ?? 0) + 1.0 const story = await prisma.story.create({ data: { pbi_id: parsed.data.pbiId, product_id: pbi.product_id, title: parsed.data.title, priority: parsed.data.priority, sort_order, status: 'OPEN', }, }) revalidatePath(`/products/${pbi.product_id}`) return { success: true, story } } export async function updateStoryAction(_prevState: unknown, formData: FormData) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const parsed = updateStorySchema.safeParse({ id: formData.get('id'), title: formData.get('title'), description: formData.get('description') || undefined, acceptance_criteria: formData.get('acceptance_criteria') || undefined, priority: formData.get('priority'), }) if (!parsed.success) return { error: parsed.error.flatten().fieldErrors } const story = await verifyStoryAccess(parsed.data.id, session.userId) if (!story) return { error: 'Story niet gevonden' } await prisma.story.update({ where: { id: parsed.data.id }, data: { title: parsed.data.title, description: parsed.data.description ?? null, acceptance_criteria: parsed.data.acceptance_criteria ?? null, priority: parsed.data.priority, }, }) revalidatePath(`/products/${story.product_id}`) return { success: true } } export async function deleteStoryAction(id: 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 verifyStoryAccess(id, session.userId) if (!story) return { error: 'Story niet gevonden' } await prisma.story.delete({ where: { id } }) revalidatePath(`/products/${story.product_id}`) return { success: true } } export async function reorderPbisAction(productId: 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 PBI-volgorde' } const product = await getAccessibleProduct(productId, session.userId) if (!product) return { error: 'Product niet gevonden' } const pbis = await prisma.pbi.findMany({ where: { id: { in: orderedIds }, product_id: productId }, select: { id: true }, }) if (pbis.length !== orderedIds.length) return { error: 'Ongeldige PBI-volgorde' } await prisma.$transaction( orderedIds.map((id, i) => prisma.pbi.update({ where: { id }, data: { sort_order: i + 1.0 } }) ) ) revalidatePath(`/products/${productId}`) return { success: true } } export async function updatePbiPriorityAction(pbiId: string, priority: number, _productId: string) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } const pbi = await prisma.pbi.findFirst({ where: { id: pbiId, product: productAccessFilter(session.userId) }, }) if (!pbi) return { error: 'PBI niet gevonden' } if (!Number.isInteger(priority) || priority < 1 || priority > 4) return { error: 'Ongeldige prioriteit' } const last = await prisma.pbi.findFirst({ where: { product_id: pbi.product_id, priority }, orderBy: { sort_order: 'desc' }, }) await prisma.pbi.update({ where: { id: pbiId }, data: { priority, sort_order: (last?.sort_order ?? 0) + 1.0 }, }) revalidatePath(`/products/${pbi.product_id}`) return { success: true } } export async function getStoryLogsAction(storyId: string) { const session = await getSession() if (!session.userId) return { error: 'Niet ingelogd' } const story = await prisma.story.findFirst({ where: { id: storyId, product: productAccessFilter(session.userId) }, include: { product: { select: { repo_url: true } } }, }) if (!story) return { error: 'Story niet gevonden' } const logs = await prisma.storyLog.findMany({ where: { story_id: storyId }, orderBy: { created_at: 'asc' }, }) return { success: true, logs: logs.map((l: (typeof logs)[number]) => ({ id: l.id, type: l.type, content: l.content, status: l.status, commit_hash: l.commit_hash, commit_message: l.commit_message, created_at: l.created_at.toISOString(), })), repoUrl: story.product.repo_url, } } export async function reorderStoriesAction( pbiId: string, productId: string, orderedIds: string[], newPriority?: number ) { 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 story-volgorde' } if (newPriority !== undefined && (!Number.isInteger(newPriority) || newPriority < 1 || newPriority > 4)) { return { error: 'Ongeldige prioriteit' } } const pbi = await prisma.pbi.findFirst({ where: { id: pbiId, product: productAccessFilter(session.userId) }, }) if (!pbi) return { error: 'PBI niet gevonden' } const stories = await prisma.story.findMany({ where: { id: { in: orderedIds }, pbi_id: pbiId, product_id: pbi.product_id }, select: { id: true }, }) if (stories.length !== orderedIds.length) return { error: 'Ongeldige story-volgorde' } await prisma.$transaction( orderedIds.map((id, i) => prisma.story.update({ where: { id }, data: { sort_order: i + 1.0, ...(newPriority !== undefined ? { priority: newPriority } : {}), }, }) ) ) revalidatePath(`/products/${pbi.product_id}`) return { success: true } }