Benutzer-UI: Suche, Restspalte, Inline-Bestaetigung, Detaildialog
This commit is contained in:
1 parent
5939a7f785
commit
66ca955ff8
1 file changed
+148
-32
+148
-32
@@ -4,29 +4,58 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
getTeam,
|
||||
listUsers,
|
||||
setUserRole,
|
||||
type ManagedUser,
|
||||
type TeamMember,
|
||||
} from "@/lib/api-client";
|
||||
import { formatDays, shortGermanDate } from "@/lib/format";
|
||||
|
||||
const ROLES = ["benutzer", "teamleiter", "admin"] as const;
|
||||
|
||||
export function UsersTab({ selfId }: { selfId: string }) {
|
||||
const [users, setUsers] = useState<ManagedUser[] | null>(null);
|
||||
const [remainingById, setRemainingById] = useState<Record<string, number>>({});
|
||||
const [failed, setFailed] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [role, setRole] = useState<(typeof ROLES)[number]>("benutzer");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [detailsId, setDetailsId] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [teamById, setTeamById] = useState<Record<string, TeamMember>>({});
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
const data = await listUsers();
|
||||
setUsers(data.users);
|
||||
const [usersResult, teamResult] = await Promise.allSettled([
|
||||
listUsers(),
|
||||
getTeam(new Date().getFullYear()),
|
||||
]);
|
||||
if (usersResult.status === "rejected") {
|
||||
throw usersResult.reason;
|
||||
}
|
||||
setUsers(usersResult.value.users);
|
||||
if (teamResult.status === "fulfilled") {
|
||||
setRemainingById(
|
||||
Object.fromEntries(
|
||||
teamResult.value.team.map((member) => [
|
||||
member.id,
|
||||
member.remainingUnits,
|
||||
]),
|
||||
),
|
||||
);
|
||||
setTeamById(
|
||||
Object.fromEntries(
|
||||
teamResult.value.team.map((member) => [member.id, member]),
|
||||
),
|
||||
);
|
||||
}
|
||||
setFailed(false);
|
||||
} catch {
|
||||
setFailed(true);
|
||||
@@ -83,6 +112,7 @@ export function UsersTab({ selfId }: { selfId: string }) {
|
||||
async function remove(id: string) {
|
||||
setMessage(null);
|
||||
setError(null);
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteUser(id);
|
||||
setConfirmDelete(null);
|
||||
@@ -94,6 +124,8 @@ export function UsersTab({ selfId }: { selfId: string }) {
|
||||
? removeError.message
|
||||
: "Löschen fehlgeschlagen.",
|
||||
);
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,23 +146,56 @@ export function UsersTab({ selfId }: { selfId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const needle = query.trim().toLowerCase();
|
||||
const visible =
|
||||
needle === ""
|
||||
? users
|
||||
: users.filter(
|
||||
(user) =>
|
||||
user.name.toLowerCase().includes(needle) ||
|
||||
user.email.toLowerCase().includes(needle),
|
||||
);
|
||||
|
||||
return (
|
||||
<section aria-label="Benutzer">
|
||||
<h2>Benutzer</h2>
|
||||
<table>
|
||||
<label>
|
||||
Suchen
|
||||
<input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Name oder E-Mail"
|
||||
/>
|
||||
</label>
|
||||
<table className="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">E-Mail</th>
|
||||
<th scope="col">Rest</th>
|
||||
<th scope="col">Rolle</th>
|
||||
<th scope="col">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user) => (
|
||||
{visible.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<th scope="row">{user.name}</th>
|
||||
<td>{user.email}</td>
|
||||
<th scope="row">
|
||||
<button
|
||||
type="button"
|
||||
className="link-button"
|
||||
aria-label={`Details zu ${user.name}`}
|
||||
onClick={() => setDetailsId(user.id)}
|
||||
>
|
||||
<strong>{user.name}</strong>
|
||||
</button>
|
||||
<span className="muted-note">{user.email}</span>
|
||||
</th>
|
||||
<td>
|
||||
{remainingById[user.id] !== undefined
|
||||
? formatDays(remainingById[user.id])
|
||||
: "–"}
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
Rolle
|
||||
@@ -148,43 +213,94 @@ export function UsersTab({ selfId }: { selfId: string }) {
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
{user.id !== selfId && confirmDelete !== user.id && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
aria-label={`${user.name} löschen`}
|
||||
onClick={() => setConfirmDelete(user.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
)}
|
||||
{user.id !== selfId &&
|
||||
(confirmDelete === user.id ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger"
|
||||
disabled={deleting}
|
||||
onClick={() => remove(user.id)}
|
||||
>
|
||||
Ja, löschen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
aria-label={`${user.name} löschen`}
|
||||
onClick={() => setConfirmDelete(user.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{confirmDelete && (
|
||||
<div className="confirm-row">
|
||||
<p>Wirklich löschen? Alle Zeitdaten dieses Kontos entfallen.</p>
|
||||
<div>
|
||||
{visible.length === 0 && <p>Keine Treffer.</p>}
|
||||
{detailsId && teamById[detailsId] && (
|
||||
<div className="popup-overlay">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-label={teamById[detailsId].name}
|
||||
className="popup"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-danger"
|
||||
onClick={() => remove(confirmDelete)}
|
||||
className="modal-close"
|
||||
aria-label="Schließen"
|
||||
onClick={() => setDetailsId(null)}
|
||||
>
|
||||
Ja, löschen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-secondary"
|
||||
onClick={() => setConfirmDelete(null)}
|
||||
>
|
||||
Abbrechen
|
||||
✕
|
||||
</button>
|
||||
<h2>{teamById[detailsId].name}</h2>
|
||||
<p>
|
||||
Resturlaub: {formatDays(teamById[detailsId].remainingUnits)}
|
||||
{teamById[detailsId].pendingUnits > 0 &&
|
||||
` (davon ${formatDays(teamById[detailsId].pendingUnits)} beantragt)`}
|
||||
</p>
|
||||
<h3>Buchungen</h3>
|
||||
{teamById[detailsId].requests.length === 0 ? (
|
||||
<p>Keine Buchungen.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{teamById[detailsId].requests.map((request) => (
|
||||
<li key={request.id}>
|
||||
{request.startDate === request.endDate
|
||||
? shortGermanDate(request.startDate)
|
||||
: `${shortGermanDate(request.startDate)} – ${shortGermanDate(request.endDate)}`}{" "}
|
||||
· {formatDays(request.units)} · {request.status}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<h3>Krankmeldungen</h3>
|
||||
{teamById[detailsId].sickDays.length === 0 ? (
|
||||
<p>Keine Krankmeldungen.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{teamById[detailsId].sickDays.map((day) => (
|
||||
<li key={day.date}>
|
||||
{shortGermanDate(day.date)} · {formatDays(day.units)}
|
||||
{day.note ? ` · ${day.note}` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={submit} aria-label="Benutzer anlegen">
|
||||
<form onSubmit={submit} aria-label="Benutzer anlegen" className="form-card">
|
||||
<h3>Benutzer anlegen</h3>
|
||||
<label>
|
||||
Name
|
||||
|
||||
Reference in new issue
Block a user