Files
Speisekarte/components/UserManager.tsx
T
florian.wildeandClaude Sonnet 5 0c69a21a85 feat: interner Bereich für Mobilgeräte nutzbar
AppHeader.tsx: Hamburger-Menü unterhalb "sm" mit denselben Links
gestapelt (Vorschläge-Badge-Logik in gemeinsame renderLinks()-Funktion
ausgelagert), Panel schließt automatisch bei Navigation. Bisherige
einzeilige Navigation bleibt ab "sm" unverändert.

WeekEditor.tsx: Mobile Tag-für-Tag-Ansicht mit Tab-Leiste (Mo-Fr)
statt der 6-spaltigen Tabelle, die bei 375px unbenutzbar gequetscht
war. Zellinhalt (Eintragsliste + "hinzufügen"-Flow) in renderCell()
extrahiert und von Tabelle und Karten-Ansicht gemeinsam genutzt, damit
der Bearbeitungs-Zustand nicht dupliziert werden muss. Desktop-Tabelle
unverändert ab "sm".

DishManager/CodeListManager/UserManager/SectionManager/PasskeyManager:
Kopf- und Listenzeilen brechen jetzt bei Bedarf um (flex-wrap) statt
Buttons abzuschneiden, gleiches Muster wie bereits in
SuggestionReview.tsx vorhanden.

Version 1.9.5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 00:54:05 +02:00

263 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import type { Profile, Role } from "@/lib/types";
import { useDict } from "@/components/i18n/LocaleProvider";
interface UserForm {
id: string | null; // null = neuer Benutzer
email: string;
full_name: string;
role: Role;
password: string; // bei Bearbeitung optional (leer = unverändert)
passwordRepeat: string;
}
const EMPTY_FORM: UserForm = {
id: null,
email: "",
full_name: "",
role: "staff",
password: "",
passwordRepeat: "",
};
export default function UserManager({ meId }: { meId: string }) {
const dict = useDict();
const roleLabels: Record<Role, string> = {
admin: dict.common.roleAdmin,
staff: dict.common.roleStaff,
};
const [profiles, setProfiles] = useState<Profile[]>([]);
const [form, setForm] = useState<UserForm | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const res = await fetch("/api/admin/users");
if (!res.ok)
setError(formatMessage(dict.userManager.loadError, { message: String(res.status) }));
else setProfiles(await res.json());
}, [dict]);
useEffect(() => {
load();
}, [load]);
async function callApi(method: string, body: unknown): Promise<boolean> {
setBusy(true);
setError(null);
try {
const res = await fetch("/api/admin/users", {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const data = await res.json().catch(() => null);
setError(data?.error ?? `Fehler (${res.status})`);
return false;
}
return true;
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
return false;
} finally {
setBusy(false);
}
}
async function save(e: React.FormEvent) {
e.preventDefault();
if (!form) return;
if (form.password && form.password !== form.passwordRepeat) {
setError(dict.userManager.passwordMismatch);
return;
}
const ok = form.id
? await callApi("PATCH", {
id: form.id,
full_name: form.full_name,
role: form.role,
password: form.password || undefined,
})
: await callApi("POST", {
email: form.email,
full_name: form.full_name,
role: form.role,
password: form.password,
});
if (ok) {
setForm(null);
await load();
}
}
async function remove(profile: Profile) {
if (
!window.confirm(
formatMessage(dict.userManager.confirmDelete, { email: profile.email })
)
)
return;
if (await callApi("DELETE", { id: profile.id })) await load();
}
return (
<div className="max-w-3xl">
<div className="flex flex-wrap items-center gap-3 mb-4">
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
{dict.userManager.title}
</h1>
<button
type="button"
className="btn btn-add"
onClick={() => setForm(EMPTY_FORM)}
>
{dict.userManager.newUser}
</button>
</div>
{error && (
<p className="mb-3 text-sm text-[var(--c-del)]" role="alert">
{error}
</p>
)}
{form && (
<form onSubmit={save} className="card mb-4 space-y-3">
<h2 className="font-semibold">
{form.id ? dict.userManager.editUser : dict.userManager.newUser}
</h2>
{!form.id && (
<div>
<label className="label" htmlFor="user-email">
{dict.common.email}
</label>
<input
id="user-email"
type="email"
className="input"
value={form.email}
onChange={(e) => setForm({ ...form, email: e.target.value })}
required
/>
</div>
)}
<div>
<label className="label" htmlFor="user-name">
{dict.common.name}
</label>
<input
id="user-name"
className="input"
value={form.full_name}
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
/>
</div>
<div className="max-w-60">
<label className="label" htmlFor="user-role">
{dict.common.role}
</label>
<select
id="user-role"
className="input"
value={form.role}
onChange={(e) =>
setForm({ ...form, role: e.target.value as Role })
}
>
<option value="staff">{dict.common.roleStaff}</option>
<option value="admin">{dict.common.roleAdmin}</option>
</select>
</div>
<div className="max-w-60">
<label className="label" htmlFor="user-password">
{form.id ? dict.userManager.newPasswordLabel : dict.common.password}
</label>
<input
id="user-password"
type="password"
className="input"
value={form.password}
onChange={(e) => setForm({ ...form, password: e.target.value })}
minLength={8}
required={!form.id}
autoComplete="new-password"
/>
</div>
<div className="max-w-60">
<label className="label" htmlFor="user-password-repeat">
{dict.userManager.repeatPasswordLabel}
</label>
<input
id="user-password-repeat"
type="password"
className="input"
value={form.passwordRepeat}
onChange={(e) =>
setForm({ ...form, passwordRepeat: e.target.value })
}
minLength={8}
required={!form.id || !!form.password}
autoComplete="new-password"
/>
</div>
<div className="flex gap-2">
<button type="submit" className="btn btn-add" disabled={busy}>
{dict.common.save}
</button>
<button
type="button"
className="btn btn-neutral"
onClick={() => setForm(null)}
>
{dict.common.cancel}
</button>
</div>
</form>
)}
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
{profiles.map((p) => (
<li key={p.id} className="flex flex-wrap items-center gap-3 px-4 py-2 text-sm">
<span className="mr-auto">
<span className="font-semibold">{p.full_name || p.email}</span>{" "}
<span className="text-gray-500">
– {p.email} – {roleLabels[p.role]}
</span>
{p.id === meId && <span className="text-gray-400"> {dict.common.you}</span>}
</span>
<button
type="button"
className="btn btn-primary"
onClick={() =>
setForm({
id: p.id,
email: p.email,
full_name: p.full_name,
role: p.role,
password: "",
passwordRepeat: "",
})
}
>
{dict.common.edit}
</button>
{p.id !== meId && (
<button
type="button"
className="btn btn-del"
onClick={() => remove(p)}
>
{dict.common.delete}
</button>
)}
</li>
))}
</ul>
</div>
);
}