User, Groups, Logs Update
This commit is contained in:
@@ -1,34 +1,25 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
Typography,
|
||||
Button,
|
||||
Spinner,
|
||||
CardFooter,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Select,
|
||||
Option
|
||||
Option,
|
||||
Input,
|
||||
Switch
|
||||
} from "@material-tailwind/react";
|
||||
import {
|
||||
PencilIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { useToast } from "@/components/ToastProvider.jsx";
|
||||
import { ProfileInfoCard } from "@/widgets/cards";
|
||||
import { platformSettingsData, projectsData } from "@/data";
|
||||
import { platformSettingsData } from "@/data";
|
||||
import { AVATAR_COLORS } from "@/data/avatarColors.js";
|
||||
import { useMaintenance } from "@/components/MaintenanceProvider.jsx";
|
||||
|
||||
const _ = AVATAR_COLORS.join(" ");
|
||||
|
||||
|
||||
const normalizeUserGroups = (rawGroups) => {
|
||||
if (!Array.isArray(rawGroups)) {
|
||||
return [];
|
||||
@@ -67,47 +58,74 @@ const mapUser = (item) => ({
|
||||
groups: normalizeUserGroups(item?.groups)
|
||||
});
|
||||
|
||||
const extractPrimaryGroupId = (user) => {
|
||||
if (!user || !Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const firstValid = user.groups
|
||||
.map((group) => Number(group.id))
|
||||
.find((id) => Number.isFinite(id) && id > 0);
|
||||
return Number.isFinite(firstValid) ? firstValid : null;
|
||||
};
|
||||
|
||||
const buildInitialFormValues = (user) => {
|
||||
if (!user) {
|
||||
return {
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
groupId: null,
|
||||
avatarColor: ""
|
||||
};
|
||||
}
|
||||
|
||||
const primaryGroupId = extractPrimaryGroupId(user);
|
||||
|
||||
return {
|
||||
username: user.username || "",
|
||||
email: user.email || "",
|
||||
password: "",
|
||||
groupId: Number.isFinite(primaryGroupId) ? primaryGroupId : null,
|
||||
avatarColor: user.avatarColor || ""
|
||||
};
|
||||
};
|
||||
|
||||
export function UserDetails() {
|
||||
const { userId } = useParams();
|
||||
const { showToast } = useToast();
|
||||
const { maintenance } = useMaintenance();
|
||||
|
||||
const [user, setUser] = useState(null);
|
||||
const [formValues, setFormValues] = useState(buildInitialFormValues(null));
|
||||
const initialFormValuesRef = useRef(buildInitialFormValues(null));
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [hasLoaded, setHasLoaded] = useState(false);
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
const [groupsLoading, setGroupsLoading] = useState(false);
|
||||
const [groupsError, setGroupsError] = useState("");
|
||||
const [selectedGroupId, setSelectedGroupId] = useState(null);
|
||||
const [savingGroup, setSavingGroup] = useState(false);
|
||||
const [savingUser, setSavingUser] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
|
||||
const maintenanceActive = Boolean(maintenance?.active);
|
||||
const isSuperuserUser = useMemo(() => {
|
||||
if (!Array.isArray(user?.groups)) {
|
||||
return false;
|
||||
}
|
||||
return user.groups.some((group) => (group?.name || "").toLowerCase() === "superuser");
|
||||
}, [user]);
|
||||
|
||||
const numericUserId = useMemo(() => {
|
||||
const asNumber = Number(userId);
|
||||
return Number.isFinite(asNumber) ? asNumber : null;
|
||||
}, [userId]);
|
||||
|
||||
const formatTimestamp = useCallback((value) => {
|
||||
if (!value) {
|
||||
return "–";
|
||||
}
|
||||
const normalized = typeof value === "string" ? value.replace(" ", "T") : value;
|
||||
const parsed = new Date(normalized);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
return value;
|
||||
}
|
||||
return new Intl.DateTimeFormat("de-DE", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short"
|
||||
}).format(parsed);
|
||||
}, []);
|
||||
|
||||
const fetchUserDetails = useCallback(async () => {
|
||||
if (!numericUserId) {
|
||||
setError("Ungültige Benutzer-ID.");
|
||||
setUser(null);
|
||||
setFormValues(buildInitialFormValues(null));
|
||||
initialFormValuesRef.current = buildInitialFormValues(null);
|
||||
setHasLoaded(true);
|
||||
return;
|
||||
}
|
||||
@@ -122,7 +140,10 @@ export function UserDetails() {
|
||||
throw new Error("USER_NOT_FOUND");
|
||||
}
|
||||
setUser(item);
|
||||
setError("");
|
||||
const initialValues = buildInitialFormValues(item);
|
||||
initialFormValuesRef.current = { ...initialValues };
|
||||
setFormValues(initialValues);
|
||||
setSaveError("");
|
||||
} catch (err) {
|
||||
const serverError = err.response?.data?.error;
|
||||
let message = "Benutzerdetails konnten nicht geladen werden.";
|
||||
@@ -136,6 +157,8 @@ export function UserDetails() {
|
||||
}
|
||||
|
||||
setUser(null);
|
||||
initialFormValuesRef.current = buildInitialFormValues(null);
|
||||
setFormValues(buildInitialFormValues(null));
|
||||
setError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
@@ -185,340 +208,415 @@ export function UserDetails() {
|
||||
fetchAvailableGroups();
|
||||
}, [fetchAvailableGroups]);
|
||||
|
||||
const originalGroupId = useMemo(() => {
|
||||
if (!user || !Array.isArray(user.groups) || user.groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const firstValid = user.groups
|
||||
.map((group) => Number(group.id))
|
||||
.find((id) => Number.isFinite(id) && id > 0);
|
||||
return Number.isFinite(firstValid) ? firstValid : null;
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedGroupId(originalGroupId);
|
||||
}, [originalGroupId]);
|
||||
|
||||
const hasGroupChanges = useMemo(() => {
|
||||
if (!hasLoaded) {
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!hasLoaded || !user) {
|
||||
return false;
|
||||
}
|
||||
return originalGroupId !== selectedGroupId;
|
||||
}, [originalGroupId, selectedGroupId, hasLoaded]);
|
||||
|
||||
const initial = initialFormValuesRef.current;
|
||||
if (!initial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialUsername = initial.username || "";
|
||||
const currentUsername = formValues.username || "";
|
||||
|
||||
const initialEmail = initial.email || "";
|
||||
const currentEmail = formValues.email || "";
|
||||
|
||||
const initialGroupId = Number.isFinite(initial.groupId) ? Number(initial.groupId) : null;
|
||||
const currentGroupId = Number.isFinite(formValues.groupId) ? Number(formValues.groupId) : null;
|
||||
|
||||
const initialAvatar = initial.avatarColor || "";
|
||||
const currentAvatar = formValues.avatarColor || "";
|
||||
|
||||
const passwordChanged = Boolean(formValues.password && formValues.password.trim().length > 0);
|
||||
|
||||
return (
|
||||
initialUsername !== currentUsername ||
|
||||
initialEmail !== currentEmail ||
|
||||
(!isSuperuserUser && initialGroupId !== currentGroupId) ||
|
||||
initialAvatar !== currentAvatar ||
|
||||
passwordChanged
|
||||
);
|
||||
}, [formValues, hasLoaded, user, isSuperuserUser]);
|
||||
|
||||
const renderSelectedAvatarLabel = useCallback(
|
||||
(element) => {
|
||||
if (element?.props?.children) {
|
||||
return element.props.children;
|
||||
}
|
||||
if (!formValues.avatarColor) {
|
||||
return "Standardfarbe";
|
||||
}
|
||||
return formValues.avatarColor;
|
||||
},
|
||||
[formValues.avatarColor]
|
||||
);
|
||||
|
||||
const handleUsernameChange = useCallback((event) => {
|
||||
const { value } = event.target;
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
username: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleEmailChange = useCallback((event) => {
|
||||
const { value } = event.target;
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
email: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handlePasswordChange = useCallback((event) => {
|
||||
const { value } = event.target;
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
password: value
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleGroupChange = useCallback((value) => {
|
||||
if (!value) {
|
||||
setSelectedGroupId(null);
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
groupId: null
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const numeric = Number(value);
|
||||
setSelectedGroupId(Number.isFinite(numeric) && numeric > 0 ? numeric : null);
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
groupId: Number.isFinite(numeric) && numeric > 0 ? numeric : null
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleResetGroup = useCallback(() => {
|
||||
setSelectedGroupId(originalGroupId);
|
||||
}, [originalGroupId]);
|
||||
const handleAvatarColorChange = useCallback((value) => {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
avatarColor: value || ""
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleSaveGroup = useCallback(async () => {
|
||||
if (!user || !hasGroupChanges) {
|
||||
const handleSaveUser = useCallback(async () => {
|
||||
if (!user || !hasChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payloadGroups = selectedGroupId ? [selectedGroupId] : [];
|
||||
setSavingUser(true);
|
||||
setSaveError("");
|
||||
|
||||
setSavingGroup(true);
|
||||
setGroupsError("");
|
||||
try {
|
||||
const response = await axios.put(`/api/users/${user.id}/groups`, { groupIds: payloadGroups });
|
||||
const payload = {
|
||||
username: formValues.username,
|
||||
email: formValues.email,
|
||||
password: formValues.password,
|
||||
avatarColor: formValues.avatarColor
|
||||
};
|
||||
|
||||
if (!isSuperuserUser) {
|
||||
payload.groupId = formValues.groupId;
|
||||
}
|
||||
|
||||
const response = await axios.put(`/api/users/${user.id}`, payload);
|
||||
const updatedUser = mapUser(response.data?.item || response.data?.user);
|
||||
setUser(updatedUser);
|
||||
const nextGroupId = Array.isArray(updatedUser.groups) && updatedUser.groups.length > 0
|
||||
? Number(updatedUser.groups[0].id)
|
||||
: null;
|
||||
setSelectedGroupId(Number.isFinite(nextGroupId) && nextGroupId > 0 ? nextGroupId : null);
|
||||
const nextInitial = buildInitialFormValues(updatedUser);
|
||||
initialFormValuesRef.current = { ...nextInitial };
|
||||
setFormValues(nextInitial);
|
||||
setGroupsError("");
|
||||
showToast({
|
||||
variant: "success",
|
||||
title: "Globale Rolle aktualisiert",
|
||||
description: "Die Gruppenzuordnung wurde gespeichert."
|
||||
title: "Benutzer gespeichert",
|
||||
description: "Die Änderungen wurden erfolgreich gespeichert."
|
||||
});
|
||||
} catch (err) {
|
||||
const serverError = err.response?.data?.error;
|
||||
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.";
|
||||
let message = "Die Benutzerdaten konnten nicht gespeichert werden.";
|
||||
|
||||
if (serverError === "USERNAME_REQUIRED") {
|
||||
message = "Bitte einen Benutzernamen angeben.";
|
||||
} else if (serverError === "USERNAME_TAKEN") {
|
||||
message = "Der Benutzername wird bereits verwendet.";
|
||||
} else if (serverError === "INVALID_EMAIL") {
|
||||
message = "Bitte eine gültige E-Mail-Adresse eingeben.";
|
||||
} else if (serverError === "EMAIL_TAKEN") {
|
||||
message = "Die E-Mail-Adresse wird bereits verwendet.";
|
||||
} else if (serverError === "INVALID_PASSWORD") {
|
||||
message = "Das Passwort ist ungültig.";
|
||||
} else if (serverError === "PASSWORD_TOO_SHORT") {
|
||||
message = "Das Passwort muss mindestens 8 Zeichen enthalten.";
|
||||
} else if (serverError === "INVALID_AVATAR_COLOR") {
|
||||
message = "Bitte eine gültige Avatar-Farbe auswählen.";
|
||||
} else if (serverError === "GROUP_NOT_FOUND") {
|
||||
message = "Die ausgewählte Gruppe existiert nicht mehr.";
|
||||
} else if (typeof serverError === "string" && serverError.length > 0) {
|
||||
message = serverError;
|
||||
message = "Die ausgewählte Benutzergruppe existiert nicht mehr.";
|
||||
setGroupsError(message);
|
||||
}
|
||||
setGroupsError(message);
|
||||
|
||||
setSaveError(message);
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: "Globale Rolle",
|
||||
title: "Speichern fehlgeschlagen",
|
||||
description: message
|
||||
});
|
||||
} finally {
|
||||
setSavingGroup(false);
|
||||
setSavingUser(false);
|
||||
}
|
||||
}, [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;
|
||||
}, [user, hasChanges, formValues, showToast, isSuperuserUser]);
|
||||
|
||||
const avatarLabel = useMemo(() => {
|
||||
const source = (user?.username || user?.email || "").trim();
|
||||
const source = (formValues.username || formValues.email || "").trim();
|
||||
if (!source) {
|
||||
return "?";
|
||||
}
|
||||
return source.charAt(0).toUpperCase();
|
||||
}, [user]);
|
||||
}, [formValues.username, formValues.email]);
|
||||
|
||||
const avatarColorClass = useMemo(() => {
|
||||
if (formValues.avatarColor) {
|
||||
return formValues.avatarColor;
|
||||
}
|
||||
return user?.avatarColor || "";
|
||||
}, [formValues.avatarColor, user]);
|
||||
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!Array.isArray(availableGroups)) {
|
||||
return [];
|
||||
}
|
||||
return availableGroups.filter((group) => (group?.name || "").toLowerCase() !== "superuser");
|
||||
}, [availableGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuperuserUser) {
|
||||
return;
|
||||
}
|
||||
const currentGroupId = formValues.groupId ? Number(formValues.groupId) : null;
|
||||
const stillExists = filteredGroups.some((group) => Number(group.id) === currentGroupId);
|
||||
if (!stillExists) {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
groupId: null
|
||||
}));
|
||||
}
|
||||
}, [filteredGroups, formValues.groupId, isSuperuserUser]);
|
||||
|
||||
const selectDisabled = maintenanceActive || savingUser || !user || groupsLoading;
|
||||
const avatarSelectDisabled = maintenanceActive || savingUser || !user;
|
||||
const inputDisabled = maintenanceActive || savingUser || !user;
|
||||
const groupSelectValue = formValues.groupId ? String(formValues.groupId) : "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative mt-8 h-72 w-full overflow-hidden rounded-xl bg-[url('/img/background-image.png')] bg-cover bg-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 items-center justify-between flex-wrap gap-6">
|
||||
<div className="flex items-center gap-6">
|
||||
<div
|
||||
className={`text-black flex h-[74px] w-[74px] items-center justify-center rounded-xl text-3xl font-semibold uppercase shadow-lg shadow-blue-gray-500/40 ${user?.avatarColor}`}
|
||||
aria-label={user?.username || "Benutzeravatar"}
|
||||
>
|
||||
{avatarLabel}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h5" color="blue-gray" className="mb-1">
|
||||
{user?.username}
|
||||
</Typography>
|
||||
<Typography className="text-xs font-semibold uppercase tracking-wide text-stormGrey-400">
|
||||
User-ID: {user?.id || "–"}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 flex flex-col gap-12">
|
||||
{maintenanceActive && (
|
||||
<div className="mb-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
Wartungsmodus aktiv – Änderungen sind deaktiviert.
|
||||
</div>
|
||||
{loading && !user && (
|
||||
<div className="mb-6 flex items-center gap-3 rounded-lg border border-blue-gray-50 bg-blue-gray-50/50 px-4 py-3 text-sm text-blue-gray-500">
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span>Benutzerdaten werden geladen ...</span>
|
||||
)}
|
||||
<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">
|
||||
<div className="flex items-center gap-6">
|
||||
<div
|
||||
className={`text-black flex h-[74px] w-[74px] items-center justify-center rounded-xl text-3xl font-semibold uppercase shadow-lg shadow-blue-gray-500/40 ${avatarColorClass}`}
|
||||
aria-label={formValues.username || "Benutzeravatar"}
|
||||
>
|
||||
{avatarLabel}
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h5" color="blue-gray">
|
||||
{formValues.username || "–"}
|
||||
</Typography>
|
||||
<Typography className="text-xs font-semibold tracking-wide text-stormGrey-400">
|
||||
{formValues.email || "–"}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<Button
|
||||
color="green"
|
||||
className="normal-case"
|
||||
onClick={handleSaveUser}
|
||||
disabled={maintenanceActive || savingUser}
|
||||
>
|
||||
{savingUser ? "Speichert ..." : "Änderungen speichern"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<div className="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="gird-cols-1 mb-12 grid gap-12 px-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" className="mb-3">
|
||||
Platform Settings
|
||||
</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",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{loading && !user && (
|
||||
<div className="mb-6 flex items-center gap-3 rounded-lg border border-blue-gray-50 bg-blue-gray-50/50 px-4 py-3 text-sm text-blue-gray-500">
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span>Benutzerdaten werden geladen ...</span>
|
||||
</div>
|
||||
)}
|
||||
{error && !loading && (
|
||||
<div className="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{saveError && !loading && (
|
||||
<div className="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{saveError}
|
||||
</div>
|
||||
)}
|
||||
<div className="grid-cols-1 mb-12 grid gap-12 px-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" className="mb-4">
|
||||
Allgemeine Einstellungen
|
||||
</Typography>
|
||||
<div className="mb-6">
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Benutzername
|
||||
</Typography>
|
||||
<Input
|
||||
value={formValues.username}
|
||||
onChange={handleUsernameChange}
|
||||
placeholder="Benutzername"
|
||||
disabled={inputDisabled}
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
E-Mail-Adresse
|
||||
</Typography>
|
||||
<Input
|
||||
type="email"
|
||||
value={formValues.email}
|
||||
onChange={handleEmailChange}
|
||||
placeholder="benutzer@example.com"
|
||||
disabled={inputDisabled}
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Neues Passwort
|
||||
</Typography>
|
||||
<Input
|
||||
type="password"
|
||||
value={formValues.password}
|
||||
onChange={handlePasswordChange}
|
||||
placeholder="Passwort setzen"
|
||||
disabled={inputDisabled}
|
||||
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
|
||||
labelProps={{
|
||||
className: "before:content-none after:content-none"
|
||||
}}
|
||||
/>
|
||||
<Typography className="mt-1 text-xs text-blue-gray-400">
|
||||
Das Passwort wird nur geändert, wenn ein neuer Wert eingetragen wird.
|
||||
</Typography>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Globale Rolle
|
||||
</Typography>
|
||||
{groupsError && !isSuperuserUser && (
|
||||
<div className="mb-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{groupsError}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ProfileInfoCard
|
||||
title="Profile Information"
|
||||
description="Hi, I'm Alec Thompson, Decisions: If you can't decide, the answer is no. If two equally difficult paths, choose the one more painful in the short term (pain avoidance is creating an illusion of equality)."
|
||||
details={{
|
||||
"first name": "Alec M. Thompson",
|
||||
mobile: "(44) 123 1234 123",
|
||||
email: "alecthompson@mail.com",
|
||||
location: "USA",
|
||||
social: (
|
||||
<div className="flex items-center gap-4">
|
||||
<i className="fa-brands fa-facebook text-blue-700" />
|
||||
<i className="fa-brands fa-twitter text-blue-400" />
|
||||
<i className="fa-brands fa-instagram text-purple-500" />
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
action={
|
||||
<Tooltip content="Edit Profile">
|
||||
<PencilIcon className="h-4 w-4 cursor-pointer text-blue-gray-500" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
<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">
|
||||
Globale Rolle
|
||||
</Typography>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="text"
|
||||
size="sm"
|
||||
color="blue-gray"
|
||||
className="normal-case"
|
||||
onClick={handleResetGroup}
|
||||
disabled={!hasGroupChanges || savingGroup || !user}
|
||||
>
|
||||
Änderungen verwerfen
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="green"
|
||||
className="normal-case"
|
||||
onClick={handleSaveGroup}
|
||||
disabled={maintenanceActive || savingGroup || !hasGroupChanges || !user}
|
||||
>
|
||||
{savingGroup ? "Speichert ..." : "Speichern"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{maintenanceActive && (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
|
||||
Wartungsmodus aktiv – Änderungen sind deaktiviert.
|
||||
</div>
|
||||
)}
|
||||
{groupsError && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
||||
{groupsError}
|
||||
</div>
|
||||
)}
|
||||
{groupsLoading && availableGroups.length === 0 ? (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-blue-gray-50 bg-blue-gray-50/50 px-4 py-3 text-sm text-blue-gray-500">
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span>Benutzergruppen werden geladen ...</span>
|
||||
</div>
|
||||
) : availableGroups.length === 0 ? (
|
||||
<Typography variant="small" className="text-sm text-stormGrey-500">
|
||||
Es sind noch keine Benutzergruppen vorhanden.
|
||||
</Typography>
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div className="sm:flex-1">
|
||||
)}
|
||||
{isSuperuserUser ? (
|
||||
<Typography className="block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Die Rolle für den Superuser kann nicht geändert werden.
|
||||
</Typography>
|
||||
) : groupsLoading && availableGroups.length === 0 ? (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-blue-gray-50 bg-blue-gray-50/50 px-4 py-3 text-sm text-blue-gray-500">
|
||||
<Spinner className="h-4 w-4" />
|
||||
<span>Benutzergruppen werden geladen ...</span>
|
||||
</div>
|
||||
) : filteredGroups.length === 0 ? (
|
||||
<Typography className="block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Es sind keine weiteren Benutzergruppen vorhanden. Bitte lege zunächst eine neue Gruppe an.
|
||||
</Typography>
|
||||
) : (
|
||||
<Select
|
||||
label="Benutzergruppe wählen"
|
||||
value={selectValue}
|
||||
value={groupSelectValue}
|
||||
onChange={handleGroupChange}
|
||||
disabled={selectDisabled}
|
||||
variant="outlined"
|
||||
>
|
||||
{availableGroups.map((group) => (
|
||||
{filteredGroups.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."}
|
||||
<div className="mb-6">
|
||||
<Typography className="mb-2 block text-xs font-semibold uppercase text-blue-gray-500">
|
||||
Avatar-Farbe
|
||||
</Typography>
|
||||
<Select
|
||||
label="Avatar-Farbe auswählen"
|
||||
variant="outlined"
|
||||
value={formValues.avatarColor}
|
||||
onChange={handleAvatarColorChange}
|
||||
disabled={avatarSelectDisabled}
|
||||
selected={renderSelectedAvatarLabel}
|
||||
>
|
||||
<Option value="">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-4 w-4 rounded border border-blue-gray-100" />
|
||||
<span className="text-xs">Keine</span>
|
||||
</span>
|
||||
</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>
|
||||
<div>
|
||||
<Typography variant="h6" color="blue-gray" className="mb-3">
|
||||
Platform Settings
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
<Typography variant="h6" color="blue-gray" className="mb-2">
|
||||
Projects
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="small"
|
||||
className="font-normal text-blue-gray-500"
|
||||
>
|
||||
Architects design houses
|
||||
</Typography>
|
||||
<div className="mt-6 grid grid-cols-1 gap-12 md:grid-cols-2 xl:grid-cols-4">
|
||||
{projectsData.map(
|
||||
({ img, title, description, tag, route, members }) => (
|
||||
<Card key={title} color="transparent" shadow={false}>
|
||||
<CardHeader
|
||||
floated={false}
|
||||
color="gray"
|
||||
className="mx-0 mt-0 mb-4 h-64 xl:h-40"
|
||||
>
|
||||
<img
|
||||
src={img}
|
||||
alt={title}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardBody className="py-0 px-1">
|
||||
<Typography
|
||||
variant="small"
|
||||
className="font-normal text-blue-gray-500"
|
||||
>
|
||||
{tag}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h5"
|
||||
color="blue-gray"
|
||||
className="mt-1 mb-2"
|
||||
>
|
||||
<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>
|
||||
<Typography
|
||||
variant="small"
|
||||
className="font-normal text-blue-gray-500"
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardBody>
|
||||
<CardFooter className="mt-6 flex items-center justify-between py-0 px-1">
|
||||
<Link to={route}>
|
||||
<Button variant="outlined" size="sm">
|
||||
view project
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
{members.map(({ img, name }, key) => (
|
||||
<Tooltip key={name} content={name}>
|
||||
<Avatar
|
||||
src={img}
|
||||
alt={name}
|
||||
size="xs"
|
||||
variant="circular"
|
||||
className={`cursor-pointer border-2 border-white ${key === 0 ? "" : "-ml-2.5"
|
||||
}`}
|
||||
/>
|
||||
</Tooltip>
|
||||
<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",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
</CardBody>
|
||||
</Card></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user