Update
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
export const AVATAR_COLORS = [
|
||||
"bg-arcticBlue-600",
|
||||
"bg-copperRust-500",
|
||||
"bg-sunsetCoral-600",
|
||||
"bg-mintTea-400",
|
||||
"bg-lavenderSmoke-500",
|
||||
"bg-emeraldMist-500",
|
||||
"bg-roseQuartz-500",
|
||||
"bg-auroraTeal-500",
|
||||
"bg-citrusPunch-500",
|
||||
"bg-mossGreen-400"
|
||||
];
|
||||
|
||||
export const DEFAULT_AVATAR_COLOR = "bg-mossGreen-500";
|
||||
@@ -9,18 +9,13 @@ import {
|
||||
Button,
|
||||
Spinner,
|
||||
CardFooter,
|
||||
Tabs,
|
||||
TabsHeader,
|
||||
Tab,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Checkbox
|
||||
Select,
|
||||
Option
|
||||
} from "@material-tailwind/react";
|
||||
import {
|
||||
HomeIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
Cog6ToothIcon,
|
||||
PencilIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -28,22 +23,9 @@ import { Link } from "react-router-dom";
|
||||
import { useToast } from "@/components/ToastProvider.jsx";
|
||||
import { ProfileInfoCard } from "@/widgets/cards";
|
||||
import { platformSettingsData, projectsData } from "@/data";
|
||||
import { AVATAR_COLORS } from "@/data/avatarColors.js";
|
||||
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
|
||||
|
||||
const DEFAULT_AVATAR = { background: "bg-blue-gray-500", text: "text-white" };
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'bg-arcticBlue-600',
|
||||
'bg-copperRust-500',
|
||||
'bg-sunsetCoral-600',
|
||||
'bg-mintTea-400',
|
||||
'bg-lavenderSmoke-500',
|
||||
'bg-emeraldMist-500',
|
||||
'bg-roseQuartz-500',
|
||||
'bg-auroraTeal-500',
|
||||
'bg-citrusPunch-500',
|
||||
'bg-mossGreen-400'
|
||||
];
|
||||
const _ = AVATAR_COLORS.join(" ");
|
||||
|
||||
|
||||
@@ -97,8 +79,8 @@ export function UserDetails() {
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
const [groupsLoading, setGroupsLoading] = useState(false);
|
||||
const [groupsError, setGroupsError] = useState("");
|
||||
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
|
||||
const [savingGroups, setSavingGroups] = useState(false);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
||||
const [savingGroup, setSavingGroup] = useState(false);
|
||||
|
||||
const maintenanceActive = Boolean(maintenance?.active);
|
||||
|
||||
@@ -203,109 +185,95 @@ export function UserDetails() {
|
||||
fetchAvailableGroups();
|
||||
}, [fetchAvailableGroups]);
|
||||
|
||||
const originalGroupIds = useMemo(() => {
|
||||
if (!user || !Array.isArray(user.groups)) {
|
||||
return [];
|
||||
const originalGroupId = useMemo(() => {
|
||||
if (!user || !Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return user.groups
|
||||
const firstValid = user.groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0);
|
||||
.find((id) => Number.isFinite(id) && id > 0);
|
||||
return Number.isFinite(firstValid) ? firstValid : null;
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedGroupIds([...originalGroupIds]);
|
||||
}, [originalGroupIds]);
|
||||
setSelectedGroupId(originalGroupId);
|
||||
}, [originalGroupId]);
|
||||
|
||||
const hasGroupChanges = useMemo(() => {
|
||||
const normalize = (ids) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
ids
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0)
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
|
||||
const next = normalize(selectedGroupIds);
|
||||
const baseline = normalize(originalGroupIds);
|
||||
|
||||
if (next.length !== baseline.length) {
|
||||
return true;
|
||||
if (!hasLoaded) {
|
||||
return false;
|
||||
}
|
||||
return originalGroupId !== selectedGroupId;
|
||||
}, [originalGroupId, selectedGroupId, hasLoaded]);
|
||||
|
||||
return next.some((value, index) => value !== baseline[index]);
|
||||
}, [selectedGroupIds, originalGroupIds]);
|
||||
|
||||
const handleToggleGroup = useCallback((groupId) => {
|
||||
const numeric = Number(groupId);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
const handleGroupChange = useCallback((value) => {
|
||||
if (!value) {
|
||||
setSelectedGroupId(null);
|
||||
return;
|
||||
}
|
||||
setSelectedGroupIds((prev) => {
|
||||
if (prev.includes(numeric)) {
|
||||
return prev.filter((id) => id !== numeric);
|
||||
}
|
||||
return [...prev, numeric];
|
||||
});
|
||||
const numeric = Number(value);
|
||||
setSelectedGroupId(Number.isFinite(numeric) && numeric > 0 ? numeric : null);
|
||||
}, []);
|
||||
|
||||
const handleResetGroups = useCallback(() => {
|
||||
setSelectedGroupIds([...originalGroupIds]);
|
||||
}, [originalGroupIds]);
|
||||
const handleResetGroup = useCallback(() => {
|
||||
setSelectedGroupId(originalGroupId);
|
||||
}, [originalGroupId]);
|
||||
|
||||
const handleSaveGroups = useCallback(async () => {
|
||||
if (!user) {
|
||||
const handleSaveGroup = useCallback(async () => {
|
||||
if (!user || !hasGroupChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadGroups = Array.from(
|
||||
new Set(
|
||||
selectedGroupIds
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0)
|
||||
)
|
||||
);
|
||||
const payloadGroups = selectedGroupId ? [selectedGroupId] : [];
|
||||
|
||||
setSavingGroups(true);
|
||||
setSavingGroup(true);
|
||||
setGroupsError("");
|
||||
try {
|
||||
const response = await axios.put(`/api/users/${user.id}/groups`, { groupIds: payloadGroups });
|
||||
const updatedUser = mapUser(response.data?.item || response.data?.user);
|
||||
setUser(updatedUser);
|
||||
setSelectedGroupIds(
|
||||
Array.isArray(updatedUser.groups)
|
||||
? updatedUser.groups
|
||||
.map((group) => Number(group.id))
|
||||
.filter((id) => Number.isFinite(id) && id > 0)
|
||||
: []
|
||||
);
|
||||
const nextGroupId = Array.isArray(updatedUser.groups) && updatedUser.groups.length > 0
|
||||
? Number(updatedUser.groups[0].id)
|
||||
: null;
|
||||
setSelectedGroupId(Number.isFinite(nextGroupId) && nextGroupId > 0 ? nextGroupId : null);
|
||||
showToast({
|
||||
variant: "success",
|
||||
title: "Benutzergruppen aktualisiert",
|
||||
title: "Globale Rolle aktualisiert",
|
||||
description: "Die Gruppenzuordnung wurde gespeichert."
|
||||
});
|
||||
} catch (err) {
|
||||
const serverError = err.response?.data?.error;
|
||||
let message = err.message || "Benutzergruppen konnten nicht aktualisiert werden.";
|
||||
let message = err.message || "Die globale Rolle konnte nicht aktualisiert werden.";
|
||||
if (serverError === "INVALID_USER_ID") {
|
||||
message = "Die Benutzer-ID ist ungültig.";
|
||||
} else if (serverError === "USER_NOT_FOUND") {
|
||||
message = "Der Benutzer konnte nicht gefunden werden.";
|
||||
} else if (serverError === "GROUP_NOT_FOUND") {
|
||||
message = "Mindestens eine ausgewählte Gruppe existiert nicht mehr.";
|
||||
message = "Die ausgewählte Gruppe existiert nicht mehr.";
|
||||
} else if (typeof serverError === "string" && serverError.length > 0) {
|
||||
message = serverError;
|
||||
}
|
||||
setGroupsError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Benutzergruppen",
|
||||
title: "Globale Rolle",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setSavingGroups(false);
|
||||
setSavingGroup(false);
|
||||
}
|
||||
}, [user, selectedGroupIds, showToast]);
|
||||
}, [user, selectedGroupId, hasGroupChanges, showToast]);
|
||||
|
||||
const selectedGroupLabel = useMemo(() => {
|
||||
if (!selectedGroupId) {
|
||||
return null;
|
||||
}
|
||||
const match = availableGroups.find((group) => group.id === selectedGroupId);
|
||||
return match ? match.name : null;
|
||||
}, [availableGroups, selectedGroupId]);
|
||||
|
||||
const selectValue = selectedGroupId ? String(selectedGroupId) : "";
|
||||
const selectDisabled = maintenanceActive || savingGroup || !user || groupsLoading;
|
||||
|
||||
const avatarLabel = useMemo(() => {
|
||||
const source = (user?.username || user?.email || "").trim();
|
||||
@@ -406,7 +374,7 @@ export function UserDetails() {
|
||||
<div id="platform" className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<Typography variant="h6" color="blue-gray">
|
||||
Benutzergruppen
|
||||
Globale Rolle
|
||||
</Typography>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@@ -414,8 +382,8 @@ export function UserDetails() {
|
||||
size="sm"
|
||||
color="blue-gray"
|
||||
className="normal-case"
|
||||
onClick={handleResetGroups}
|
||||
disabled={!hasGroupChanges || savingGroups || !user}
|
||||
onClick={handleResetGroup}
|
||||
disabled={!hasGroupChanges || savingGroup || !user}
|
||||
>
|
||||
Änderungen verwerfen
|
||||
</Button>
|
||||
@@ -423,10 +391,10 @@ export function UserDetails() {
|
||||
size="sm"
|
||||
color="green"
|
||||
className="normal-case"
|
||||
onClick={handleSaveGroups}
|
||||
disabled={maintenanceActive || savingGroups || !hasGroupChanges || !user}
|
||||
onClick={handleSaveGroup}
|
||||
disabled={maintenanceActive || savingGroup || !hasGroupChanges || !user}
|
||||
>
|
||||
{savingGroups ? "Speichert ..." : "Speichern"}
|
||||
{savingGroup ? "Speichert ..." : "Speichern"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -450,43 +418,31 @@ export function UserDetails() {
|
||||
Es sind noch keine Benutzergruppen vorhanden.
|
||||
</Typography>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3">
|
||||
{availableGroups.map((group) => {
|
||||
const checked = selectedGroupIds.includes(group.id);
|
||||
return (
|
||||
<label
|
||||
key={group.id}
|
||||
className={`flex items-start justify-between gap-3 rounded-lg border px-4 py-3 transition ${
|
||||
checked ? "border-auroraTeal-500 bg-auroraTeal-500/10" : "border-blue-gray-100 bg-white"
|
||||
} ${maintenanceActive ? "opacity-70" : ""}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
ripple={false}
|
||||
className="mt-1"
|
||||
checked={checked}
|
||||
onChange={() => handleToggleGroup(group.id)}
|
||||
disabled={maintenanceActive || savingGroups || !user}
|
||||
/>
|
||||
<div>
|
||||
<Typography variant="small" className="font-semibold text-blue-gray-900">
|
||||
{group.name}
|
||||
</Typography>
|
||||
{group.description && (
|
||||
<Typography variant="small" className="text-xs text-stormGrey-500">
|
||||
{group.description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Typography variant="small" className="text-xs font-medium text-stormGrey-500">
|
||||
Mitglieder: {group.memberCount}
|
||||
</Typography>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div className="sm:flex-1">
|
||||
<Select
|
||||
label="Benutzergruppe wählen"
|
||||
value={selectValue}
|
||||
onChange={handleGroupChange}
|
||||
disabled={selectDisabled}
|
||||
variant="outlined"
|
||||
>
|
||||
{availableGroups.map((group) => (
|
||||
<Option key={group.id} value={String(group.id)}>
|
||||
{group.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(groupsLoading && availableGroups.length === 0) && (
|
||||
<Typography variant="small" className="text-sm text-stormGrey-500">
|
||||
{selectedGroupLabel
|
||||
? `Aktuell zugewiesene Gruppe: ${selectedGroupLabel}`
|
||||
: "Dem Benutzer ist aktuell keine Gruppe zugewiesen."}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
|
||||
@@ -9,8 +9,7 @@ import {
|
||||
Button,
|
||||
Select,
|
||||
Option,
|
||||
Input,
|
||||
useSelect
|
||||
Input
|
||||
} from "@material-tailwind/react";
|
||||
import { PaginationControls, usePage } from "@/components/PageProvider.jsx";
|
||||
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
|
||||
@@ -21,6 +20,10 @@ export function Usergroups() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [newGroupName, setNewGroupName] = useState("");
|
||||
const [newGroupDescription, setNewGroupDescription] = useState("");
|
||||
const [createGroupError, setCreateGroupError] = useState("");
|
||||
const [creatingGroup, setCreatingGroup] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { maintenance } = useMaintenance();
|
||||
@@ -171,6 +174,65 @@ export function Usergroups() {
|
||||
}).format(parsed);
|
||||
}, []);
|
||||
|
||||
const resetNewGroupForm = useCallback(() => {
|
||||
setNewGroupName("");
|
||||
setNewGroupDescription("");
|
||||
setCreateGroupError("");
|
||||
}, []);
|
||||
|
||||
const handleCreateGroup = useCallback(async () => {
|
||||
if (maintenanceActive) {
|
||||
return;
|
||||
}
|
||||
const trimmedName = newGroupName.trim();
|
||||
if (!trimmedName) {
|
||||
setCreateGroupError("Gruppenname ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
setCreatingGroup(true);
|
||||
setCreateGroupError("");
|
||||
try {
|
||||
const payload = { name: trimmedName };
|
||||
const trimmedDescription = newGroupDescription.trim();
|
||||
if (trimmedDescription) {
|
||||
payload.description = trimmedDescription;
|
||||
}
|
||||
const response = await axios.post("/api/groups", payload);
|
||||
showToast({
|
||||
variant: "success",
|
||||
title: "Gruppe angelegt",
|
||||
description: `Die Benutzergruppe "${response.data?.item?.name ?? trimmedName}" wurde erstellt.`
|
||||
});
|
||||
resetNewGroupForm();
|
||||
fetchGroups();
|
||||
} catch (err) {
|
||||
const errorCode = err.response?.data?.error;
|
||||
let message = "Benutzergruppe konnte nicht angelegt werden.";
|
||||
if (errorCode === "GROUP_NAME_REQUIRED") {
|
||||
message = "Gruppenname ist erforderlich.";
|
||||
} else if (errorCode === "GROUP_NAME_TAKEN") {
|
||||
message = "Der Gruppenname wird bereits verwendet.";
|
||||
}
|
||||
setCreateGroupError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Erstellen fehlgeschlagen",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setCreatingGroup(false);
|
||||
}
|
||||
}, [
|
||||
maintenanceActive,
|
||||
newGroupName,
|
||||
newGroupDescription,
|
||||
showToast,
|
||||
resetNewGroupForm,
|
||||
fetchGroups
|
||||
]);
|
||||
|
||||
const createGroupDisabled = maintenanceActive || creatingGroup;
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<Card className="border border-blue-gray-100 shadow-sm">
|
||||
@@ -190,6 +252,44 @@ export function Usergroups() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8 rounded-lg border border-blue-gray-100 bg-white p-4 shadow-sm">
|
||||
<Typography variant="h6" color="blue-gray" className="mb-2">
|
||||
Neue Benutzergruppe anlegen
|
||||
</Typography>
|
||||
<Typography variant="small" className="text-sm text-stormGrey-500 mb-4">
|
||||
Der Gruppenname ist Pflicht, die Beschreibung optional.
|
||||
</Typography>
|
||||
{createGroupError && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{createGroupError}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Input
|
||||
label="Gruppenname"
|
||||
value={newGroupName}
|
||||
onChange={(event) => setNewGroupName(event.target.value)}
|
||||
disabled={createGroupDisabled}
|
||||
crossOrigin=""
|
||||
/>
|
||||
<Input
|
||||
label="Beschreibung (optional)"
|
||||
value={newGroupDescription}
|
||||
onChange={(event) => setNewGroupDescription(event.target.value)}
|
||||
disabled={createGroupDisabled}
|
||||
crossOrigin=""
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button color="green" onClick={handleCreateGroup} disabled={createGroupDisabled}>
|
||||
{creatingGroup ? "Speichert ..." : "Gruppe anlegen"}
|
||||
</Button>
|
||||
<Button variant="text" color="blue-gray" onClick={resetNewGroupForm} disabled={creatingGroup}>
|
||||
Formular zurücksetzen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="md:flex-1">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { PaginationControls, usePage } from "@/components/PageProvider.jsx";
|
||||
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
|
||||
import { useToast } from "@/components/ToastProvider.jsx";
|
||||
import { AVATAR_COLORS } from "@/data/avatarColors.js";
|
||||
|
||||
const normalizeUserGroups = (rawGroups) => {
|
||||
if (!Array.isArray(rawGroups)) {
|
||||
@@ -47,6 +48,16 @@ export function Users() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
const [groupsLoading, setGroupsLoading] = useState(false);
|
||||
const [groupsError, setGroupsError] = useState("");
|
||||
const [newUsername, setNewUsername] = useState("");
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newGroupId, setNewGroupId] = useState("");
|
||||
const [newAvatarColor, setNewAvatarColor] = useState("");
|
||||
const [createError, setCreateError] = useState("");
|
||||
const [creatingUser, setCreatingUser] = useState(false);
|
||||
|
||||
const { showToast } = useToast();
|
||||
const { maintenance } = useMaintenance();
|
||||
@@ -100,13 +111,45 @@ export function Users() {
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const fetchGroups = useCallback(async () => {
|
||||
setGroupsLoading(true);
|
||||
setGroupsError("");
|
||||
try {
|
||||
const response = await axios.get("/api/groups");
|
||||
const items = Array.isArray(response.data?.items) ? response.data.items : [];
|
||||
const normalized = items
|
||||
.map((item) => ({
|
||||
id: Number(item.id),
|
||||
name: item.name || ""
|
||||
}))
|
||||
.filter((group) => Number.isFinite(group.id) && group.id > 0 && group.name)
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "de-DE"));
|
||||
setAvailableGroups(normalized);
|
||||
} catch (err) {
|
||||
const message = err.response?.data?.error || err.message || "Benutzergruppen konnten nicht geladen werden.";
|
||||
setGroupsError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Benutzergruppen",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setGroupsLoading(false);
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
}, [fetchGroups]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
fetchGroups();
|
||||
}, [fetchUsers, fetchGroups]);
|
||||
|
||||
const handleSearchChange = useCallback((event) => {
|
||||
setSearchQuery(event.target.value);
|
||||
@@ -195,6 +238,140 @@ export function Users() {
|
||||
}).format(parsed);
|
||||
}, []);
|
||||
|
||||
const resetNewUserForm = useCallback(() => {
|
||||
setNewUsername("");
|
||||
setNewEmail("");
|
||||
setNewPassword("");
|
||||
setNewGroupId("");
|
||||
setNewAvatarColor("");
|
||||
setCreateError("");
|
||||
}, []);
|
||||
|
||||
const handleCreateUser = useCallback(async () => {
|
||||
if (maintenanceActive) {
|
||||
return;
|
||||
}
|
||||
const trimmedUsername = newUsername.trim();
|
||||
if (!trimmedUsername) {
|
||||
setCreateError("Benutzername ist erforderlich.");
|
||||
return;
|
||||
}
|
||||
const sanitizedPassword = typeof newPassword === "string" ? newPassword.trim() : "";
|
||||
if (sanitizedPassword.length < 8) {
|
||||
setCreateError("Passwort muss mindestens 8 Zeichen enthalten.");
|
||||
return;
|
||||
}
|
||||
if (!newGroupId) {
|
||||
setCreateError("Bitte eine globale Rolle auswählen.");
|
||||
return;
|
||||
}
|
||||
setCreatingUser(true);
|
||||
setCreateError("");
|
||||
try {
|
||||
const payload = {
|
||||
username: trimmedUsername,
|
||||
password: sanitizedPassword,
|
||||
groupId: Number(newGroupId)
|
||||
};
|
||||
const trimmedEmail = newEmail.trim();
|
||||
if (trimmedEmail) {
|
||||
payload.email = trimmedEmail;
|
||||
}
|
||||
if (newAvatarColor) {
|
||||
payload.avatarColor = newAvatarColor;
|
||||
}
|
||||
const response = await axios.post("/api/users", payload);
|
||||
resetNewUserForm();
|
||||
fetchUsers();
|
||||
showToast({
|
||||
variant: "success",
|
||||
title: "Benutzer angelegt",
|
||||
description: `Der Benutzer "${response.data?.item?.username ?? trimmedUsername}" wurde erstellt.`
|
||||
});
|
||||
} catch (err) {
|
||||
const errorCode = err.response?.data?.error;
|
||||
let message = "Benutzer konnte nicht angelegt werden.";
|
||||
switch (errorCode) {
|
||||
case "USERNAME_REQUIRED":
|
||||
message = "Benutzername ist erforderlich.";
|
||||
break;
|
||||
case "INVALID_EMAIL":
|
||||
message = "Bitte eine gültige E-Mail-Adresse angeben.";
|
||||
break;
|
||||
case "INVALID_PASSWORD":
|
||||
case "PASSWORD_TOO_SHORT":
|
||||
message = "Passwort muss mindestens 8 Zeichen enthalten.";
|
||||
break;
|
||||
case "INVALID_GROUP_ID":
|
||||
case "GROUP_NOT_FOUND":
|
||||
message = "Bitte eine gültige globale Rolle auswählen.";
|
||||
break;
|
||||
case "USERNAME_TAKEN":
|
||||
message = "Benutzername wird bereits verwendet.";
|
||||
break;
|
||||
case "EMAIL_TAKEN":
|
||||
message = "E-Mail-Adresse wird bereits verwendet.";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
setCreateError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Erstellen fehlgeschlagen",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setCreatingUser(false);
|
||||
}
|
||||
}, [
|
||||
maintenanceActive,
|
||||
newUsername,
|
||||
newEmail,
|
||||
newPassword,
|
||||
newGroupId,
|
||||
newAvatarColor,
|
||||
showToast,
|
||||
resetNewUserForm,
|
||||
fetchUsers
|
||||
]);
|
||||
|
||||
const createDisabled = maintenanceActive || creatingUser || groupsLoading || availableGroups.length === 0;
|
||||
const groupSelectDisabled = maintenanceActive || creatingUser || groupsLoading || availableGroups.length === 0;
|
||||
const avatarSelectDisabled = maintenanceActive || creatingUser;
|
||||
|
||||
const renderSelectedGroup = useCallback(
|
||||
(element) => {
|
||||
if (element && element.props && element.props.children) {
|
||||
return element.props.children;
|
||||
}
|
||||
if (!newGroupId) {
|
||||
return "Bitte auswählen";
|
||||
}
|
||||
const selected = availableGroups.find((group) => String(group.id) === newGroupId);
|
||||
return selected ? selected.name : "Bitte auswählen";
|
||||
},
|
||||
[availableGroups, newGroupId]
|
||||
);
|
||||
|
||||
const renderSelectedAvatar = useCallback(
|
||||
(element) => {
|
||||
if (element && element.props && element.props.children) {
|
||||
return element.props.children;
|
||||
}
|
||||
if (!newAvatarColor) {
|
||||
return "Automatisch (zufällig)";
|
||||
}
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`h-4 w-4 rounded border border-blue-gray-100 ${newAvatarColor}`} />
|
||||
<span className="text-xs">{newAvatarColor}</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
[newAvatarColor]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<Card>
|
||||
@@ -213,6 +390,97 @@ export function Users() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-8 rounded-lg border border-blue-gray-100 bg-white p-4 shadow-sm">
|
||||
<Typography variant="h6" color="blue-gray" className="mb-2">
|
||||
Neuen Benutzer anlegen
|
||||
</Typography>
|
||||
<Typography variant="small" className="text-sm text-stormGrey-500 mb-4">
|
||||
Benutzername, Passwort (mindestens 8 Zeichen) und eine globale Rolle sind Pflichtfelder.
|
||||
</Typography>
|
||||
{createError && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{createError}
|
||||
</div>
|
||||
)}
|
||||
{groupsError && (
|
||||
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{groupsError}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Input
|
||||
label="Benutzername"
|
||||
value={newUsername}
|
||||
onChange={(event) => setNewUsername(event.target.value)}
|
||||
disabled={maintenanceActive || creatingUser}
|
||||
crossOrigin=""
|
||||
/>
|
||||
<Input
|
||||
label="E-Mail (optional)"
|
||||
value={newEmail}
|
||||
onChange={(event) => setNewEmail(event.target.value)}
|
||||
disabled={maintenanceActive || creatingUser}
|
||||
crossOrigin=""
|
||||
/>
|
||||
<Input
|
||||
type="password"
|
||||
label="Passwort"
|
||||
value={newPassword}
|
||||
onChange={(event) => setNewPassword(event.target.value)}
|
||||
disabled={maintenanceActive || creatingUser}
|
||||
crossOrigin=""
|
||||
/>
|
||||
<Select
|
||||
label="Globale Rolle"
|
||||
variant="outlined"
|
||||
value={newGroupId}
|
||||
onChange={setNewGroupId}
|
||||
disabled={groupSelectDisabled}
|
||||
selected={renderSelectedGroup}
|
||||
>
|
||||
<Option value="">Bitte auswählen</Option>
|
||||
{availableGroups.map((group) => (
|
||||
<Option key={group.id} value={String(group.id)}>
|
||||
{group.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label="Avatar-Farbe"
|
||||
variant="outlined"
|
||||
value={newAvatarColor}
|
||||
onChange={setNewAvatarColor}
|
||||
disabled={avatarSelectDisabled}
|
||||
selected={renderSelectedAvatar}
|
||||
>
|
||||
<Option value="">Automatisch (zufällig)</Option>
|
||||
{AVATAR_COLORS.map((color) => (
|
||||
<Option key={color} value={color}>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className={`h-4 w-4 rounded border border-blue-gray-100 ${color}`} />
|
||||
<span className="text-xs">{color}</span>
|
||||
</span>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mt-4 text-sm text-stormGrey-500">
|
||||
{groupsLoading
|
||||
? "Benutzergruppen werden geladen ..."
|
||||
: availableGroups.length === 0
|
||||
? "Noch keine Benutzergruppen vorhanden. Bitte legen Sie zuerst eine globale Rolle an."
|
||||
: ""}
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<Button color="green" onClick={handleCreateUser} disabled={createDisabled}>
|
||||
{creatingUser ? "Speichert ..." : "Benutzer anlegen"}
|
||||
</Button>
|
||||
<Button variant="text" color="blue-gray" onClick={resetNewUserForm} disabled={creatingUser}>
|
||||
Formular zurücksetzen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
|
||||
<div className="flex flex-wrap gap-2"> {/* <div className="flex flex-col items-stretch gap-3 md:flex-row md:items-center">
|
||||
|
||||
Reference in New Issue
Block a user