diff --git a/frontend/src/components/basics/ErrorPopup.tsx b/frontend/src/components/basics/ErrorPopup.tsx new file mode 100644 index 0000000..1fc93ec --- /dev/null +++ b/frontend/src/components/basics/ErrorPopup.tsx @@ -0,0 +1,41 @@ +import {AlertCircle, X} from "lucide-react"; + +type ErrorPopupProps = { + message: string; + onClose: () => void; +}; + +/** + * ErrorPopup - Displays error messages in a modal overlay + */ +export default function ErrorPopup({message, onClose}: ErrorPopupProps) { + return ( +
+
+
+
+ +

Fehler

+
+ +
+ +

{message}

+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/users/UserEditForm.tsx b/frontend/src/components/users/UserEditForm.tsx new file mode 100644 index 0000000..6ca0f30 --- /dev/null +++ b/frontend/src/components/users/UserEditForm.tsx @@ -0,0 +1,160 @@ +import {useState} from "react"; +import type {UserDto} from "../../api/dtos/UserDto"; +import Button from "../basics/Button"; +import {ButtonType} from "../basics/BasicButtonDefinitions"; +import PasswordField from "../basics/PasswordField"; +import ButtonGroupLayout from "../basics/ButtonGroupLayout"; +import TextLinkButton from "../basics/TextLinkButton"; +import SelectField from "../basics/SelectField"; + +type UserEditFormProps = { + user: UserDto; + isAdmin: boolean; + isSaving: boolean; + onSave: (user: UserDto, password?: string) => Promise; + onCancel: () => void; + onOpenPasswordModal: () => void; +}; + +/** + * UserEditForm - Form for creating/editing a user + */ +export default function UserEditForm({ + user, + isAdmin, + isSaving, + onSave, + onCancel, + onOpenPasswordModal, + }: UserEditFormProps) { + const [editedUser, setEditedUser] = useState({...user}); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [passwordError, setPasswordError] = useState(""); + + const handleSave = async () => { + if (!editedUser.id) { + // New user - validate passwords + if (confirmPassword !== password) { + setPasswordError("Passwörter stimmen nicht überein"); + return; + } + if (!password || password.length === 0) { + setPasswordError("Passwort ist erforderlich"); + return; + } + setPasswordError(""); + await onSave(editedUser, password); + } else { + // Existing user + await onSave(editedUser); + } + }; + + // @todo API string + const roleOptions = [ + {value: "user", label: "Benutzer"}, + {value: "admin", label: "Administrator"}, + ]; + + return ( +
+

{editedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"}

+
+ + + setEditedUser({...editedUser, userName: e.target.value}) + } + /> + + + + setEditedUser({...editedUser, firstName: e.target.value}) + } + /> + + + + setEditedUser({...editedUser, lastName: e.target.value}) + } + /> + + + + setEditedUser({...editedUser, email: e.target.value}) + } + /> + + {isAdmin && ( + <> + + + setEditedUser({...editedUser, role: value}) + } + options={roleOptions} + /> + + )} + + {/* Show password field only when creating new user */} + {!editedUser.id && ( + <> + + { + }} + /> + + + { + }} + /> + {passwordError &&

{passwordError}

} + + )} + + {/* Change password link for existing user */} + {editedUser.id && ( +
+ +
+ )} + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/users/UserList.tsx b/frontend/src/components/users/UserList.tsx new file mode 100644 index 0000000..c888a0a --- /dev/null +++ b/frontend/src/components/users/UserList.tsx @@ -0,0 +1,48 @@ +import clsx from "clsx"; +import type {UserDto} from "../../api/dtos/UserDto"; + +type UserListProps = { + users: UserDto[]; + selectedUser: UserDto | null; + onSelectUser: (user: UserDto) => void; +}; + +/** + * UserList - Displays a list of users with selection + */ +export default function UserList({users, selectedUser, onSelectUser}: UserListProps) { + const formatUserName = (user: UserDto) => { + const parts = []; + if (user.lastName) parts.push(user.lastName); + if (user.firstName) parts.push(user.firstName); + return parts.length > 0 ? parts.join(", ") : ""; + }; + + return ( +
+

Benutzer

+
    + {users.map((user) => ( +
  • onSelectUser(user)} + > +
    + {formatUserName(user)} +
    +
    + {user.userName} +
    +
  • + ))} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/users/UserManagementPage.tsx b/frontend/src/components/users/UserManagementPage.tsx index 6e230b4..6c7c72f 100644 --- a/frontend/src/components/users/UserManagementPage.tsx +++ b/frontend/src/components/users/UserManagementPage.tsx @@ -1,29 +1,28 @@ import {useEffect, useState} from "react"; -import {createUser, fetchAllUsers, fetchCurrentUser, updateUser} from "../../api/points/UserPoint"; -import type {UserDto} from "../../api/dtos/UserDto.ts"; +import {createUser, fetchAllUsers, fetchCurrentUser, updateUser,} from "../../api/points/UserPoint"; +import type {UserDto} from "../../api/dtos/UserDto"; import ContentBackground from "../basics/ContentBackground"; import ContentBody from "../basics/ContentBody"; import StickyHeader from "../basics/StickyHeader"; import Button from "../basics/Button"; import {ButtonType} from "../basics/BasicButtonDefinitions"; -import clsx from "clsx"; -import PageContainer from "../basics/PageContainer.tsx"; -import {ChangePasswordModal} from "./ChangePasswordModal.tsx"; -import PasswordField from "../basics/PasswordField.tsx"; -import ButtonGroupLayout from "../basics/ButtonGroupLayout.tsx"; +import PageContainer from "../basics/PageContainer"; +import {ChangePasswordModal} from "./ChangePasswordModal"; +import ButtonGroupLayout from "../basics/ButtonGroupLayout"; import {Plus, X} from "lucide-react"; -import ButtonLink from "../basics/ButtonLink.tsx"; -import {getRecipeListUrl} from "../../routes.ts"; -import TextLinkButton from "../basics/TextLinkButton.tsx"; -import SelectField from "../basics/SelectField.tsx"; -import type {CreateUserResponse} from "../../api/dtos/CreateUserResponse.ts"; -import type {UserListResponse} from "../../api/dtos/UserListResponse.ts"; +import ButtonLink from "../basics/ButtonLink"; +import {getRecipeListUrl} from "../../routes"; +import type {CreateUserResponse} from "../../api/dtos/CreateUserResponse"; +import type {UserListResponse} from "../../api/dtos/UserListResponse"; +import UserList from "./UserList"; +import UserEditForm from "./UserEditForm"; +import ErrorPopup from "../basics/ErrorPopup"; /** * UserManagementPage * ------------------- * Displays a two-column layout: - * - Left: list of all users (wider on desktop) + * - Left: list of all users * - Right: edit form for selected or new user * * Allows: @@ -36,43 +35,49 @@ export default function UserManagementPage() { const [selectedUser, setSelectedUser] = useState(null); const [currentUser, setCurrentUser] = useState(null); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); - const [password, setPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - const [passwordError, setPasswordError] = useState(""); const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); - const isAdmin = currentUser?.role === "admin"; + //@todo API enum + const adminRole: string = "admin"; + const isAdmin = currentUser?.role === adminRole; // Load current user and user list (if admin) useEffect(() => { - loadData().catch(console.error); + loadData(); }, []); const loadData = async () => { - const me = await fetchCurrentUser(); - setCurrentUser(me); + try { + const me = await fetchCurrentUser(); + setCurrentUser(me); - if (me.role === "admin") { - const userResponse: UserListResponse = await fetchAllUsers(); + if (me.role === adminRole) { + const userResponse: UserListResponse = await fetchAllUsers(); - // Sort users alphabetically by last name, then first name - const sortedUsers = userResponse.valueList.sort((a, b) => { - // Compare last names first - const lastNameCompare = (a.lastName || "").localeCompare(b.lastName || ""); - if (lastNameCompare !== 0) { - return lastNameCompare; - } - // If last names are equal, compare first names - return (a.firstName || "").localeCompare(b.firstName || ""); - }); + // Sort users alphabetically by last name, then first name + const sortedUsers = userResponse.valueList.sort((a, b) => { + const lastNameCompare = (a.lastName || "").localeCompare( + b.lastName || "" + ); + if (lastNameCompare !== 0) { + return lastNameCompare; + } + return (a.firstName || "").localeCompare(b.firstName || ""); + }); - setUsers(sortedUsers); - - // Pre-select current user (admin's own profile) - setSelectedUser(me); - } else { - setUsers([me]); - setSelectedUser(me); + setUsers(sortedUsers); + setSelectedUser(me); + } else { + setUsers([me]); + setSelectedUser(me); + } + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Fehler beim Laden der Benutzerdaten" + ); } }; @@ -82,30 +87,42 @@ export default function UserManagementPage() { }; /** Handles saving user (create or update) */ - const handleSave = async () => { - if (!selectedUser) return; + const handleSave = async (user: UserDto, password?: string) => { setIsSaving(true); + setError(null); try { - if (!selectedUser.id) { - // New user - check passwords and save - if (confirmPassword !== password) { - setPasswordError("Passwords do not match"); - return; - } - const response: CreateUserResponse = await createUser({userData: selectedUser, password}); + if (!user.id) { + // New user + const response: CreateUserResponse = await createUser({ + userData: user, + password: password || "", + }); const userDto = response.userData; if (userDto) { - // add user to list and slect - setUsers([...users, userDto]); + const newUsers = [...users, userDto].sort((a, b) => { + const lastNameCompare = (a.lastName || "").localeCompare( + b.lastName || "" + ); + if (lastNameCompare !== 0) { + return lastNameCompare; + } + return (a.firstName || "").localeCompare(b.firstName || ""); + }); + setUsers(newUsers); setSelectedUser(userDto); } } else { - // existing user - update - const updated = await updateUser(selectedUser); - // update user data in user list and select correct user - setUsers(users.map(u => (u.id === updated.id ? updated : u))); + // Existing user + const updated = await updateUser(user); + setUsers(users.map((u) => (u.id === updated.id ? updated : u))); setSelectedUser(updated); } + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Fehler beim Speichern der Benutzerdaten" + ); } finally { setIsSaving(false); } @@ -113,8 +130,6 @@ export default function UserManagementPage() { /** Opens password modal */ const openPasswordModal = () => { - setPassword(""); - setConfirmPassword(""); setIsPasswordModalOpen(true); }; @@ -129,16 +144,10 @@ export default function UserManagementPage() { }); }; - // @todo API enum! - const roleOptions = [ - {value: "user", label: "Benutzer"}, - {value: "admin", label: "Administrator"} - ]; - return ( - +

Benutzerverwaltung

{isAdmin && ( @@ -149,141 +158,36 @@ export default function UserManagementPage() { buttonType={ButtonType.PrimaryButton} /> )} - +
- -
- {/* User List - Wider on desktop, attached to left */} -
-

Benutzer

-
    - {users.map((user) => ( -
  • handleSelectUser(user)} - > - {user.lastName}, {user.firstName} ({user.userName}) -
  • - ))} -
+ +
+ {/* User List - Left side, no padding */} +
+
- {/* Edit Form */} -
+ {/* Edit Form - Right side, takes remaining space */} +
{selectedUser ? ( -
-

- {selectedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"} -

-
- - - setSelectedUser({...selectedUser, userName: e.target.value}) - } - /> - - - - setSelectedUser({...selectedUser, firstName: e.target.value}) - } - /> - - - - setSelectedUser({...selectedUser, lastName: e.target.value}) - } - /> - - - - setSelectedUser({...selectedUser, email: e.target.value}) - } - /> - - {isAdmin && ( - <> - - - setSelectedUser({...selectedUser, role: value}) - } - options={roleOptions} - /> - - )} - - {/* Show password field only when creating new user */} - {!selectedUser.id && ( - <> - - { - }} - /> - - - { - }} - /> - {passwordError &&

{passwordError}

} - - )} - - {/* Change password link for existing user */} - {selectedUser.id && ( -
- -
- )} - - -
-
+ ) : ( -

Bitte einen Benutzer auswählen.

+
+

Bitte einen Benutzer auswählen.

+
)}
@@ -296,6 +200,11 @@ export default function UserManagementPage() { onClose={() => setIsPasswordModalOpen(false)} /> )} + + {/* Error Popup */} + {error && ( + setError(null)}/> + )} );