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:
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user