From f95754db1bcc6e039f249175c5f8f221a57aecd9 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Tue, 5 May 2026 15:00:24 +0200 Subject: [PATCH] feat(ST-abeu63oz): /admin/products/[id] product-detail met ledenbeheer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - page.tsx: product-header (naam/eigenaar/repo_url-link), leden-tabel, niet-leden dropdown - MemberActions: 'remove' → adminRemoveMemberAction, 'add' → select + adminAddMemberAction - ← Terug naar producten link --- .../[id]/_components/MemberActions.tsx | 54 +++++++++ app/(app)/admin/products/[id]/page.tsx | 104 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 app/(app)/admin/products/[id]/_components/MemberActions.tsx create mode 100644 app/(app)/admin/products/[id]/page.tsx diff --git a/app/(app)/admin/products/[id]/_components/MemberActions.tsx b/app/(app)/admin/products/[id]/_components/MemberActions.tsx new file mode 100644 index 0000000..deb3445 --- /dev/null +++ b/app/(app)/admin/products/[id]/_components/MemberActions.tsx @@ -0,0 +1,54 @@ +'use client' + +import { useRef, useTransition } from 'react' +import { Button } from '@/components/ui/button' +import { adminAddMemberAction, adminRemoveMemberAction } from '@/actions/admin/products' + +type NonMember = { id: string; username: string } + +type Props = + | { productId: string; userId: string; action: 'remove'; label: string; nonMembers?: never } + | { productId: string; userId?: never; action: 'add'; label: string; nonMembers: NonMember[] } + +export function MemberActions({ productId, userId, action, label, nonMembers }: Props) { + const [pending, startTransition] = useTransition() + const selectRef = useRef(null) + + function handleRemove() { + startTransition(async () => { + await adminRemoveMemberAction(productId, userId!) + }) + } + + function handleAdd() { + const selectedId = selectRef.current?.value + if (!selectedId) return + startTransition(async () => { + await adminAddMemberAction(productId, selectedId) + }) + } + + if (action === 'remove') { + return ( + + ) + } + + return ( +
+ + +
+ ) +} diff --git a/app/(app)/admin/products/[id]/page.tsx b/app/(app)/admin/products/[id]/page.tsx new file mode 100644 index 0000000..30c6268 --- /dev/null +++ b/app/(app)/admin/products/[id]/page.tsx @@ -0,0 +1,104 @@ +import Link from 'next/link' +import { notFound } from 'next/navigation' +import { requireAdmin } from '@/lib/auth-guard' +import { prisma } from '@/lib/prisma' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { MemberActions } from './_components/MemberActions' + +export default async function AdminProductDetailPage({ + params, +}: { + params: Promise<{ id: string }> +}) { + await requireAdmin() + const { id } = await params + + const product = await prisma.product.findUnique({ + where: { id }, + include: { + user: { select: { username: true } }, + members: { + include: { user: { select: { id: true, username: true, email: true } } }, + orderBy: { created_at: 'asc' }, + }, + }, + }) + + if (!product) notFound() + + const nonMembers = await prisma.user.findMany({ + where: { NOT: { product_members: { some: { product_id: id } } } }, + select: { id: true, username: true }, + orderBy: { username: 'asc' }, + }) + + return ( +
+
+ + ← Terug naar producten + +
+ +
+

{product.name}

+

Eigenaar: {product.user.username}

+ {product.repo_url && ( + + {product.repo_url} + + )} +
+ +
+

Leden ({product.members.length})

+ + + + Gebruiker + Email + Acties + + + + {product.members.map(m => ( + + {m.user.username} + {m.user.email ?? '—'} + + + + + ))} + {product.members.length === 0 && ( + + + Geen leden. + + + )} + +
+ + {nonMembers.length > 0 && ( +
+ Lid toevoegen: + +
+ )} +
+
+ ) +}