v1.5: Alle Manager-Komponenten auf eigene API-Routen umgestellt

Neue interne Route Handler unter app/api/ (dishes inkl. Bulk-Delete,
sections, allergens, additives, menu-entries, dish-suggestions,
settings, admin/branding) ersetzen den direkten Browser→Postgrest-
Zugriff der bisherigen Supabase-Anbindung. Jede Route prüft Autorisierung
selbst (lib/auth/session.ts requireRole) statt sich auf Postgres-RLS zu
verlassen – ersetzt 1:1 die bisherigen Policies (öffentlich lesbar /
staff+admin schreiben / nur admin für Einstellungen & Benutzer).

Rezeptvorschlag-Dublettencheck (SuggestionForm) läuft jetzt zwingend
serverseitig im POST-Handler, da es keinen Browser→Postgres-Pfad mehr
gibt. "Passkey annehmen" (SuggestionReview) läuft transaktional
(Gericht anlegen + Vorschlag löschen in einem Schritt).

Logo/Favicon: neuer lokaler Dateispeicher (lib/branding.ts, Docker-
Volume) statt Supabase-Storage-Bucket, ausgeliefert über
app/branding/[filename]. logoUrl()/faviconUrl() liefern jetzt einfache
relative Pfade statt einer fest verdrahteten Supabase-Storage-URL.

DeepL-Route (api/translate) und Benutzerverwaltung (api/admin/users)
laufen jetzt gegen die users-Tabelle statt Supabase Auth.

