add basic user management

This commit is contained in:
araemer 2025-11-02 09:09:34 +01:00
parent 09150ba3bb
commit 9e7ad622f9
12 changed files with 673 additions and 35 deletions

View file

@ -0,0 +1,269 @@
import {useEffect, useState} from "react";
import {createUser, fetchCurrentUser, updateUser} from "../../api/points/UserPoint";
import type {UserDto} from "../../api/dtos/UserDto.ts"; // @todo add model and mapper!
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 {Plus, X} from "lucide-react";
import ButtonLink from "../basics/ButtonLink.tsx";
import {getRecipeListUrl} from "../../routes.ts";
/**
* 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 [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [isSaving, setIsSaving] = useState(false);
const isAdmin = currentUser?.role === "admin";
// Load current user and user list (if admin)
useEffect(() => {
loadData().catch(console.error);
}, []);
const loadData = async () => {
const me = await fetchCurrentUser();
setCurrentUser(me);
//if (me.role === "admin") {
// const allUsers = await fetchAllUsers();
// setUsers(allUsers);
//} else {
setUsers([me]);
setSelectedUser(me);
//}
};
/** Handles selecting a user from the list */
const handleSelectUser = (user: UserDto) => {
setSelectedUser({...user});
};
/** Handles saving user (create or update) */
const handleSave = async () => {
if (!selectedUser) return;
setIsSaving(true);
try {
if (!selectedUser.id) {
//@todo check passwords!
const created = await createUser({userData: selectedUser, password});
setUsers([...users, created]);
setSelectedUser(created);
} else {
const updated = await updateUser(selectedUser);
setUsers(users.map(u => (u.id === updated.id ? updated : u)));
setSelectedUser(updated);
}
} finally {
setIsSaving(false);
}
};
/** Opens password modal */
const openPasswordModal = () => {
setPassword("");
setConfirmPassword("");
setIsPasswordModalOpen(true);
};
/** Handles creating a new user (admin only) */
const handleAddUser = () => {
setSelectedUser({
userName: "",
firstName: "",
lastName: "",
email: "",
role: "user",
});
};
return (
<PageContainer>
<ContentBackground>
<StickyHeader>
<div className="flex justify-between items-center">
<h1>Benutzerverwaltung</h1>
<ButtonGroupLayout>
{isAdmin && (
<Button
icon={Plus}
onClick={handleAddUser}
text="Neuer Benutzer"
buttonType={ButtonType.PrimaryButton}
/>
)}
<ButtonLink
icon={X}
to={getRecipeListUrl()}
/>
</ButtonGroupLayout>
</div>
</StickyHeader>
<ContentBody>
<div className="flex flex-col md:flex-row gap-6">
{/* User List */}
<div className="md:w-1/3 border-r border-gray-300">
<h2 className="mb-2 text-lg font-semibold">Benutzer</h2>
<ul>
{users.map((user) => (
<li
key={user.id ?? user.userName}
className={clsx(
"p-2 cursor-pointer rounded hover:bg-gray-200",
selectedUser?.id === user.id && "bg-gray-300 font-semibold"
)}
onClick={() => handleSelectUser(user)}
>
{user.firstName} {user.lastName} ({user.userName})
</li>
))}
</ul>
</div>
{/* Edit Form */}
<div className="md:w-2/3">
{selectedUser ? (
<div>
<h2 className="text-lg font-semibold mb-4">
{selectedUser.id ? "Benutzer bearbeiten" : "Neuen Benutzer anlegen"}
</h2>
<form
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
className="flex flex-col gap-3 max-w-md"
>
{/* @todo create component for laben and input field combination */}
<label>Benutzername</label>
<input
type="text"
placeholder="Benutzername"
value={selectedUser.userName}
onChange={(e) =>
setSelectedUser({...selectedUser, userName: e.target.value})
}
/>
<label>Vorname</label>
<input
type="text"
placeholder="Vorname"
value={selectedUser.firstName ?? ""}
onChange={(e) =>
setSelectedUser({...selectedUser, firstName: e.target.value})
}
/>
<label>Nachname</label>
<input
type="text"
placeholder="Nachname"
value={selectedUser.lastName ?? ""}
onChange={(e) =>
setSelectedUser({...selectedUser, lastName: e.target.value})
}
/>
<label>E-Mail</label>
<input
type="email"
placeholder="E-Mail"
value={selectedUser.email ?? ""}
onChange={(e) =>
setSelectedUser({...selectedUser, email: e.target.value})
}
/>
{isAdmin && (<label>Benutzergruppe</label>)}
{isAdmin && (
// @todo style
<select
className="input-field"
value={selectedUser.role ?? "user"}
onChange={(e) =>
setSelectedUser({...selectedUser, role: e.target.value})
}
>
<option value="user">Benutzer</option>
<option value="admin">Administrator</option>
</select>
)}
{/* Show password field only when creating new user */}
{!selectedUser.id && (<label>Passwort</label>)}
{!selectedUser.id && (
<PasswordField
onPasswordChanged={setPassword}
onKeyDown={() => {
}}
/>
)}
{!selectedUser.id && (<label>Passwort bestätigen</label>)}
{!selectedUser.id && (
<PasswordField
placeholder="Passwort bestätigen"
onPasswordChanged={setConfirmPassword}
onKeyDown={() => {
}}
/>
)}
{/* Change password button for existing user */}
{selectedUser.id && (
<Button
text="Passwort ändern"
onClick={openPasswordModal}
/>
)}
<ButtonGroupLayout>
<Button
text={isSaving ? "Speichern..." : "Speichern"}
onClick={handleSave}
buttonType={ButtonType.PrimaryButton}
/>
<Button
text={"Abbrechen"}
onClick={loadData}
/>
</ButtonGroupLayout>
</form>
</div>
) : (
<p className="text-gray-600">Bitte einen Benutzer auswählen.</p>
)}
</div>
</div>
</ContentBody>
{/* Password Change Modal */}
{isPasswordModalOpen && (
<ChangePasswordModal
userId={selectedUser?.id}
onClose={() => setIsPasswordModalOpen(false)}
/>
)}
</ContentBackground>
</PageContainer>
);
}