diff --git a/actions/ideas.ts b/actions/ideas.ts index 1ae5e47..dbfa806 100644 --- a/actions/ideas.ts +++ b/actions/ideas.ts @@ -10,6 +10,8 @@ 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 { enforceUserRateLimit } from '@/lib/rate-limit' @@ -165,6 +167,63 @@ export async function deleteIdeaAction(id: string): Promise { return { success: true } } +// --------------------------------------------------------------------------- +// Secondary products + +const secondaryProductsSchema = z.object({ + ideaId: z.string().cuid(), + productIds: z.array(z.string().cuid()).max(10), +}) + +export async function updateSecondaryProductsAction( + ideaId: string, + productIds: string[], +): Promise { + const session = await getSession() + if (!session.userId) return { error: 'Niet ingelogd', code: 401 } + if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus', code: 403 } + + const parsed = secondaryProductsSchema.safeParse({ ideaId, productIds }) + if (!parsed.success) return { error: 'Ongeldige invoer', code: 422 } + + const idea = await prisma.idea.findFirst({ + where: { id: parsed.data.ideaId, user_id: session.userId }, + select: { id: true, product_id: true }, + }) + if (!idea) return { error: 'Idee niet gevonden', code: 404 } + + // Verwijder primair product uit de lijst (mag niet dubbel) + const filtered = parsed.data.productIds.filter((pid) => pid !== idea.product_id) + + // Valideer dat alle gevraagde producten toegankelijk zijn voor de user + if (filtered.length > 0) { + const { productAccessFilter } = await import('@/lib/product-access') + const accessible = await prisma.product.findMany({ + where: { id: { in: filtered }, ...productAccessFilter(session.userId) }, + select: { id: true }, + }) + if (accessible.length !== filtered.length) + return { error: 'Een of meer producten zijn niet toegankelijk', code: 403 } + } + + // Atomisch: verwijder alle bestaande, voeg nieuwe in + await prisma.$transaction([ + prisma.ideaProduct.deleteMany({ where: { idea_id: idea.id } }), + ...(filtered.length > 0 + ? [ + prisma.ideaProduct.createMany({ + data: filtered.map((pid) => ({ idea_id: idea.id, product_id: pid })), + skipDuplicates: true, + }), + ] + : []), + ]) + + revalidatePath('/ideas/' + idea.id, 'page') + revalidatePath('/ideas', 'page') + return { success: true } +} + // --------------------------------------------------------------------------- // Markdown-edits (grill_md & plan_md handmatig fine-tunen) diff --git a/app/(app)/ideas/[id]/page.tsx b/app/(app)/ideas/[id]/page.tsx index 10439f2..d548a81 100644 --- a/app/(app)/ideas/[id]/page.tsx +++ b/app/(app)/ideas/[id]/page.tsx @@ -29,6 +29,7 @@ export default async function IdeaDetailPage({ params, searchParams }: PageProps include: { product: { select: { id: true, name: true, repo_url: true } }, pbi: { select: { id: true, code: true, title: true } }, + secondary_products: { include: { product: { select: { id: true, name: true } } } }, }, }) if (!idea) notFound() diff --git a/app/(app)/ideas/page.tsx b/app/(app)/ideas/page.tsx index 142e376..1b2c45d 100644 --- a/app/(app)/ideas/page.tsx +++ b/app/(app)/ideas/page.tsx @@ -16,7 +16,10 @@ export default async function IdeasPage() { const ideas = await prisma.idea.findMany({ where: { user_id: session.userId, archived: false }, orderBy: { created_at: 'desc' }, - include: { product: { select: { id: true, name: true, repo_url: true } } }, + include: { + product: { select: { id: true, name: true, repo_url: true } }, + secondary_products: { include: { product: { select: { id: true, name: true } } } }, + }, take: 200, }) diff --git a/app/api/ideas/route.ts b/app/api/ideas/route.ts index 84d1ad7..7da26ac 100644 --- a/app/api/ideas/route.ts +++ b/app/api/ideas/route.ts @@ -32,7 +32,10 @@ export async function GET(request: Request) { ...(productIdParam ? { product_id: productIdParam } : {}), ...(status ? { status } : {}), }, - include: { product: { select: { id: true, name: true, repo_url: true } } }, + include: { + product: { select: { id: true, name: true, repo_url: true } }, + secondary_products: { include: { product: { select: { id: true, name: true } } } }, + }, orderBy: { created_at: 'desc' }, take: 200, }) diff --git a/lib/idea-dto.ts b/lib/idea-dto.ts index b32d14a..3b6557c 100644 --- a/lib/idea-dto.ts +++ b/lib/idea-dto.ts @@ -9,6 +9,7 @@ import type { Idea, IdeaStatus, Product } from '@prisma/client' type IdeaWithProduct = Idea & { product: Pick | null pbi?: { id: string; code: string; title: string } | null + secondary_products?: { id: string; product_id: string; product: { id: string; name: string } }[] } export interface IdeaDto { @@ -21,6 +22,7 @@ export interface IdeaDto { product: { id: string; name: string; repo_url: string | null } | null pbi_id: string | null pbi?: { id: string; code: string; title: string } | null + secondary_products: { id: string; product_id: string; product: { id: string; name: string } }[] archived: boolean has_grill_md: boolean has_plan_md: boolean @@ -39,6 +41,7 @@ export function ideaToDto(idea: IdeaWithProduct & { status: IdeaStatus }): IdeaD product: idea.product, pbi_id: idea.pbi_id, pbi: idea.pbi ?? null, + secondary_products: idea.secondary_products ?? [], archived: idea.archived, // Geen md-content in lijst-payloads (kan groot zijn) — enkel een vlag. has_grill_md: idea.grill_md !== null,