Alle "use client"-Manager-Komponenten (DishManager, WeekEditor,
SectionManager, CodeListManager, SuggestionForm/Review, UserManager,
DesignEditor) rufen ihre jeweiligen /api/-Routen per fetch() statt den
Supabase-Browser-Client zu nutzen.
This commit is contained in:
2026-07-18 11:01:52 +02:00
parent 28c9ec15c8
commit a9a9cf7ba4
58 changed files with 1184 additions and 1858 deletions
+23 -25
View File
@@ -3,8 +3,7 @@
// Pflege der globalen Allergen-/Zusatzstofflisten, die unterhalb
// der öffentlichen Speisekarte angezeigt werden.
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { CodeItem } from "@/lib/types";
@@ -17,7 +16,6 @@ export default function CodeListManager({
table: "allergens" | "additives";
translateEnabled: boolean;
}) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const title =
@@ -41,10 +39,10 @@ export default function CodeListManager({
);
const load = useCallback(async () => {
const { data, error } = await supabase.from(table).select("*").order("sort");
if (error) setError(formatMessage(dict.codeList.loadError, { message: error.message }));
else setItems(data as CodeItem[]);
}, [supabase, table, dict]);
const res = await fetch(`/api/${table}`);
if (!res.ok) setError(formatMessage(dict.codeList.loadError, { message: String(res.status) }));
else setItems(await res.json());
}, [table, dict]);
useEffect(() => {
load();
@@ -64,23 +62,23 @@ export default function CodeListManager({
name_pl: editing.namePl.trim() || null,
};
let dbError;
if (editing.original) {
({ error: dbError } = await supabase
.from(table)
.update(payload)
.eq("code", editing.original));
} else {
const maxSort = items.reduce((m, i) => Math.max(m, i.sort), 0);
({ error: dbError } = await supabase
.from(table)
.insert({ ...payload, sort: maxSort + 1 }));
}
if (dbError) {
const res = editing.original
? await fetch(`/api/${table}/${editing.original}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
: await fetch(`/api/${table}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(
/duplicate key/i.test(dbError.message)
data.error === "duplicate_code"
? formatMessage(dict.codeList.duplicateCode, { code })
: formatMessage(dict.codeList.saveError, { message: dbError.message })
: formatMessage(dict.codeList.saveError, { message: data.error ?? String(res.status) })
);
return;
}
@@ -95,9 +93,9 @@ export default function CodeListManager({
)
)
return;
const { error } = await supabase.from(table).delete().eq("code", item.code);
if (error)
setError(formatMessage(dict.codeList.deleteError, { message: error.message }));
const res = await fetch(`/api/${table}/${item.code}`, { method: "DELETE" });
if (!res.ok)
setError(formatMessage(dict.codeList.deleteError, { message: String(res.status) }));
await load();
}
+32 -48
View File
@@ -1,21 +1,14 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { formatMessage } from "@/lib/i18n/shared";
import { DEFAULT_COLORS, faviconUrl, logoUrl } from "@/lib/types";
import { useDict } from "@/components/i18n/LocaleProvider";
type AssetKind = "logo" | "favicon";
const ASSET_COLUMN: Record<AssetKind, "logo_path" | "favicon_path"> = {
logo: "logo_path",
favicon: "favicon_path",
};
export default function DesignEditor() {
const supabase = useMemo(() => createClient(), []);
const router = useRouter();
const dict = useDict();
const colorLabels: Record<string, string> = {
@@ -36,19 +29,16 @@ export default function DesignEditor() {
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const { data, error } = await supabase
.from("settings")
.select("colors, logo_path, favicon_path")
.eq("id", 1)
.maybeSingle();
if (error) {
setError(formatMessage(dict.designEditor.loadError, { message: error.message }));
const res = await fetch("/api/settings");
if (!res.ok) {
setError(formatMessage(dict.designEditor.loadError, { message: String(res.status) }));
return;
}
const data = await res.json();
setColors({ ...DEFAULT_COLORS, ...(data?.colors ?? {}) });
setLogoPath(data?.logo_path ?? null);
setFaviconPath(data?.favicon_path ?? null);
}, [supabase, dict]);
}, [dict]);
useEffect(() => {
load();
@@ -58,12 +48,13 @@ export default function DesignEditor() {
setBusy(true);
setError(null);
setInfo(null);
const { error } = await supabase
.from("settings")
.update({ colors })
.eq("id", 1);
if (error)
setError(formatMessage(dict.designEditor.saveError, { message: error.message }));
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ colors }),
});
if (!res.ok)
setError(formatMessage(dict.designEditor.saveError, { message: String(res.status) }));
else {
setInfo(dict.designEditor.colorsSaved);
router.refresh();
@@ -87,28 +78,21 @@ export default function DesignEditor() {
setError(null);
setInfo(null);
const oldPath = kind === "logo" ? logoPath : faviconPath;
const ext = file.name.split(".").pop()?.toLowerCase() ?? "png";
const path = `${kind}-${Date.now()}.${ext}`;
const { error: uploadError } = await supabase.storage
.from("branding")
.upload(path, file, { contentType: file.type });
if (uploadError) {
setError(formatMessage(dict.designEditor.uploadError, { message: uploadError.message }));
const formData = new FormData();
formData.append("kind", kind);
formData.append("file", file);
const res = await fetch("/api/admin/branding", { method: "POST", body: formData });
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(
formatMessage(dict.designEditor.uploadError, {
message: data.error ?? String(res.status),
})
);
setBusy(false);
return;
}
const { error: settingsError } = await supabase
.from("settings")
.update({ [ASSET_COLUMN[kind]]: path })
.eq("id", 1);
if (settingsError) {
setError(formatMessage(dict.designEditor.saveError, { message: settingsError.message }));
setBusy(false);
return;
}
if (oldPath) await supabase.storage.from("branding").remove([oldPath]);
const { path } = await res.json();
if (kind === "logo") setLogoPath(path);
else setFaviconPath(path);
@@ -130,14 +114,14 @@ export default function DesignEditor() {
return;
setBusy(true);
setError(null);
const { error } = await supabase
.from("settings")
.update({ [ASSET_COLUMN[kind]]: null })
.eq("id", 1);
if (error) {
setError(formatMessage(dict.designEditor.saveError, { message: error.message }));
const res = await fetch("/api/admin/branding", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind }),
});
if (!res.ok) {
setError(formatMessage(dict.designEditor.saveError, { message: String(res.status) }));
} else {
await supabase.storage.from("branding").remove([path]);
if (kind === "logo") setLogoPath(null);
else setFaviconPath(null);
setInfo(kind === "logo" ? dict.designEditor.logoRemoved : dict.designEditor.faviconRemoved);
+33 -27
View File
@@ -1,7 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { dishCodes, formatPrice } from "@/lib/menu";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
@@ -34,7 +33,6 @@ export default function DishManager({
}: {
translateEnabled: boolean;
}) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const [dishes, setDishes] = useState<Dish[]>([]);
@@ -58,20 +56,19 @@ export default function DishManager({
const load = useCallback(async () => {
const [dishesRes, allergensRes, additivesRes] = await Promise.all([
supabase.from("dishes").select("*").order("name"),
supabase.from("allergens").select("*").order("sort"),
supabase.from("additives").select("*").order("sort"),
fetch("/api/dishes"),
fetch("/api/allergens"),
fetch("/api/additives"),
]);
const problem = dishesRes.error ?? allergensRes.error ?? additivesRes.error;
if (problem)
setError(formatMessage(dict.dishManager.loadError, { message: problem.message }));
else {
setDishes(dishesRes.data as Dish[]);
setAllergens(allergensRes.data as CodeItem[]);
setAdditives(additivesRes.data as CodeItem[]);
if (!dishesRes.ok || !allergensRes.ok || !additivesRes.ok) {
setError(formatMessage(dict.dishManager.loadError, { message: "load failed" }));
} else {
setDishes(await dishesRes.json());
setAllergens(await allergensRes.json());
setAdditives(await additivesRes.json());
}
setLoading(false);
}, [supabase, dict]);
}, [dict]);
useEffect(() => {
load();
@@ -94,11 +91,19 @@ export default function DishManager({
allergens: form.allergens,
additives: form.additives,
};
const { error } = form.id
? await supabase.from("dishes").update(payload).eq("id", form.id)
: await supabase.from("dishes").insert(payload);
if (error) {
setError(formatMessage(dict.dishManager.saveError, { message: error.message }));
const res = form.id
? await fetch(`/api/dishes/${form.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
: await fetch("/api/dishes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) {
setError(formatMessage(dict.dishManager.saveError, { message: String(res.status) }));
return;
}
setForm(null);
@@ -112,9 +117,9 @@ export default function DishManager({
)
)
return;
const { error } = await supabase.from("dishes").delete().eq("id", dish.id);
if (error)
setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
const res = await fetch(`/api/dishes/${dish.id}`, { method: "DELETE" });
if (!res.ok)
setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected((prev) => {
const next = new Set(prev);
next.delete(dish.id);
@@ -156,12 +161,13 @@ export default function DishManager({
return;
setBulkBusy(true);
setError(null);
const { error } = await supabase
.from("dishes")
.delete()
.in("id", Array.from(selected));
if (error)
setError(formatMessage(dict.dishManager.deleteError, { message: error.message }));
const res = await fetch("/api/dishes/bulk-delete", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: Array.from(selected) }),
});
if (!res.ok)
setError(formatMessage(dict.dishManager.deleteError, { message: String(res.status) }));
setSelected(new Set());
setBulkBusy(false);
await load();
+34 -37
View File
@@ -3,8 +3,7 @@
// Verwaltung der Essenszeiten (Brotzeit, Mittagessen, …):
// anlegen, umbenennen, Uhrzeit ändern, löschen, Reihenfolge anpassen.
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { localized } from "@/lib/types";
import type { Section } from "@/lib/types";
@@ -34,7 +33,6 @@ function splitTimeRange(range: string): { from: string; to: string } {
}
export default function SectionManager() {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const [sections, setSections] = useState<Section[]>([]);
@@ -44,12 +42,12 @@ export default function SectionManager() {
const [busy, setBusy] = useState(false);
const load = useCallback(async () => {
const { data, error } = await supabase.from("sections").select("*").order("sort");
if (error)
setError(formatMessage(dict.sectionManager.loadError, { message: error.message }));
else setSections(data as Section[]);
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);
}, [supabase, dict]);
}, [dict]);
useEffect(() => {
load();
@@ -68,27 +66,19 @@ export default function SectionManager() {
time_range: `${form.timeFrom} - ${form.timeTo}`,
};
let dbError;
if (form.key) {
({ error: dbError } = await supabase
.from("sections")
.update(payload)
.eq("key", form.key));
} else {
// Frisch aus der DB lesen statt den lokalen State zu nutzen, damit
// ein veralteter Snapshot nicht zu doppelten sort-Werten führt.
const { data: maxRow } = await supabase
.from("sections")
.select("sort")
.order("sort", { ascending: false })
.limit(1)
.maybeSingle();
({ error: dbError } = await supabase
.from("sections")
.insert({ ...payload, sort: (maxRow?.sort ?? 0) + 1 }));
}
if (dbError) {
setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
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);
@@ -102,9 +92,9 @@ export default function SectionManager() {
)
)
return;
const { error } = await supabase.from("sections").delete().eq("key", section.key);
if (error)
setError(formatMessage(dict.sectionManager.deleteError, { message: error.message }));
const res = await fetch(`/api/sections/${section.key}`, { method: "DELETE" });
if (!res.ok)
setError(formatMessage(dict.sectionManager.deleteError, { message: String(res.status) }));
await load();
}
@@ -115,12 +105,19 @@ export default function SectionManager() {
setBusy(true);
setError(null);
const [r1, r2] = await Promise.all([
supabase.from("sections").update({ sort: neighbor.sort }).eq("key", section.key),
supabase.from("sections").update({ sort: section.sort }).eq("key", neighbor.key),
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 }),
}),
]);
const dbError = r1.error ?? r2.error;
if (dbError)
setError(formatMessage(dict.sectionManager.saveError, { message: dbError.message }));
if (!r1.ok || !r2.ok)
setError(formatMessage(dict.sectionManager.saveError, { message: "move failed" }));
setBusy(false);
await load();
}
+13 -18
View File
@@ -2,13 +2,11 @@
import { useState } from "react";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import { formatMessage } from "@/lib/i18n/shared";
import { useDict } from "@/components/i18n/LocaleProvider";
import LanguageSwitcher from "@/components/i18n/LanguageSwitcher";
export default function SuggestionForm() {
const supabase = createClient();
const dict = useDict();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
@@ -34,23 +32,20 @@ export default function SuggestionForm() {
}
setBusy(true);
const { data: existing } = await supabase
.from("dishes")
.select("id")
.ilike("name", trimmedName)
.limit(1);
if (existing && existing.length > 0) {
setError(dict.suggestions.duplicate);
setBusy(false);
return;
}
const { error: insertError } = await supabase.from("dish_suggestions").insert({
name: trimmedName,
description: description.trim() || null,
const res = await fetch("/api/dish-suggestions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: trimmedName, description: description.trim() || null }),
});
if (insertError) {
setError(formatMessage(dict.suggestions.genericError, { message: insertError.message }));
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(
data.error === "duplicate"
? dict.suggestions.duplicate
: formatMessage(dict.suggestions.genericError, {
message: data.error ?? String(res.status),
})
);
setBusy(false);
return;
}
+12 -33
View File
@@ -1,13 +1,11 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/client";
import { useCallback, useEffect, useState } from "react";
import { formatMessage } from "@/lib/i18n/shared";
import { useDict } from "@/components/i18n/LocaleProvider";
import type { DishSuggestion } from "@/lib/types";
export default function SuggestionReview() {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const [items, setItems] = useState<DishSuggestion[]>([]);
const [loading, setLoading] = useState(true);
@@ -16,15 +14,12 @@ export default function SuggestionReview() {
const [busyId, setBusyId] = useState<string | null>(null);
const load = useCallback(async () => {
const { data, error } = await supabase
.from("dish_suggestions")
.select("*")
.order("created_at", { ascending: false });
if (error)
setError(formatMessage(dict.suggestions.loadError, { message: error.message }));
else setItems(data as DishSuggestion[]);
const res = await fetch("/api/dish-suggestions");
if (!res.ok)
setError(formatMessage(dict.suggestions.loadError, { message: String(res.status) }));
else setItems(await res.json());
setLoading(false);
}, [supabase, dict]);
}, [dict]);
useEffect(() => {
load();
@@ -34,25 +29,9 @@ export default function SuggestionReview() {
setBusyId(item.id);
setError(null);
setInfo(null);
const { error: insertError } = await supabase.from("dishes").insert({
name: item.name,
price: 0,
});
if (insertError) {
setError(
formatMessage(dict.suggestions.actionError, { message: insertError.message })
);
setBusyId(null);
return;
}
const { error: deleteError } = await supabase
.from("dish_suggestions")
.delete()
.eq("id", item.id);
if (deleteError) {
setError(
formatMessage(dict.suggestions.actionError, { message: deleteError.message })
);
const res = await fetch(`/api/dish-suggestions/${item.id}/accept`, { method: "POST" });
if (!res.ok) {
setError(formatMessage(dict.suggestions.actionError, { message: String(res.status) }));
setBusyId(null);
return;
}
@@ -67,9 +46,9 @@ export default function SuggestionReview() {
setBusyId(item.id);
setError(null);
setInfo(null);
const { error } = await supabase.from("dish_suggestions").delete().eq("id", item.id);
if (error) {
setError(formatMessage(dict.suggestions.actionError, { message: error.message }));
const res = await fetch(`/api/dish-suggestions/${item.id}`, { method: "DELETE" });
if (!res.ok) {
setError(formatMessage(dict.suggestions.actionError, { message: String(res.status) }));
} else {
setInfo(dict.suggestions.rejected);
}
+9 -17
View File
@@ -1,7 +1,6 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { createClient } from "@/lib/supabase/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";
@@ -24,30 +23,23 @@ const EMPTY_FORM: UserForm = {
passwordRepeat: "",
};
export default function UserManager() {
const supabase = useMemo(() => createClient(), []);
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 [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]);
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();
@@ -235,7 +227,7 @@ export default function UserManager() {
<span className="text-gray-500">
– {p.email} – {roleLabels[p.role]}
</span>
{p.id === me && <span className="text-gray-400"> {dict.common.you}</span>}
{p.id === meId && <span className="text-gray-400"> {dict.common.you}</span>}
</span>
<button
type="button"
@@ -253,7 +245,7 @@ export default function UserManager() {
>
{dict.common.edit}
</button>
{p.id !== me && (
{p.id !== meId && (
<button
type="button"
className="btn btn-del"
+25 -31
View File
@@ -1,8 +1,7 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { createClient } from "@/lib/supabase/client";
import {
addDays,
dishCodes,
@@ -18,7 +17,6 @@ import type { Dish, MenuEntry, Section } from "@/lib/types";
import { useDict, useLocale } from "@/components/i18n/LocaleProvider";
export default function WeekEditor({ monday }: { monday: string }) {
const supabase = useMemo(() => createClient(), []);
const dict = useDict();
const locale = useLocale();
const friday = addDays(monday, 4);
@@ -41,28 +39,19 @@ export default function WeekEditor({ monday }: { monday: string }) {
const load = useCallback(async () => {
setError(null);
const [se, di, en] = await Promise.all([
supabase.from("sections").select("*").order("sort"),
supabase.from("dishes").select("*").order("name"),
supabase
.from("menu_entries")
.select(
"id, menu_date, section_key, position, dish:dishes(id, name, name_en, name_pl, price, allergens, additives)"
)
.gte("menu_date", monday)
.lte("menu_date", friday)
.order("position")
.order("created_at"),
fetch("/api/sections"),
fetch("/api/dishes"),
fetch(`/api/menu-entries?from=${monday}&to=${friday}`),
]);
const problem = se.error ?? di.error ?? en.error;
if (problem) {
setError(formatMessage(dict.weekEditor.loadError, { message: problem.message }));
if (!se.ok || !di.ok || !en.ok) {
setError(formatMessage(dict.weekEditor.loadError, { message: "load failed" }));
} else {
setSections(se.data as Section[]);
setDishes(di.data as Dish[]);
setEntries(en.data as unknown as MenuEntry[]);
setSections(await se.json());
setDishes(await di.json());
setEntries(await en.json());
}
setLoading(false);
}, [supabase, monday, friday, dict]);
}, [monday, friday, dict]);
useEffect(() => {
setLoading(true);
@@ -71,16 +60,21 @@ export default function WeekEditor({ monday }: { monday: string }) {
async function addEntry() {
if (!adding || !selectedDish) return;
const { error } = await supabase.from("menu_entries").insert({
menu_date: adding.iso,
section_key: adding.section,
dish_id: selectedDish,
const res = await fetch("/api/menu-entries", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
menu_date: adding.iso,
section_key: adding.section,
dish_id: selectedDish,
}),
});
if (error) {
if (!res.ok) {
const data = await res.json().catch(() => ({}));
setError(
/duplicate key/i.test(error.message)
data.error === "duplicate_entry"
? dict.weekEditor.duplicateEntry
: formatMessage(dict.weekEditor.saveError, { message: error.message })
: formatMessage(dict.weekEditor.saveError, { message: data.error ?? String(res.status) })
);
}
setAdding(null);
@@ -89,9 +83,9 @@ export default function WeekEditor({ monday }: { monday: string }) {
}
async function removeEntry(id: string) {
const { error } = await supabase.from("menu_entries").delete().eq("id", id);
if (error)
setError(formatMessage(dict.weekEditor.deleteError, { message: error.message }));
const res = await fetch(`/api/menu-entries/${id}`, { method: "DELETE" });
if (!res.ok)
setError(formatMessage(dict.weekEditor.deleteError, { message: String(res.status) }));
await load();
}