From dfdff6bf995e5a3bc9e8195a4cde6c4156a0ca49 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 13 Apr 2026 23:44:46 +0200 Subject: [PATCH] Rollen-Fix --- src/app/api/users/[id]/route.ts | 50 +++++++++++++++- src/app/api/users/route.ts | 51 +++++++++++++++-- src/components/dashboard/dashboard-shell.tsx | 60 +++++++++++++++++--- 3 files changed, 146 insertions(+), 15 deletions(-) diff --git a/src/app/api/users/[id]/route.ts b/src/app/api/users/[id]/route.ts index 3d6471c..9e950de 100644 --- a/src/app/api/users/[id]/route.ts +++ b/src/app/api/users/[id]/route.ts @@ -21,6 +21,33 @@ const updateUserSchema = z.object({ approvalPermissions: z.array(approvalPermissionSchema).default([]) }); +function serializeManagedUser(user: { + id: string; + name: string; + username: string; + role: "ADMIN" | "FINANCE" | "MEMBER"; + workingGroupId: string | null; + workingGroup: { name: string } | null; + approvalPreference: "CHAIR_A" | "CHAIR_B" | "FINANCE" | null; + approvalPermissions: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[]; + _count: { + createdExpenses: number; + approvals: number; + }; +}) { + return { + id: user.id, + name: user.username, + username: user.username, + role: user.role, + workingGroupId: user.workingGroupId, + workingGroupName: user.workingGroup?.name ?? null, + approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference), + createdExpensesCount: user._count.createdExpenses, + approvalsCount: user._count.approvals + }; +} + type Context = { params: { id: string; @@ -91,7 +118,26 @@ export async function PATCH(request: Request, { params }: Context) { role: parsed.data.role, workingGroupId, approvalPreference, - approvalPermissions + approvalPermissions: { + set: approvalPermissions + } + } + }); + + const refreshedUser = await prisma.user.findUniqueOrThrow({ + where: { id: updatedUser.id }, + include: { + workingGroup: { + select: { + name: true + } + }, + _count: { + select: { + approvals: true, + createdExpenses: true + } + } } }); @@ -113,7 +159,7 @@ export async function PATCH(request: Request, { params }: Context) { } }); - return NextResponse.json({ ok: true }); + return NextResponse.json({ user: serializeManagedUser(refreshedUser) }); } export async function DELETE(_: Request, { params }: Context) { diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index 9f0822a..b4c1a56 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -24,6 +24,33 @@ const createUserSchema = z.object({ approvalPermissions: z.array(approvalPermissionSchema).default([]) }); +function serializeManagedUser(user: { + id: string; + name: string; + username: string; + role: "ADMIN" | "FINANCE" | "MEMBER"; + workingGroupId: string | null; + workingGroup: { name: string } | null; + approvalPreference: "CHAIR_A" | "CHAIR_B" | "FINANCE" | null; + approvalPermissions: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[]; + _count: { + createdExpenses: number; + approvals: number; + }; +}) { + return { + id: user.id, + name: user.username, + username: user.username, + role: user.role, + workingGroupId: user.workingGroupId, + workingGroupName: user.workingGroup?.name ?? null, + approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference), + createdExpensesCount: user._count.createdExpenses, + approvalsCount: user._count.approvals + }; +} + export async function POST(request: Request) { const viewer = await getCurrentViewer(); @@ -90,6 +117,23 @@ export async function POST(request: Request) { } }); + const createdUser = await prisma.user.findUniqueOrThrow({ + where: { id: user.id }, + include: { + workingGroup: { + select: { + name: true + } + }, + _count: { + select: { + approvals: true, + createdExpenses: true + } + } + } + }); + await createAuditLog(prisma, { actorId: viewer.id, action: "user.create", @@ -109,11 +153,6 @@ export async function POST(request: Request) { }); return NextResponse.json({ - user: { - id: user.id, - name: user.name, - username: user.username, - role: user.role - } + user: serializeManagedUser(createdUser) }); } diff --git a/src/components/dashboard/dashboard-shell.tsx b/src/components/dashboard/dashboard-shell.tsx index 732469a..17ff2e9 100644 --- a/src/components/dashboard/dashboard-shell.tsx +++ b/src/components/dashboard/dashboard-shell.tsx @@ -127,6 +127,23 @@ function toggleApprovalPermission( ? currentValue.filter((entry) => entry !== approvalType) : sortApprovalPermissions([...currentValue, approvalType]); } +function sortManagedUsersList(users: DashboardManagedUser[]) { + const roleOrder: Record = { + ADMIN: 0, + FINANCE: 1, + MEMBER: 2 + }; + + return [...users].sort((left, right) => { + const roleDifference = roleOrder[left.role] - roleOrder[right.role]; + if (roleDifference !== 0) { + return roleDifference; + } + + return left.username.localeCompare(right.username, "de-DE"); + }); +} + type MobileSection = "overview" | "actions"; type DesktopSection = "overview" | "budgetGroups" | "periods" | "users" | "logs"; @@ -276,6 +293,7 @@ export function DashboardShell({ const [editingPasswordUserId, setEditingPasswordUserId] = useState(null); const [editingUserId, setEditingUserId] = useState(null); const [passwordDrafts, setPasswordDrafts] = useState>({}); + const [managedUsersState, setManagedUsersState] = useState(() => sortManagedUsersList(managedUsers)); const [userDrafts, setUserDrafts] = useState>({}); const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2)); const [periodForm, setPeriodForm] = useState(getSuggestedPeriodDraft(currentPeriod)); @@ -422,10 +440,14 @@ export function DashboardShell({ }, [approvalThreshold]); useEffect(() => { - if (editingUserId && !managedUsers.some((user) => user.id === editingUserId)) { + setManagedUsersState(sortManagedUsersList(managedUsers)); + }, [managedUsers]); + + useEffect(() => { + if (editingUserId && !managedUsersState.some((user) => user.id === editingUserId)) { setEditingUserId(null); } - }, [editingUserId, managedUsers]); + }, [editingUserId, managedUsersState]); const selectedExpenseGroup = editableExpenseGroups.find((group) => group.id === expenseForm.agId) ?? defaultEditableGroup; const selectedBudgetOptions = selectedExpenseGroup?.budgets ?? []; @@ -859,7 +881,7 @@ export function DashboardShell({ const createdPassword = userForm.password; const createdUsername = userForm.username.trim().toLowerCase(); - await parseResponse( + const result = (await parseResponse( await fetch("/api/users", { method: "POST", headers: { @@ -873,7 +895,13 @@ export function DashboardShell({ approvalPermissions: sortApprovalPermissions(userForm.approvalPermissions) }) }) - ); + )) as { user?: DashboardManagedUser }; + + if (result.user) { + setManagedUsersState((current) => + sortManagedUsersList([...current.filter((user) => user.id !== result.user!.id), result.user!]) + ); + } setUserForm({ username: "", @@ -897,7 +925,7 @@ export function DashboardShell({ const draft = getManagedUserDraft(user); await runAction(async () => { - await parseResponse( + const result = (await parseResponse( await fetch(`/api/users/${user.id}`, { method: "PATCH", headers: { @@ -909,7 +937,13 @@ export function DashboardShell({ approvalPermissions: sortApprovalPermissions(draft.approvalPermissions) }) }) - ); + )) as { user?: DashboardManagedUser }; + + if (result.user) { + setManagedUsersState((current) => + sortManagedUsersList([...current.filter((entry) => entry.id !== result.user!.id), result.user!]) + ); + } setEditingUserId(null); }, `Nutzer ${user.username} wurde aktualisiert.`); @@ -947,6 +981,18 @@ export function DashboardShell({ method: "DELETE" }) ); + + setManagedUsersState((current) => current.filter((user) => user.id !== userId)); + setUserDrafts((current) => { + const next = { ...current }; + delete next[userId]; + return next; + }); + setPasswordDrafts((current) => { + const next = { ...current }; + delete next[userId]; + return next; + }); }, "Nutzer wurde gel\u00f6scht."); } @@ -1786,7 +1832,7 @@ export function DashboardShell({ - {managedUsers.map((user) => { + {managedUsersState.map((user) => { const canDelete = user.id !== viewer.id && user.createdExpensesCount === 0 && user.approvalsCount === 0; const isResetOpen = editingPasswordUserId === user.id; const isEditingUser = editingUserId === user.id;