217 lines
No EOL
8.1 KiB
TypeScript
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>
|
|
);
|
|
} |