From 4a929b19622815c7d1b97853679fcab12caf6f68 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 6 May 2026 02:20:34 +0200 Subject: [PATCH 1/2] feat(ideas): secondary_products meeladen in IdeaDto en alle queries Voegt IdeaProduct schema toe (dependency van story-qtkvz6ly), breidt IdeaWithProduct type en IdeaDto interface uit met secondary_products array, en laadt de relatie mee in findMany/findFirst in page.tsx en REST GET. Co-Authored-By: Claude Sonnet 4.6 --- app/(app)/ideas/[id]/page.tsx | 1 + app/(app)/ideas/page.tsx | 5 ++++- app/api/ideas/route.ts | 5 ++++- lib/idea-dto.ts | 3 +++ prisma/schema.prisma | 24 ++++++++++++++++++++---- 5 files changed, 32 insertions(+), 6 deletions(-) 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, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 87f12af..352e5c2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -188,6 +188,7 @@ model Product { claude_questions ClaudeQuestion[] claude_jobs ClaudeJob[] ideas Idea[] + idea_products IdeaProduct[] @@unique([user_id, name]) @@unique([user_id, code]) @@ -416,10 +417,11 @@ model Idea { created_at DateTime @default(now()) updated_at DateTime @updatedAt - questions ClaudeQuestion[] - jobs ClaudeJob[] - logs IdeaLog[] - user_questions UserQuestion[] + questions ClaudeQuestion[] + jobs ClaudeJob[] + logs IdeaLog[] + user_questions UserQuestion[] + secondary_products IdeaProduct[] @@unique([user_id, code]) @@index([user_id, archived, status]) @@ -427,6 +429,20 @@ model Idea { @@map("ideas") } +model IdeaProduct { + id String @id @default(cuid()) + idea_id String + product_id String + created_at DateTime @default(now()) + + idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade) + product Product @relation(fields: [product_id], references: [id], onDelete: Cascade) + + @@unique([idea_id, product_id]) + @@index([product_id]) + @@map("idea_products") +} + model IdeaLog { id String @id @default(cuid()) idea Idea @relation(fields: [idea_id], references: [id], onDelete: Cascade) From a5afb8c5fda0a202bbb0fbb97c64fc66c5261d3c Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Wed, 6 May 2026 02:23:00 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(ideas):=20updateSecondaryProductsActio?= =?UTF-8?q?n=20=E2=80=94=20atomisch=20vervangen=20secundaire=20producten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voegt server action toe die secondary_products voor een idee atomisch vervangt: primair product gefilterd, toegankelijkheid gevalideerd via productAccessFilter, deleteMany + createMany in één transactie. Demo-geblokkeerd, Zod-gevalideerd. Co-Authored-By: Claude Sonnet 4.6 --- actions/ideas.ts | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) 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)