Kantinen-Bereich: Wochenplan-Editor, Gerichte, Kennzeichnungen, Passkey-Verwaltung
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { createClient } from "@/lib/supabase/client";
|
||||
import { addDays, formatDateLong, formatPrice, WEEKDAYS } from "@/lib/menu";
|
||||
import type { Dish, MenuEntry, Section } from "@/lib/types";
|
||||
|
||||
export default function WeekEditor({ monday }: { monday: string }) {
|
||||
const supabase = useMemo(() => createClient(), []);
|
||||
const friday = addDays(monday, 4);
|
||||
const days = WEEKDAYS.map((weekday, i) => ({
|
||||
weekday,
|
||||
iso: addDays(monday, i),
|
||||
}));
|
||||
|
||||
const [sections, setSections] = useState<Section[]>([]);
|
||||
const [dishes, setDishes] = useState<Dish[]>([]);
|
||||
const [entries, setEntries] = useState<MenuEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// Zelle, in der gerade ein Gericht hinzugefügt wird
|
||||
const [adding, setAdding] = useState<{ iso: string; section: string } | null>(
|
||||
null
|
||||
);
|
||||
const [selectedDish, setSelectedDish] = useState("");
|
||||
|
||||
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, price)")
|
||||
.gte("menu_date", monday)
|
||||
.lte("menu_date", friday)
|
||||
.order("position")
|
||||
.order("created_at"),
|
||||
]);
|
||||
const problem = se.error ?? di.error ?? en.error;
|
||||
if (problem) {
|
||||
setError(`Laden fehlgeschlagen: ${problem.message}`);
|
||||
} else {
|
||||
setSections(se.data as Section[]);
|
||||
setDishes(di.data as Dish[]);
|
||||
setEntries(en.data as unknown as MenuEntry[]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [supabase, monday, friday]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
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,
|
||||
});
|
||||
if (error) {
|
||||
setError(
|
||||
/duplicate key/i.test(error.message)
|
||||
? "Dieses Gericht ist an diesem Tag bereits zugewiesen."
|
||||
: `Speichern fehlgeschlagen: ${error.message}`
|
||||
);
|
||||
}
|
||||
setAdding(null);
|
||||
setSelectedDish("");
|
||||
await load();
|
||||
}
|
||||
|
||||
async function removeEntry(id: string) {
|
||||
const { error } = await supabase.from("menu_entries").delete().eq("id", id);
|
||||
if (error) setError(`Löschen fehlgeschlagen: ${error.message}`);
|
||||
await load();
|
||||
}
|
||||
|
||||
const cell = (iso: string, sectionKey: string) =>
|
||||
entries.filter((e) => e.menu_date === iso && e.section_key === sectionKey);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<h1 className="text-xl font-bold text-[var(--c-primary)] mr-auto">
|
||||
Wochenplan
|
||||
</h1>
|
||||
<Link href={`/intern?woche=${addDays(monday, -7)}`} className="btn btn-add">
|
||||
← Vorherige Woche
|
||||
</Link>
|
||||
<Link href={`/intern?woche=${addDays(monday, 7)}`} className="btn btn-add">
|
||||
Nächste Woche →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="mb-3 text-sm text-[var(--c-del)]" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
{loading ? (
|
||||
<p className="text-gray-500">Lade Wochenplan…</p>
|
||||
) : (
|
||||
<table className="menu-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="section-head w-52" aria-hidden />
|
||||
{days.map((d) => (
|
||||
<th key={d.iso} className="day-head">
|
||||
<span className="font-bold">{d.weekday}</span>
|
||||
<br />
|
||||
{formatDateLong(d.iso)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sections.map((s) => (
|
||||
<tr key={s.key}>
|
||||
<th className="section-head">
|
||||
<div>{s.label}</div>
|
||||
<div className="time">{s.time_range}</div>
|
||||
</th>
|
||||
{days.map((d) => {
|
||||
const isAdding =
|
||||
adding?.iso === d.iso && adding?.section === s.key;
|
||||
return (
|
||||
<td key={d.iso} className="cell">
|
||||
{cell(d.iso, s.key).map((e) => (
|
||||
<div key={e.id} className="dish">
|
||||
<span className="font-bold">{e.dish.name}</span>
|
||||
{" – "}
|
||||
{formatPrice(e.dish.price)}
|
||||
<div className="mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-del"
|
||||
onClick={() => removeEntry(e.id)}
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isAdding ? (
|
||||
<div className="mt-2 space-y-1 text-left">
|
||||
<select
|
||||
className="input"
|
||||
value={selectedDish}
|
||||
onChange={(e) => setSelectedDish(e.target.value)}
|
||||
>
|
||||
<option value="">– Gericht wählen –</option>
|
||||
{dishes.map((dish) => (
|
||||
<option key={dish.id} value={dish.id}>
|
||||
{dish.name} ({formatPrice(dish.price)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex gap-1 justify-center">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add"
|
||||
onClick={addEntry}
|
||||
disabled={!selectedDish}
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-neutral"
|
||||
onClick={() => {
|
||||
setAdding(null);
|
||||
setSelectedDish("");
|
||||
}}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-add mt-1"
|
||||
onClick={() => {
|
||||
setAdding({ iso: d.iso, section: s.key });
|
||||
setSelectedDish("");
|
||||
}}
|
||||
>
|
||||
Mahlzeit hinzufügen
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<p className="mt-4 text-sm text-gray-500">
|
||||
Neue Gerichte legst du unter{" "}
|
||||
<Link href="/intern/gerichte" className="underline">
|
||||
Gerichte
|
||||
</Link>{" "}
|
||||
an.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user