From 31edfa8194c6690bcc83f640f6070db91ed845e2 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Tue, 5 May 2026 14:44:04 +0200 Subject: [PATCH] feat(ST-111ci8t4): /admin/users pagina met tabel, role-editor en delete-dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/(app)/admin/layout.tsx: admin-sidebar met links (Gebruikers/Claude Jobs/Producten) - app/(app)/admin/page.tsx: redirect naar /admin/users - app/(app)/admin/users/page.tsx: server component, query users+roles, geeft userId door - components/admin/users-table.tsx: client component met UsersTable, RoleBadge, RolesDialog (checkboxes, eigen ADMIN-rol geblokkeerd), DeleteDialog (confirm), ResetToggle — alles via useTransition + server actions --- app/(app)/admin/layout.tsx | 16 +++ app/(app)/admin/page.tsx | 5 + app/(app)/admin/users/page.tsx | 19 +++ components/admin/users-table.tsx | 239 +++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 app/(app)/admin/layout.tsx create mode 100644 app/(app)/admin/page.tsx create mode 100644 app/(app)/admin/users/page.tsx create mode 100644 components/admin/users-table.tsx diff --git a/app/(app)/admin/layout.tsx b/app/(app)/admin/layout.tsx new file mode 100644 index 0000000..6c2c912 --- /dev/null +++ b/app/(app)/admin/layout.tsx @@ -0,0 +1,16 @@ +import { requireAdmin } from '@/lib/auth-guard' +import Link from 'next/link' + +export default async function AdminLayout({ children }: { children: React.ReactNode }) { + await requireAdmin() + return ( +
+ +
{children}
+
+ ) +} diff --git a/app/(app)/admin/page.tsx b/app/(app)/admin/page.tsx new file mode 100644 index 0000000..f07ba33 --- /dev/null +++ b/app/(app)/admin/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation' + +export default function AdminPage() { + redirect('/admin/users') +} diff --git a/app/(app)/admin/users/page.tsx b/app/(app)/admin/users/page.tsx new file mode 100644 index 0000000..6d3543d --- /dev/null +++ b/app/(app)/admin/users/page.tsx @@ -0,0 +1,19 @@ +import { requireAdmin } from '@/lib/auth-guard' +import { prisma } from '@/lib/prisma' +import { UsersTable } from '@/components/admin/users-table' + +export default async function AdminUsersPage() { + const session = await requireAdmin() + + const users = await prisma.user.findMany({ + include: { roles: { select: { role: true } } }, + orderBy: { created_at: 'desc' }, + }) + + return ( +
+

Gebruikers

+ +
+ ) +} diff --git a/components/admin/users-table.tsx b/components/admin/users-table.tsx new file mode 100644 index 0000000..172cd41 --- /dev/null +++ b/components/admin/users-table.tsx @@ -0,0 +1,239 @@ +'use client' + +import { useState, useTransition } from 'react' +import { Role } from '@prisma/client' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, + DialogClose, +} from '@/components/ui/dialog' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { + deleteUserAction, + updateUserRolesAction, + setMustResetPasswordAction, +} from '@/actions/admin/users' + +type UserWithRoles = { + id: string + username: string + email: string | null + must_reset_password: boolean + created_at: Date + roles: { role: Role }[] +} + +const ALL_ROLES: Role[] = [Role.PRODUCT_OWNER, Role.SCRUM_MASTER, Role.DEVELOPER, Role.ADMIN] + +const ROLE_LABEL: Record = { + PRODUCT_OWNER: 'Product Owner', + SCRUM_MASTER: 'Scrum Master', + DEVELOPER: 'Developer', + ADMIN: 'Admin', +} + +function RoleBadge({ role }: { role: Role }) { + const cls = + role === Role.ADMIN + ? 'bg-status-done text-white border-transparent' + : role === Role.PRODUCT_OWNER + ? 'bg-status-in-progress text-white border-transparent' + : role === Role.SCRUM_MASTER + ? 'bg-priority-medium text-white border-transparent' + : 'bg-secondary text-secondary-foreground' + return {ROLE_LABEL[role]} +} + +function RolesDialog({ user, currentUserId }: { user: UserWithRoles; currentUserId: string }) { + const [open, setOpen] = useState(false) + const [selected, setSelected] = useState>(new Set(user.roles.map(r => r.role))) + const [pending, startTransition] = useTransition() + const isSelf = user.id === currentUserId + + function toggle(role: Role) { + setSelected(prev => { + const next = new Set(prev) + if (next.has(role)) next.delete(role) + else next.add(role) + return next + }) + } + + function handleSave() { + startTransition(async () => { + await updateUserRolesAction(user.id, Array.from(selected)) + setOpen(false) + }) + } + + return ( + + }> + Rollen + + + + Rollen voor {user.username} + +
+ {ALL_ROLES.map(role => { + const isDisabled = isSelf && role === Role.ADMIN && selected.has(role) + return ( + + ) + })} +
+ + + +
+
+ ) +} + +function DeleteDialog({ user, currentUserId }: { user: UserWithRoles; currentUserId: string }) { + const [open, setOpen] = useState(false) + const [pending, startTransition] = useTransition() + const isSelf = user.id === currentUserId + + function handleDelete() { + startTransition(async () => { + await deleteUserAction(user.id) + setOpen(false) + }) + } + + return ( + + + } + > + Verwijder + + + + Gebruiker verwijderen + +

+ Weet je zeker dat je {user.username} wilt verwijderen? Dit kan niet ongedaan worden gemaakt. +

+ + }>Annuleer + + +
+
+ ) +} + +function ResetToggle({ user }: { user: UserWithRoles }) { + const [pending, startTransition] = useTransition() + + function handleToggle() { + startTransition(async () => { + await setMustResetPasswordAction(user.id, !user.must_reset_password) + }) + } + + return ( + + ) +} + +export function UsersTable({ + users, + currentUserId, +}: { + users: UserWithRoles[] + currentUserId: string +}) { + return ( + + + + Gebruiker + Email + Rollen + Reset pw + Aangemaakt + Acties + + + + {users.map(user => ( + + {user.username} + {user.email ?? '—'} + +
+ {user.roles.map(r => ( + + ))} + {user.roles.length === 0 && Geen} +
+
+ + {user.must_reset_password ? ( + Ja + ) : ( + + )} + + + {new Date(user.created_at).toLocaleDateString('nl-NL')} + + +
+ + + +
+
+
+ ))} +
+
+ ) +}