recipe-app/frontend/src/components/users/UserManagementPage.tsx
2025-12-07 08:42:56 +01:00

217 lines
No EOL
8.1 KiB
TypeScript

import {useEffect, useState} from "react";
import {createUser, fetchAllUsers, fetchCurrentUser, updateUser,} from "../../api/points/UserPoint";
import type {UserDto} from "../../api/dtos/UserDto";
import ContentBody from "../basics/ContentBody";
import StickyHeader from "../basics/StickyHeader";
import Button from "../basics/Button";
import {ButtonType} from "../basics/BasicButtonDefinitions";
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";
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";
import {UserRole} from "../../api/enums/UserRole.ts";
import PageContentLayout from "../basics/PageContentLayout.tsx";
/**
* UserManagementPage
* -------------------
* Displays a two-column layout:
* - Left: list of all users
* - Right: edit form for selected or new user
*
* Allows:
* - Admins to manage all users (add/edit)
* - Regular users to edit their own profile
* - Password changes via modal
*/
export default function UserManagementPage() {
const [users, setUsers] = useState<UserDto[]>([]);
const [selectedUser, setSelectedUser] = useState<UserDto | null>(null);
const [currentUser, setCurrentUser] = useState<UserDto | null>(null);
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const isAdmin = currentUser?.role === UserRole.ADMIN;
// Load current user and user list (if admin)
useEffect(() => {
loadData();
}, []);
/**
* Load data for user management page
*
* An admin can see all users while a normal user can only see his own user data.
* Initially, the current user is selected.
*/
const loadData = async () => {
try {
const me = await fetchCurrentUser();
setCurrentUser(me);
if (me.role === UserRole.ADMIN) {
const userResponse: UserListResponse = await fetchAllUsers();
// 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);
setSelectedUser(me);
} else {
setUsers([me]);
setSelectedUser(me);
}
} catch (err) {
setError(
err instanceof Error
? err.message
: "Fehler beim Laden der Benutzerdaten"
);
}
};
/** Handles selecting a user from the list */
const handleSelectUser = (user: UserDto) => {
setSelectedUser({...user});
};
/** Handles saving user (create or update) */
const handleSave = async (user: UserDto, password?: string) => {
setIsSaving(true);
setError(null);
try {
if (!user.id) {
// New user
const response: CreateUserResponse = await createUser({
userData: user,
password: password || "",
});
const userDto = response.userData;
if (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
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);
}
};
/** Opens password modal */
const openPasswordModal = () => {
setIsPasswordModalOpen(true);
};
/** Add new empty user */
const handleAddUser = () => {
setSelectedUser({
userName: "",
firstName: "",
lastName: "",
email: "",
role: UserRole.USER,
});
};
return (
<PageContainer>
<PageContentLayout>
<StickyHeader className="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<h1>Benutzerverwaltung</h1>
<ButtonGroupLayout>
{isAdmin && (
<Button
icon={Plus}
onClick={handleAddUser}
text="Neuer Benutzer"
buttonType={ButtonType.PrimaryButton}
/>
)}
{/* Leave user management and return to recipe list. @todo handle unsaved changes?*/}
<ButtonLink icon={X} to={getRecipeListUrl()}/>
</ButtonGroupLayout>
</StickyHeader>
<ContentBody className="p-0">
<div className="flex flex-col md:flex-row min-h-[500px]">
{/* User List - Left side, no padding */}
<div className="md:w-2/5 lg:w-1/3">
<UserList
users={users}
selectedUser={selectedUser}
onSelectUser={handleSelectUser}
/>
</div>
{/* Edit Form - Right side, takes remaining space */}
<div className="md:w-3/5 lg:w-2/3">
{selectedUser ? (
<UserEditForm
user={selectedUser}
isAdmin={isAdmin}
isSaving={isSaving}
onSave={handleSave}
onCancel={loadData}
onOpenPasswordModal={openPasswordModal}
/>
) : (
<div className="px-6 py-4">
<p className="text-gray-600">Bitte einen Benutzer auswählen.</p>
</div>
)}
</div>
</div>
</ContentBody>
{/* Password Change Modal */}
{isPasswordModalOpen && (
<ChangePasswordModal
userId={selectedUser?.id}
onClose={() => setIsPasswordModalOpen(false)}
/>
)}
{/* Error Popup */}
{error && (
<ErrorPopup message={error} onClose={() => setError(null)}/>
)}
</PageContentLayout>
</PageContainer>
);
}