Merge pull request #110 from madhura68/feat/story-i9ylvvhk
feat(PBI-34 ST-1212): backend — secondary_products in DTO + updateSecondaryProductsAction
This commit is contained in:
commit
f360c8fe81
5 changed files with 71 additions and 2 deletions
|
|
@ -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<ActionResult> {
|
|||
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<ActionResult> {
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type { Idea, IdeaStatus, Product } from '@prisma/client'
|
|||
type IdeaWithProduct = Idea & {
|
||||
product: Pick<Product, 'id' | 'name' | 'repo_url'> | 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue