'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 } from '@/lib/product-access' import { isValidCode, normalizeCode } from '@/lib/code' import { createWithCodeRetry, generateNextPbiCode } from '@/lib/code-server' import { pbiStatusFromApi } from '@/lib/task-status' import { createPbiSchema, updatePbiSchema } from '@/lib/schemas/pbi' import { enforceUserRateLimit } from '@/lib/rate-limit' async function getSession() { return getIronSession(await cookies(), sessionOptions) } type PbiFieldErrors = Record export async function createPbiAction(_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 limited = enforceUserRateLimit('create-pbi', session.userId) if (limited) return limited const parsed = createPbiSchema.safeParse({ productId: formData.get('productId'), code: (formData.get('code') as string) || undefined, title: formData.get('title'), description: formData.get('description') || undefined, priority: formData.get('priority'), status: (formData.get('status') as string) || undefined, }) if (!parsed.success) { return { error: 'Validatie mislukt', code: 422, fieldErrors: parsed.error.flatten().fieldErrors as PbiFieldErrors, } } const product = await getAccessibleProduct(parsed.data.productId, session.userId) if (!product) return { error: 'Product niet gevonden', code: 403 } const manualCode = normalizeCode(parsed.data.code) if (manualCode !== null && !isValidCode(manualCode)) { return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Alleen letters, cijfers, punten, koppeltekens of underscores'] } as PbiFieldErrors, } } if (manualCode) { const dup = await prisma.pbi.findFirst({ where: { product_id: parsed.data.productId, code: manualCode } }) if (dup) { return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Deze code is al in gebruik binnen dit product'] } as PbiFieldErrors, } } } const last = await prisma.pbi.findFirst({ where: { product_id: parsed.data.productId, priority: parsed.data.priority }, orderBy: { sort_order: 'desc' }, }) const sort_order = (last?.sort_order ?? 0) + 1.0 const status = parsed.data.status ? pbiStatusFromApi(parsed.data.status) ?? undefined : undefined const insert = (code: string) => prisma.pbi.create({ data: { product_id: parsed.data.productId, code, title: parsed.data.title, description: parsed.data.description ?? null, priority: parsed.data.priority, sort_order, ...(status ? { status } : {}), }, }) let pbi try { pbi = manualCode ? await insert(manualCode) : await createWithCodeRetry( () => generateNextPbiCode(parsed.data.productId), (code) => insert(code), ) } catch { return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Kon geen unieke code genereren — probeer opnieuw'] } as PbiFieldErrors, } } revalidatePath(`/products/${parsed.data.productId}`) return { success: true, pbi } } export async function updatePbiAction(_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 = updatePbiSchema.safeParse({ id: formData.get('id'), code: (formData.get('code') as string) || undefined, title: formData.get('title'), description: formData.get('description') || undefined, priority: formData.get('priority'), status: (formData.get('status') as string) || undefined, }) if (!parsed.success) { return { error: 'Validatie mislukt', code: 422, fieldErrors: parsed.error.flatten().fieldErrors as PbiFieldErrors, } } const pbi = await prisma.pbi.findFirst({ where: { id: parsed.data.id }, include: { product: true }, }) if (!pbi) return { error: 'PBI niet gevonden', code: 403 } const accessible = await getAccessibleProduct(pbi.product_id, session.userId) if (!accessible) return { error: 'PBI niet gevonden', code: 403 } const code = normalizeCode(parsed.data.code) if (code !== null && !isValidCode(code)) { return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Alleen letters, cijfers, punten, koppeltekens of underscores'] } as PbiFieldErrors, } } if (code) { const dup = await prisma.pbi.findFirst({ where: { product_id: pbi.product_id, code, NOT: { id: parsed.data.id } }, }) if (dup) { return { error: 'Validatie mislukt', code: 422, fieldErrors: { code: ['Deze code is al in gebruik binnen dit product'] } as PbiFieldErrors, } } } const status = parsed.data.status ? pbiStatusFromApi(parsed.data.status) ?? undefined : undefined await prisma.pbi.update({ where: { id: parsed.data.id }, data: { ...(code ? { code } : {}), title: parsed.data.title, description: parsed.data.description ?? null, priority: parsed.data.priority, ...(status ? { status } : {}), }, }) revalidatePath(`/products/${pbi.product_id}`) return { success: true } } export async function deletePbiAction(id: string) { 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 pbi = await prisma.pbi.findFirst({ where: { id }, include: { product: true }, }) if (!pbi) return { error: 'PBI niet gevonden', code: 403 } const accessible = await getAccessibleProduct(pbi.product_id, session.userId) if (!accessible) return { error: 'PBI niet gevonden', code: 403 } await prisma.pbi.delete({ where: { id } }) revalidatePath(`/products/${pbi.product_id}`) return { success: true } }