Files
Speisekarte/components/UserManager.tsx
T
florian.wilde a6165de10c Mehrsprachige Oberfläche + lokalisierte Gericht-/Bereichsnamen
Alle Seiten und Komponenten (öffentliche Speisekarte, Login, Kantinen-
und Admin-Bereich, eigenes Konto) verwenden jetzt die Wörterbücher aus
lib/i18n statt fest codierter deutscher Texte. Gerichte/Bereiche/
Allergene/Zusatzstoffe zeigen den Namen zur aktuell gewählten Sprache;
Gerichte- und Kennzeichnungs-Formulare bekommen Englisch-/Polnisch-
Felder mit Übersetzen-Button (siehe vorherigen Commit). Datumsformat-
ierung (formatDateLong) nimmt jetzt ein locale-Argument statt fix
'de-DE'.
2026-07-18 02:08:03 +02:00

271 lines
8.1 KiB
TypeScript
Raw Permalink 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, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
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() {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const roleLabels: Record<Role, string> = {
admin: dict.common.roleAdmin,
staff: dict.common.roleStaff,
};
const [profiles, setProfiles] = useState<Profile[]>([]);
const [me, setMe] = useState<string | null>(null);
const [form, setForm] = useState<UserForm | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const [{ data: profileData, error: profileError }, { data: userData }] =
await Promise.all([
supabase.from("profiles").select("*").order("created_at"),
supabase.auth.getUser(),
]);
if (profileError)
setError(formatMessage(dict.userManager.loadError, { message: profileError.message }));
else setProfiles(profileData as Profile[]);
setMe(userData.user?.id ?? null);
}, [supabase, 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 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 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 === me && <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 !== me && (
<button
type="button"
className="btn btn-del"
onClick={() => remove(p)}
>
{dict.common.delete}
</button>
)}
</li>
))}
</ul>
</div>
);
}