This commit is contained in:
2025-12-01 07:59:40 +00:00
parent fc3ed57a40
commit f109423f54
39 changed files with 2914 additions and 2958 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+425 -23
View File
@@ -14,7 +14,6 @@ import {
} from "@material-tailwind/react";
import { useToast } from "@/components/ToastProvider.jsx";
import { platformSettingsData } from "@/data";
import { AVATAR_COLORS } from "@/data/avatarColors.js";
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
import { useAuth } from "@/components/AuthProvider.jsx";
@@ -69,6 +68,47 @@ const mapUser = (item) => ({
securityPhraseDownloadedAt: item?.securityPhraseDownloadedAt || null
});
const normalizeServer = (item) => {
if (!item || typeof item !== "object") {
return null;
}
const id = Number(item.id ?? item.serverId ?? item.server_id);
if (!Number.isFinite(id)) {
return null;
}
const name = item.name || item.serverName || item.server_name || `Server ${id}`;
return {
id,
name,
url: item.url || item.serverUrl || item.server_url || ""
};
};
const normalizeAssignmentsList = (list, servers = []) => {
const serverMap = new Map(servers.map((server) => [server.id, server]));
if (!Array.isArray(list)) {
return [];
}
return list
.map((entry) => {
const serverId = Number(entry?.serverId ?? entry?.server_id);
if (!Number.isFinite(serverId)) {
return null;
}
const groupId = Number(entry?.groupId ?? entry?.group_id);
const serverFallback = serverMap.get(serverId);
return {
serverId,
serverName: entry?.serverName || entry?.server_name || serverFallback?.name || "",
serverUrl: entry?.serverUrl || entry?.server_url || serverFallback?.url || "",
groupId: Number.isFinite(groupId) ? groupId : null,
groupName: entry?.groupName || entry?.group_name || "",
useGlobalGroup: Boolean(entry?.useGlobalGroup ?? entry?.use_global_group)
};
})
.filter(Boolean);
};
const extractPrimaryGroupId = (user) => {
if (!user || !Array.isArray(user.groups) || user.groups.length === 0) {
return null;
@@ -123,6 +163,11 @@ export function UserDetails() {
const [securityPhraseLoading, setSecurityPhraseLoading] = useState(false);
const [securityPhraseError, setSecurityPhraseError] = useState("");
const [renewingSecurityPhrase, setRenewingSecurityPhrase] = useState(false);
const [serverAssignments, setServerAssignments] = useState([]);
const [availableServers, setAvailableServers] = useState([]);
const [assignmentsError, setAssignmentsError] = useState("");
const [savingAssignments, setSavingAssignments] = useState(false);
const initialAssignmentsRef = useRef([]);
const maintenanceActive = Boolean(maintenanceMeta?.active);
const maintenanceMessage = maintenanceMeta?.message;
@@ -172,11 +217,22 @@ export function UserDetails() {
if (!item.id) {
throw new Error("USER_NOT_FOUND");
}
const normalizedServers = Array.isArray(response.data?.servers)
? response.data.servers.map(normalizeServer).filter(Boolean)
: [];
setAvailableServers(normalizedServers);
const normalizedAssignments = normalizeAssignmentsList(
response.data?.item?.serverAssignments ?? response.data?.serverAssignments ?? [],
normalizedServers
);
initialAssignmentsRef.current = normalizedAssignments;
setServerAssignments(normalizedAssignments);
setAssignmentsError("");
setUser(item);
setSecurityPhraseWords([]);
setSecurityPhraseDownloadedAt(item.securityPhraseDownloadedAt || null);
setSecurityPhraseError("");
const initialValues = buildInitialFormValues(item);
const initialValues = buildInitialFormValues(item);
initialFormValuesRef.current = { ...initialValues };
setFormValues(initialValues);
setSaveError("");
@@ -195,6 +251,9 @@ export function UserDetails() {
setUser(null);
initialFormValuesRef.current = buildInitialFormValues(null);
setFormValues(buildInitialFormValues(null));
setServerAssignments([]);
setAvailableServers([]);
setAssignmentsError("");
setError(message);
showToast({
variant: "error",
@@ -341,6 +400,208 @@ export function UserDetails() {
}));
}, [canEditUsers]);
const assignmentsChanged = useMemo(() => {
const serialize = (list) =>
JSON.stringify(
list
.map((entry) => ({
serverId: Number(entry.serverId) || null,
groupId: entry.useGlobalGroup ? null : (Number(entry.groupId) || null),
useGlobalGroup: Boolean(entry.useGlobalGroup)
}))
.sort((a, b) => (a.serverId || 0) - (b.serverId || 0))
);
return serialize(serverAssignments) !== serialize(initialAssignmentsRef.current);
}, [serverAssignments]);
const usedServerIds = useMemo(
() =>
new Set(
serverAssignments
.map((entry) => Number(entry.serverId))
.filter((value) => Number.isFinite(value) && value > 0)
),
[serverAssignments]
);
const getServerOptionsForRow = useCallback(
(currentServerId) => {
return availableServers.filter(
(server) => server.id === currentServerId || !usedServerIds.has(server.id)
);
},
[availableServers, usedServerIds]
);
const handleAddAssignment = useCallback(() => {
if (!canEditUsers || isSuperuserUser) return;
const usedServerIds = new Set(
serverAssignments
.map((entry) => Number(entry.serverId))
.filter((value) => Number.isFinite(value) && value > 0)
);
const nextServer = availableServers.find((server) => !usedServerIds.has(server.id));
if (!nextServer) {
setAssignmentsError("Alle verfügbaren Server sind bereits zugeordnet.");
return;
}
setAssignmentsError("");
setServerAssignments((prev) => [
...prev,
{
serverId: nextServer.id,
serverName: nextServer.name ?? "",
serverUrl: nextServer.url ?? "",
groupId: null,
groupName: "",
useGlobalGroup: true
}
]);
}, [canEditUsers, serverAssignments, availableServers]);
const handleAssignmentServerChange = useCallback(
(index, value) => {
if (!canEditUsers || isSuperuserUser) return;
const numeric = Number(value);
const server = availableServers.find((item) => item.id === numeric);
setServerAssignments((prev) =>
prev.map((entry, idx) =>
idx === index
? {
...entry,
serverId: Number.isFinite(numeric) ? numeric : null,
serverName: server?.name || entry.serverName,
serverUrl: server?.url || entry.serverUrl
}
: entry
)
);
},
[availableServers, canEditUsers]
);
const handleAssignmentGroupChange = useCallback(
(index, value) => {
if (!canEditUsers || isSuperuserUser) return;
const numeric = Number(value);
setServerAssignments((prev) =>
prev.map((entry, idx) =>
idx === index
? {
...entry,
groupId: Number.isFinite(numeric) ? numeric : null,
useGlobalGroup: false
}
: entry
)
);
},
[canEditUsers]
);
const handleAssignmentUseGlobalToggle = useCallback(
(index, checked) => {
if (!canEditUsers || isSuperuserUser) return;
setServerAssignments((prev) =>
prev.map((entry, idx) =>
idx === index
? {
...entry,
useGlobalGroup: Boolean(checked),
groupId: checked ? null : entry.groupId
}
: entry
)
);
},
[canEditUsers]
);
const handleRemoveAssignment = useCallback(
(index) => {
if (!canEditUsers || isSuperuserUser) return;
setServerAssignments((prev) => prev.filter((_, idx) => idx !== index));
},
[canEditUsers, isSuperuserUser]
);
const handleSaveAssignments = useCallback(async () => {
if (!canEditUsers || !numericUserId || !assignmentsChanged || isSuperuserUser) {
return;
}
const normalizedPayload = [];
const seenServers = new Set();
for (const assignment of serverAssignments) {
const serverId = Number(assignment.serverId);
if (!Number.isFinite(serverId) || serverId <= 0) {
setAssignmentsError("Bitte wähle für jede Zuordnung einen Server aus.");
return;
}
if (seenServers.has(serverId)) {
setAssignmentsError("Jeder Server kann nur einmal zugeordnet werden.");
return;
}
seenServers.add(serverId);
const useGlobalGroup = Boolean(assignment.useGlobalGroup);
const groupId = Number(assignment.groupId);
if (!useGlobalGroup && (!Number.isFinite(groupId) || groupId <= 0)) {
setAssignmentsError("Bitte wähle eine Rechtegruppe oder aktiviere die globale Gruppe.");
return;
}
normalizedPayload.push({
serverId,
groupId: useGlobalGroup ? null : groupId,
useGlobalGroup
});
}
setAssignmentsError("");
setSavingAssignments(true);
try {
const response = await axios.put(
`/api/users/${numericUserId}/server-assignments`,
{ assignments: normalizedPayload }
);
const normalizedAssignments = normalizeAssignmentsList(
response.data?.items ?? [],
availableServers
);
initialAssignmentsRef.current = normalizedAssignments;
setServerAssignments(normalizedAssignments);
showToast({
variant: "success",
title: "Serverzuordnungen gespeichert",
description: "Die Zuordnungen wurden erfolgreich aktualisiert."
});
} catch (err) {
const serverError = err.response?.data?.error;
let message = "Die Serverzuordnungen konnten nicht gespeichert werden.";
if (serverError === "USER_NOT_FOUND") {
message = "Der Benutzer wurde nicht gefunden.";
} else if (serverError === "INVALID_USER_ID") {
message = "Die Benutzer-ID ist ungültig.";
} else if (serverError === "USER_SUPERUSER_PROTECTED") {
message = "Für den Superuser können keine Server-Zuordnungen gesetzt werden.";
} else if (serverError === "SERVER_NOT_FOUND") {
message = "Ein ausgewählter Server existiert nicht mehr.";
} else if (serverError === "GROUP_NOT_FOUND") {
message = "Eine ausgewählte Rechtegruppe existiert nicht mehr.";
} else if (serverError === "INSUFFICIENT_PERMISSIONS") {
message = "Keine Berechtigung zum Ändern der Zuordnungen.";
}
setAssignmentsError(message);
showToast({
variant: "error",
title: "Speichern fehlgeschlagen",
description: message
});
} finally {
setSavingAssignments(false);
}
}, [canEditUsers, numericUserId, serverAssignments, availableServers, assignmentsChanged, showToast]);
const handleSaveUser = useCallback(async () => {
if (!canEditUsers || !user || !hasChanges) {
return;
@@ -775,30 +1036,171 @@ export function UserDetails() {
</div>
<div>
<Typography variant="h6" color="blue-gray" className="mb-3">
Platform Settings
Server-Zuordnungen
</Typography>
<div className="flex flex-col gap-12">
{platformSettingsData.map(({ title, options }) => (
<div key={title}>
<Typography className="mb-4 block text-xs font-semibold uppercase text-blue-gray-500">
{title}
</Typography>
<div className="flex flex-col gap-6">
{options.map(({ checked, label }) => (
<Switch
key={label}
id={label}
label={label}
defaultChecked={checked}
labelProps={{
className: "text-sm font-normal text-blue-gray-500",
}}
/>
))}
{isSuperuserUser ? (
<div className="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
Für den Superuser gelten immer alle Rechte und es können keine server-spezifischen Zuordnungen gesetzt werden.
</div>
) : (
<>
<Typography className="mb-4 text-sm text-blue-gray-500">
Ohne Zuordnungen hat der Benutzer Zugriff auf alle Server mit seinen globalen Rechten.
Lege hier Server-spezifische Gruppen fest oder markiere den Server explizit zur Nutzung der globalen Gruppe.
</Typography>
{assignmentsError && (
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
{assignmentsError}
</div>
)}
<div className="flex flex-col gap-4">
{serverAssignments.length === 0 ? (
<div className="rounded-lg border border-blue-gray-100 bg-blue-gray-50/60 px-4 py-3 text-sm text-blue-gray-600">
Keine Server zugeordnet. Die globalen Gruppen gelten für alle Server.
</div>
) : (
serverAssignments.map((assignment, index) => {
const serverOptions = getServerOptionsForRow(assignment.serverId);
const groupSelectValue =
assignment.useGlobalGroup || !assignment.groupId
? ""
: String(assignment.groupId);
return (
<div
key={`assignment-${assignment.serverId ?? index}-${index}`}
className="rounded-lg border border-blue-gray-100 bg-white px-4 py-3 shadow-sm"
>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
Server
</Typography>
<Select
label="Server auswählen"
value={
assignment.serverId
? String(assignment.serverId)
: ""
}
onChange={(value) =>
handleAssignmentServerChange(index, value)
}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers
}
variant="outlined"
>
{serverOptions.map((server) => (
<Option key={server.id} value={String(server.id)}>
{server.name}
</Option>
))}
</Select>
</div>
<div>
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
Rechtegruppe
</Typography>
<div className="flex flex-col gap-2">
<Switch
id={`assignment-global-${index}`}
label="Globale Gruppe verwenden"
checked={assignment.useGlobalGroup}
onChange={({ target }) =>
handleAssignmentUseGlobalToggle(
index,
target?.checked
)
}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers
}
labelProps={{
className: "text-sm font-normal text-blue-gray-600"
}}
/>
<Select
label="Server-Gruppe wählen"
value={groupSelectValue}
onChange={(value) =>
handleAssignmentGroupChange(index, value)
}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers ||
assignment.useGlobalGroup
}
variant="outlined"
className="max-w-xl"
>
<Option value="">Globale Gruppe</Option>
{availableGroups.map((group) => (
<Option key={group.id} value={String(group.id)}>
{group.name}
</Option>
))}
</Select>
</div>
</div>
</div>
<div className="mt-3 flex justify-end">
<Button
variant="text"
color="red"
className="normal-case"
onClick={() => handleRemoveAssignment(index)}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers
}
>
Entfernen
</Button>
</div>
</div>
);
})
)}
</div>
))}
</div>
<div className="mt-4 flex flex-wrap items-center gap-3">
<Button
variant="outlined"
color="blue"
className="normal-case"
onClick={handleAddAssignment}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers ||
availableServers.length === 0 ||
usedServerIds.size >= availableServers.length
}
>
Server hinzufügen
</Button>
<Button
color="green"
className="normal-case"
onClick={handleSaveAssignments}
disabled={
maintenanceLocked ||
savingAssignments ||
!canEditUsers ||
!assignmentsChanged
}
>
{savingAssignments ? "Speichert ..." : "Zuordnungen speichern"}
</Button>
{savingAssignments && <Spinner className="h-4 w-4" />}
</div>
</>
)}
</div>
</div>
+187 -97
View File
@@ -78,8 +78,14 @@ const LEVEL_PRIORITY = {
full: 2
};
const normalizePermissionLevel = (key, value) =>
key === "maintenance-server-delete" && value !== "full" ? "none" : value;
const isDependencySatisfied = (value, requirement) => {
const req = requirement || "!=none";
if (req === "!=none") return value !== "none";
if (req.startsWith("!=")) return value !== req.substring(2);
if (req.startsWith("=")) return value === req.substring(1);
if (!req) return true;
return value === req;
};
export function UserGroupDetail() {
const { groupId } = useParams();
@@ -177,22 +183,28 @@ export function UserGroupDetail() {
};
};
const normalizeGroup = (group, groupIdx = 0) => {
const groupItems = Array.isArray(group.items)
? group.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
: [];
const childGroups = Array.isArray(group.groups)
? group.groups.map((child, childIdx) => normalizeGroup(child, childIdx))
: [];
return {
...group,
sortOrder: typeof group.sortOrder === "number" ? group.sortOrder : groupIdx,
items: groupItems,
groups: childGroups
};
};
const normalizedSections = rawSections.map((section, sectionIndex) => {
const sectionItems = Array.isArray(section.items)
? section.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
: [];
const sectionGroups = Array.isArray(section.groups)
? section.groups.map((group, groupIdx) => {
const groupItems = Array.isArray(group.items)
? group.items.map((item, itemIdx) => normalizeItem(item, itemIdx)).filter(Boolean)
: [];
return {
...group,
sortOrder: typeof group.sortOrder === "number" ? group.sortOrder : groupIdx,
items: groupItems
};
})
? section.groups.map((group, groupIdx) => normalizeGroup(group, groupIdx))
: [];
return {
@@ -515,16 +527,52 @@ export function UserGroupDetail() {
if (!permissionKey || !canAdjustPermissions || isSuperuserGroup) {
return;
}
const nextValue = normalizePermissionLevel(permissionKey, value);
setPermissionSelection((prev) => {
const shouldResetDelete =
permissionKey === "maintenance-server-manage" && nextValue !== "full";
const shouldResetEdit =
permissionKey === "maintenance-server-manage" && nextValue === "none";
const shouldResetDeleteFromEdit =
permissionKey === "maintenance-server-edit" && nextValue !== "full";
const getValueWithDefaults = (key, selectionMap) =>
selectionMap[key] ?? permissionDefaults[key] ?? "none";
if (!shouldResetDelete && !shouldResetEdit && !shouldResetDeleteFromEdit && prev[permissionKey] === nextValue) {
const collectAllItems = () => {
const items = [];
const walkGroups = (groups) => {
(groups || []).forEach((group) => {
(group.items || []).forEach((item) => items.push(item));
if (group.groups) {
walkGroups(group.groups);
}
});
};
permissionSections.forEach((section) => {
(section.items || []).forEach((item) => items.push(item));
walkGroups(section.groups);
});
return items;
};
const allItems = collectAllItems();
const itemMap = new Map(allItems.map((item) => [item.key, item]));
const clampToAllowed = (itemKey, desiredLevel) => {
const item = itemMap.get(itemKey);
if (!item) return desiredLevel;
const allowed = Array.isArray(item.availableLevels) && item.availableLevels.length
? item.availableLevels
: LEVEL_OPTIONS.map((opt) => opt.value);
if (allowed.includes(desiredLevel)) {
return desiredLevel;
}
// Fallback: wähle niedrigstes erlaubtes Level
return allowed.reduce((lowest, candidate) => {
return LEVEL_PRIORITY[candidate] < LEVEL_PRIORITY[lowest] ? candidate : lowest;
}, allowed[0] || "none");
};
const nextValue = clampToAllowed(permissionKey, value);
setPermissionSelection((prev) => {
const currentValue = getValueWithDefaults(permissionKey, prev);
const isDowngrade = LEVEL_PRIORITY[nextValue] < LEVEL_PRIORITY[currentValue];
if (prev[permissionKey] === nextValue) {
return prev;
}
@@ -533,19 +581,31 @@ export function UserGroupDetail() {
[permissionKey]: nextValue
};
if (shouldResetDelete) {
nextState["maintenance-server-delete"] = "none";
}
if (shouldResetEdit) {
nextState["maintenance-server-edit"] = "none";
}
if (shouldResetDeleteFromEdit) {
nextState["maintenance-server-delete"] = "none";
// Kaskade: Nur bei Downgrade des auslösenden Rechts abhängige Rechte herabstufen.
if (isDowngrade) {
let changed = true;
while (changed) {
changed = false;
allItems.forEach((item) => {
const dependencies = Array.isArray(item.dependencies) ? item.dependencies : [];
if (!dependencies.length) return;
const satisfied = dependencies.every((dep) =>
isDependencySatisfied(getValueWithDefaults(dep.key, nextState), dep.requiredLevel)
);
if (!satisfied) {
const cascadedValue = clampToAllowed(item.key, nextValue);
if (getValueWithDefaults(item.key, nextState) !== cascadedValue) {
nextState[item.key] = cascadedValue;
changed = true;
}
}
});
}
}
return nextState;
});
}, [canAdjustPermissions, isSuperuserGroup]);
}, [canAdjustPermissions, isSuperuserGroup, permissionDefaults, permissionSections]);
const getPermissionValue = useCallback(
(permissionKey) => {
@@ -559,7 +619,7 @@ export function UserGroupDetail() {
value = permissionDefaults[permissionKey];
}
if (typeof value === "string") {
return normalizePermissionLevel(permissionKey, value);
return value;
}
return value;
},
@@ -572,12 +632,6 @@ export function UserGroupDetail() {
),
[]
);
const radioCheckedIcon = useMemo(
() => (
<span className="mx-auto block h-2.5 w-2.5 rounded-full border border-blue-gray-700 bg-gray-900" />
),
[]
);
const isPermissionRowVisible = useCallback(
(row) => {
@@ -617,9 +671,7 @@ export function UserGroupDetail() {
const rowName = `permission-${ownerKey}-${row.key}`;
const currentValue = getPermissionValue(row.key) ?? "none";
const displayValue = normalizePermissionLevel(row.key, currentValue);
const serverManageLevel = getPermissionValue("maintenance-server-manage") ?? "none";
const serverEditLevel = getPermissionValue("maintenance-server-edit") ?? "none";
const displayValue = currentValue;
const { addTopBorder = true } = options;
const rowClasses = [
@@ -632,11 +684,7 @@ export function UserGroupDetail() {
const baseLevels = Array.isArray(row.availableLevels) && row.availableLevels.length
? row.availableLevels
: LEVEL_OPTIONS.map((option) => option.value);
const availableLevels = new Set(
row.key === "maintenance-server-delete"
? baseLevels.filter((level) => level === "full" || level === "none")
: baseLevels
);
const availableLevels = new Set(baseLevels);
const levelOptions = LEVEL_OPTIONS;
return (
@@ -648,22 +696,12 @@ export function UserGroupDetail() {
{levelOptions.map((option) => {
const optionId = `${rowName}-${option.value}`;
const checked = displayValue === option.value;
const requiresManageFull =
row.key === "maintenance-server-delete" &&
option.value === "full" &&
(LEVEL_PRIORITY[serverManageLevel] ?? 0) < LEVEL_PRIORITY.full;
const requiresEditFull =
row.key === "maintenance-server-delete" &&
option.value === "full" &&
(LEVEL_PRIORITY[serverEditLevel] ?? 0) < LEVEL_PRIORITY.full;
const disabled =
!availableLevels.has(option.value) ||
permissionsLoading ||
savingGroup ||
!canAdjustPermissions ||
isSuperuserGroup ||
requiresManageFull ||
requiresEditFull;
isSuperuserGroup;
return (
<ListItem
@@ -689,7 +727,6 @@ export function UserGroupDetail() {
className: "p-0"
}}
icon={radioIcon}
checkedIcon={radioCheckedIcon}
/>
</ListItemPrefix>
<Typography
@@ -708,6 +745,30 @@ export function UserGroupDetail() {
);
};
const groupHasVisibleRows = (grp) => {
if (!grp) {
return false;
}
const groupItems = Array.isArray(grp.items) ? grp.items : [];
const childGroups = Array.isArray(grp.groups) ? grp.groups : [];
if (groupItems.some(isPermissionRowVisible)) {
return true;
}
return childGroups.some(groupHasVisibleRows);
};
const sectionHasVisibleRows = (section) => {
if (!section) {
return false;
}
const sectionItems = Array.isArray(section.items) ? section.items : [];
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
if (sectionItems.some(isPermissionRowVisible)) {
return true;
}
return sectionGroups.some(groupHasVisibleRows);
};
return (
<>
<div className="mt-12 flex flex-col gap-12">
@@ -725,9 +786,6 @@ export function UserGroupDetail() {
</div>
</div>
)}
<div className="relative h-72 w-full overflow-hidden rounded-xl bg-[url('/img/background-image.png')] bg-cover\tbg-center">
<div className="absolute inset-0 h-full w-full bg-gray-900/75" />
</div>
<Card className="mx-3 -mt-16 mb-6 lg:mx-4 border border-blue-gray-100">
<CardBody className="p-4">
<div className="mb-10 flex flex-wrap items-center justify-between gap-6">
@@ -893,7 +951,7 @@ export function UserGroupDetail() {
Die Superuser-Gruppe besitzt automatisch Vollzugriff auf alle Bereiche. Berechtigungen können hier nicht angepasst werden.
</div>
) : !canViewPermissionSettings ? null : (
<div className="rounded-xl border border-blue-gray-100 bg-white shadow-sm">
<>
{permissionsLoading && (
<div className="border-b border-blue-gray-100 px-4 py-4">
<div className="flex items-center gap-3 text-sm text-blue-gray-500">
@@ -913,53 +971,85 @@ export function UserGroupDetail() {
Für diese Gruppe sind keine Berechtigungen definiert.
</div>
) : (
permissionSections.map((section, sectionIndex) => {
const sectionItems = Array.isArray(section.items) ? section.items : [];
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
(() => {
const renderedSections = permissionSections
.map((section, sectionIndex) => {
const sectionItems = Array.isArray(section.items) ? section.items : [];
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
return (
<div
key={section.key || sectionIndex}
className={sectionIndex === 0 ? "" : "border-t border-blue-gray-100"}
>
<div className="border-b border-blue-gray-100 px-4 py-4">
<Typography variant="h6" className="text-lg font-semibold text-blue-gray-700">
{section.title}
</Typography>
</div>
{sectionItems.map((row, rowIndex) =>
renderPermissionRow(row, section.key || sectionIndex, {
addTopBorder: rowIndex !== 0
})
)}
{sectionGroups.map((group, groupIdx) => {
const groupItems = Array.isArray(group.items) ? group.items : [];
const hasVisibleRows = groupItems.some(isPermissionRowVisible);
if (!sectionHasVisibleRows(section)) {
return null;
}
if (!hasVisibleRows) {
const renderGroup = (currentGroup, path = `${section.key || sectionIndex}-group`, depth = 0) => {
if (!groupHasVisibleRows(currentGroup)) {
return null;
}
const groupItems = Array.isArray(currentGroup.items) ? currentGroup.items : [];
const childGroups = Array.isArray(currentGroup.groups) ? currentGroup.groups : [];
return (
<div key={group.key || `${section.key || sectionIndex}-${groupIdx}`} className="border-t border-blue-gray-100">
<div className="border-b border-blue-gray-100 px-4 py-3">
<Typography className="text-sm font-semibold uppercase tracking-wide text-blue-gray-600">
{group.title}
<div
key={currentGroup.key || path}
className=""
>
<div className="border-b border-blue-gray-100 bg-blue-gray-50/80 px-4 py-3">
<Typography className={`${depth > 0 ? "ml-2" : ""} text-xs font-semibold uppercase tracking-wide text-blue-gray-600`}>
{currentGroup.title}
</Typography>
</div>
{groupItems.map((row, rowIndex) =>
renderPermissionRow(row, group.key || `${section.key || sectionIndex}-${groupIdx}`, {
addTopBorder: rowIndex !== 0
})
)}
<div className="divide-y divide-blue-gray-100">
{groupItems.map((row) =>
renderPermissionRow(row, `${path}-${row.key}`, {
addTopBorder: false
})
)}
{childGroups.map((childGroup, childIdx) =>
renderGroup(childGroup, `${path}-${childGroup.key || childIdx}`, depth + 1)
)}
</div>
</div>
);
})}
</div>
);
})
};
return (
<div
key={section.key || sectionIndex}
className="overflow-hidden rounded-xl border border-blue-gray-100 bg-white shadow-sm"
>
<div className="border-b border-blue-gray-100 bg-blue-gray-50/70 px-4 py-3">
<Typography variant="h6" className="text-lg font-semibold text-blue-gray-700">
{section.title}
</Typography>
</div>
<div className="divide-y divide-blue-gray-100">
{sectionItems.map((row) =>
renderPermissionRow(row, section.key || sectionIndex, {
addTopBorder: false
})
)}
{sectionGroups.map((group, groupIdx) =>
renderGroup(group, `${section.key || sectionIndex}-${groupIdx}`, 0)
)}
</div>
</div>
);
})
.filter(Boolean);
if (renderedSections.length === 0) {
return (
<div className="border-b border-blue-gray-100 px-4 py-4 text-sm text-blue-gray-500">
Für diese Gruppe sind keine Berechtigungen definiert.
</div>
);
}
return <div className="flex flex-col gap-4">{renderedSections}</div>;
})()
))}
</div>
</>
)}
</>