Rollen-Fix
All checks were successful
CI / Build (push) Successful in 1m21s
CI / Deploy (push) Successful in 53s

This commit is contained in:
Jan
2026-04-13 23:44:46 +02:00
parent 6acc2852d8
commit dfdff6bf99
3 changed files with 146 additions and 15 deletions

View File

@@ -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) {

View File

@@ -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)
});
}

View File

@@ -127,6 +127,23 @@ function toggleApprovalPermission(
? currentValue.filter((entry) => entry !== approvalType)
: sortApprovalPermissions([...currentValue, approvalType]);
}
function sortManagedUsersList(users: DashboardManagedUser[]) {
const roleOrder: Record<DashboardManagedUser["role"], number> = {
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<string | null>(null);
const [editingUserId, setEditingUserId] = useState<string | null>(null);
const [passwordDrafts, setPasswordDrafts] = useState<Record<string, string>>({});
const [managedUsersState, setManagedUsersState] = useState(() => sortManagedUsersList(managedUsers));
const [userDrafts, setUserDrafts] = useState<Record<string, ManagedUserDraft>>({});
const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2));
const [periodForm, setPeriodForm] = useState<PeriodFormState>(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({
</Typography>
</Box>
<Stack spacing={1.4}>
{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;