'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' async function getSession() { return getIronSession(await cookies(), sessionOptions) } async function verifyStoryOwnership(storyId: string, userId: string) { return prisma.story.findFirst({ where: { id: storyId, product: { user_id: userId } }, include: { product: true }, }) } 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 } // Verify ownership via product const pbi = await prisma.pbi.findFirst({ where: { id: parsed.data.pbiId, product: { user_id: 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: parsed.data.productId, title: parsed.data.title, priority: parsed.data.priority, sort_order, status: 'OPEN', }, }) revalidatePath(`/products/${parsed.data.productId}`) 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 verifyStoryOwnership(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 verifyStoryOwnership(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' } const product = await prisma.product.findFirst({ where: { id: productId, user_id: session.userId }, }) if (!product) return { error: 'Product niet gevonden' } 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: { user_id: session.userId } }, }) if (!pbi) return { error: 'PBI niet gevonden' } // Place at the end of the target priority group const last = await prisma.pbi.findFirst({ where: { product_id: productId, priority }, orderBy: { sort_order: 'desc' }, }) await prisma.pbi.update({ where: { id: pbiId }, data: { priority, sort_order: (last?.sort_order ?? 0) + 1.0 }, }) revalidatePath(`/products/${productId}`) 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: { user_id: 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' } const pbi = await prisma.pbi.findFirst({ where: { id: pbiId, product: { user_id: session.userId } }, }) if (!pbi) return { error: 'PBI niet gevonden' } await prisma.$transaction( orderedIds.map((id, i) => prisma.story.update({ where: { id }, data: { sort_order: i + 1.0, ...(newPriority !== undefined ? { priority: newPriority } : {}), }, }) ) ) revalidatePath(`/products/${productId}`) return { success: true } }