Gerichtnamen, Essenszeiten und Allergen-/Zusatzstoff-Bezeichnungen sind jetzt genauso dynamisch mehrsprachig wie die Bedienoberfläche aus v1.10.0, statt fest auf Englisch/Polnisch begrenzt zu sein. Neue Übersetzungstabellen ersetzen die alten name_en/name_pl-Spalten, DeepL-Auto-Übersetzung und CSV-Import/-Export unterstützen jetzt beliebig viele aktive Sprachen.
291 lines
9.5 KiB
TypeScript
291 lines
9.5 KiB
TypeScript
"use client";
|
||
|
||
// Verwaltung der Essenszeiten (Brotzeit, Mittagessen, …):
|
||
// anlegen, umbenennen, Uhrzeit ändern, löschen, Reihenfolge anpassen.
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import { formatMessage } from "@/lib/i18n/shared";
|
||
import { localized } from "@/lib/types";
|
||
import type { Section } from "@/lib/types";
|
||
import { useDict, useLanguages, useLocale } from "@/components/i18n/LocaleProvider";
|
||
|
||
interface SectionForm {
|
||
key: string | null; // null = neue Essenszeit
|
||
label: string;
|
||
translations: Record<string, string>;
|
||
timeFrom: string;
|
||
timeTo: string;
|
||
}
|
||
|
||
const EMPTY_FORM: SectionForm = {
|
||
key: null,
|
||
label: "",
|
||
translations: {},
|
||
timeFrom: "",
|
||
timeTo: "",
|
||
};
|
||
|
||
function splitTimeRange(range: string): { from: string; to: string } {
|
||
const [from, to] = range.split("-").map((s) => s.trim());
|
||
return { from: from ?? "", to: to ?? "" };
|
||
}
|
||
|
||
export default function SectionManager() {
|
||
const dict = useDict();
|
||
const locale = useLocale();
|
||
const languages = useLanguages().filter((l) => l.code !== "de");
|
||
const [sections, setSections] = useState<Section[]>([]);
|
||
const [form, setForm] = useState<SectionForm | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
const res = await fetch("/api/sections");
|
||
if (!res.ok)
|
||
setError(formatMessage(dict.sectionManager.loadError, { message: String(res.status) }));
|
||
else setSections(await res.json());
|
||
setLoading(false);
|
||
}, [dict]);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
}, [load]);
|
||
|
||
async function save(e: React.FormEvent) {
|
||
e.preventDefault();
|
||
if (!form) return;
|
||
setError(null);
|
||
const label = form.label.trim();
|
||
if (!label || !form.timeFrom || !form.timeTo) return;
|
||
const payload = {
|
||
label,
|
||
time_range: `${form.timeFrom} - ${form.timeTo}`,
|
||
translations: form.translations,
|
||
};
|
||
|
||
const res = form.key
|
||
? await fetch(`/api/sections/${form.key}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
})
|
||
: await fetch("/api/sections", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) {
|
||
setError(formatMessage(dict.sectionManager.saveError, { message: String(res.status) }));
|
||
return;
|
||
}
|
||
setForm(null);
|
||
await load();
|
||
}
|
||
|
||
async function remove(section: Section) {
|
||
if (
|
||
!window.confirm(
|
||
formatMessage(dict.sectionManager.confirmDelete, { name: section.label })
|
||
)
|
||
)
|
||
return;
|
||
const res = await fetch(`/api/sections/${section.key}`, { method: "DELETE" });
|
||
if (!res.ok)
|
||
setError(formatMessage(dict.sectionManager.deleteError, { message: String(res.status) }));
|
||
await load();
|
||
}
|
||
|
||
async function move(section: Section, direction: -1 | 1) {
|
||
const idx = sections.findIndex((s) => s.key === section.key);
|
||
const neighbor = sections[idx + direction];
|
||
if (!neighbor) return;
|
||
setBusy(true);
|
||
setError(null);
|
||
const [r1, r2] = await Promise.all([
|
||
fetch(`/api/sections/${section.key}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ sort: neighbor.sort }),
|
||
}),
|
||
fetch(`/api/sections/${neighbor.key}`, {
|
||
method: "PATCH",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ sort: section.sort }),
|
||
}),
|
||
]);
|
||
if (!r1.ok || !r2.ok)
|
||
setError(formatMessage(dict.sectionManager.saveError, { message: "move failed" }));
|
||
setBusy(false);
|
||
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.sectionManager.title}
|
||
</h1>
|
||
<button
|
||
type="button"
|
||
className="btn btn-add"
|
||
onClick={() => setForm(EMPTY_FORM)}
|
||
>
|
||
{dict.sectionManager.newSection}
|
||
</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-4">
|
||
<h2 className="font-semibold">
|
||
{form.key ? dict.sectionManager.editSection : dict.sectionManager.newSection}
|
||
</h2>
|
||
<div>
|
||
<label className="label" htmlFor="section-label">
|
||
{dict.sectionManager.nameLabel}
|
||
</label>
|
||
<input
|
||
id="section-label"
|
||
className="input"
|
||
value={form.label}
|
||
onChange={(e) => setForm({ ...form, label: e.target.value })}
|
||
required
|
||
/>
|
||
</div>
|
||
{languages.length > 0 && (
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
{languages.map((lang) => (
|
||
<div key={lang.code}>
|
||
<label className="label" htmlFor={`section-label-${lang.code}`}>
|
||
{lang.label}
|
||
</label>
|
||
<input
|
||
id={`section-label-${lang.code}`}
|
||
className="input"
|
||
value={form.translations[lang.code] ?? ""}
|
||
onChange={(e) =>
|
||
setForm({
|
||
...form,
|
||
translations: { ...form.translations, [lang.code]: e.target.value },
|
||
})
|
||
}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
<div className="flex gap-3">
|
||
<div className="max-w-32">
|
||
<label className="label" htmlFor="section-time-from">
|
||
{dict.sectionManager.timeFrom}
|
||
</label>
|
||
<input
|
||
id="section-time-from"
|
||
type="time"
|
||
className="input"
|
||
value={form.timeFrom}
|
||
onChange={(e) => setForm({ ...form, timeFrom: e.target.value })}
|
||
required
|
||
/>
|
||
</div>
|
||
<div className="max-w-32">
|
||
<label className="label" htmlFor="section-time-to">
|
||
{dict.sectionManager.timeTo}
|
||
</label>
|
||
<input
|
||
id="section-time-to"
|
||
type="time"
|
||
className="input"
|
||
value={form.timeTo}
|
||
onChange={(e) => setForm({ ...form, timeTo: e.target.value })}
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button type="submit" className="btn btn-add">
|
||
{dict.common.save}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-neutral"
|
||
onClick={() => setForm(null)}
|
||
>
|
||
{dict.common.cancel}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
)}
|
||
|
||
{loading ? (
|
||
<p className="text-gray-500">{dict.common.loading}</p>
|
||
) : (
|
||
<ul className="divide-y divide-gray-200 border border-gray-200 rounded-md bg-white">
|
||
{sections.map((section, idx) => (
|
||
<li key={section.key} className="flex flex-wrap items-center gap-3 px-4 py-2">
|
||
<div className="flex flex-col">
|
||
<button
|
||
type="button"
|
||
className="text-gray-400 hover:text-[var(--c-primary)] disabled:opacity-30"
|
||
onClick={() => move(section, -1)}
|
||
disabled={busy || idx === 0}
|
||
aria-label={dict.sectionManager.moveUp}
|
||
title={dict.sectionManager.moveUp}
|
||
>
|
||
▲
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="text-gray-400 hover:text-[var(--c-primary)] disabled:opacity-30"
|
||
onClick={() => move(section, 1)}
|
||
disabled={busy || idx === sections.length - 1}
|
||
aria-label={dict.sectionManager.moveDown}
|
||
title={dict.sectionManager.moveDown}
|
||
>
|
||
▼
|
||
</button>
|
||
</div>
|
||
<span className="mr-auto">
|
||
<span className="font-semibold">{localized(section, locale)}</span>{" "}
|
||
<span className="text-gray-500">– {section.time_range}</span>
|
||
</span>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={() => {
|
||
const { from, to } = splitTimeRange(section.time_range);
|
||
setForm({
|
||
key: section.key,
|
||
label: section.label,
|
||
translations: { ...section.translations },
|
||
timeFrom: from,
|
||
timeTo: to,
|
||
});
|
||
}}
|
||
>
|
||
{dict.common.edit}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-del"
|
||
onClick={() => remove(section)}
|
||
>
|
||
{dict.common.delete}
|
||
</button>
|
||
</li>
|
||
))}
|
||
{sections.length === 0 && (
|
||
<li className="px-4 py-3 text-gray-500">{dict.sectionManager.empty}</li>
|
||
)}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|