add basic user management
This commit is contained in:
parent
09150ba3bb
commit
9e7ad622f9
12 changed files with 673 additions and 35 deletions
269
frontend/src/components/users/UserManagementPage.tsx
Normal file
269
frontend/src/components/users/UserManagementPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue