Files
Speisekarte/components/WeekEditor.tsx
T

213 lines
7.4 KiB
TypeScript
Raw 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 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>
);
}