Rollen-Fix
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user