From 222c702fda5bc1280d561e7898078df31ba1e907 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Tue, 5 May 2026 14:57:29 +0200 Subject: [PATCH] feat(ST-abeu63oz): /admin/products pagina met CRUD-dialogen en ledenbeheer-link - app/(app)/admin/_components/ProductFormDialog.tsx: gedeeld create/edit-formulier (naam, description, repo_url, definition_of_done, auto_pr, eigenaar-dropdown bij create) - app/(app)/admin/products/page.tsx: server component, query products+user+_count, tabel met naam-link naar /admin/products/[id], eigenaar, leden/PBI-count, archived-badge - app/(app)/admin/products/_components/ProductActions.tsx: client Bewerken/Archiveer/Verwijder --- .../admin/_components/ProductFormDialog.tsx | 122 ++++++++++++++++++ .../products/_components/ProductActions.tsx | 76 +++++++++++ app/(app)/admin/products/page.tsx | 93 +++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 app/(app)/admin/_components/ProductFormDialog.tsx create mode 100644 app/(app)/admin/products/_components/ProductActions.tsx create mode 100644 app/(app)/admin/products/page.tsx diff --git a/app/(app)/admin/_components/ProductFormDialog.tsx b/app/(app)/admin/_components/ProductFormDialog.tsx new file mode 100644 index 0000000..35690aa --- /dev/null +++ b/app/(app)/admin/_components/ProductFormDialog.tsx @@ -0,0 +1,122 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from '@/components/ui/dialog' +import { adminCreateProductAction, adminUpdateProductAction } from '@/actions/admin/products' + +type User = { id: string; username: string } + +type ProductData = { + id: string + name: string + description: string | null + repo_url: string | null + definition_of_done: string + auto_pr: boolean + user_id: string +} + +interface Props { + mode: 'create' | 'edit' + product?: ProductData + allUsers: User[] + trigger: React.ReactNode +} + +export function ProductFormDialog({ mode, product, allUsers, trigger }: Props) { + const [open, setOpen] = useState(false) + const [pending, startTransition] = useTransition() + const [error, setError] = useState(null) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + const fd = new FormData(e.currentTarget) + const data = { + name: fd.get('name') as string, + description: fd.get('description') as string || undefined, + repo_url: fd.get('repo_url') as string || undefined, + definition_of_done: fd.get('definition_of_done') as string, + auto_pr: fd.get('auto_pr') === 'on', + owner_user_id: fd.get('owner_user_id') as string, + } + + setError(null) + startTransition(async () => { + try { + if (mode === 'create') { + await adminCreateProductAction(data) + } else { + await adminUpdateProductAction(product!.id, data) + } + setOpen(false) + } catch (err) { + setError(err instanceof Error ? err.message : 'Onbekende fout') + } + }) + } + + return ( + + }>{trigger} + + + {mode === 'create' ? 'Nieuw product' : 'Product bewerken'} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {mode === 'create' && ( +
+ + +
+ )} + {mode === 'edit' && ( + + )} +
+ + +
+ {error && ( +
{error}
+ )} + + }>Annuleer + + +
+
+
+ ) +} diff --git a/app/(app)/admin/products/_components/ProductActions.tsx b/app/(app)/admin/products/_components/ProductActions.tsx new file mode 100644 index 0000000..2a5e87b --- /dev/null +++ b/app/(app)/admin/products/_components/ProductActions.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useTransition, useState } from 'react' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from '@/components/ui/dialog' +import { adminArchiveProductAction, adminDeleteProductAction } from '@/actions/admin/products' +import { ProductFormDialog } from '../../_components/ProductFormDialog' + +type User = { id: string; username: string } +type Product = { + id: string + name: string + description: string | null + repo_url: string | null + definition_of_done: string + auto_pr: boolean + archived: boolean + user_id: string +} + +export function ProductActions({ product, allUsers }: { product: Product; allUsers: User[] }) { + const [deleteOpen, setDeleteOpen] = useState(false) + const [pending, startTransition] = useTransition() + + function handleArchive() { + startTransition(async () => { + await adminArchiveProductAction(product.id, !product.archived) + }) + } + + function handleDelete() { + startTransition(async () => { + await adminDeleteProductAction(product.id) + setDeleteOpen(false) + }) + } + + return ( +
+ Bewerken} + /> + + + }>Verwijder + + + Product verwijderen + +

+ Weet je zeker dat je {product.name} wilt verwijderen? Alle PBIs, stories en taken worden ook verwijderd. +

+ + }>Annuleer + + +
+
+
+ ) +} diff --git a/app/(app)/admin/products/page.tsx b/app/(app)/admin/products/page.tsx new file mode 100644 index 0000000..1e63208 --- /dev/null +++ b/app/(app)/admin/products/page.tsx @@ -0,0 +1,93 @@ +import Link from 'next/link' +import { requireAdmin } from '@/lib/auth-guard' +import { prisma } from '@/lib/prisma' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { ProductFormDialog } from '../_components/ProductFormDialog' +import { ProductActions } from './_components/ProductActions' + +export default async function AdminProductsPage() { + await requireAdmin() + + const [products, allUsers] = await Promise.all([ + prisma.product.findMany({ + include: { + user: { select: { username: true } }, + _count: { select: { members: true, pbis: true } }, + }, + orderBy: { created_at: 'desc' }, + }), + prisma.user.findMany({ + select: { id: true, username: true }, + orderBy: { username: 'asc' }, + }), + ]) + + return ( +
+
+

Producten

+ Nieuw product} + /> +
+ + + + Naam + Eigenaar + Leden + PBIs + Status + Aangemaakt + Acties + + + + {products.map(p => ( + + + + {p.name} + + + {p.user.username} + {p._count.members} + {p._count.pbis} + + {p.archived ? ( + Gearchiveerd + ) : ( + Actief + )} + + + {new Date(p.created_at).toLocaleDateString('nl-NL')} + + + + + + ))} + {products.length === 0 && ( + + + Geen producten gevonden. + + + )} + +
+
+ ) +}