feat: Tage als geschlossen markierbar (Feiertage o. Ä.)
Im Wochenplan lässt sich pro Tag ein optionaler, mehrsprachiger Schließungsgrund hinterlegen. Geschlossene Tage werden auf der öffentlichen Speisekarte (Desktop + mobil) und im RSS-Feed als "Geschlossen" statt mit Essenszeiten angezeigt, im Wochenplan bleibt "Mahlzeit hinzufügen" für diesen Tag deaktiviert.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { removeClosedDay } from "@/lib/db/closed-days";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ date: string }> }
|
||||
) {
|
||||
if (!isSameOrigin(request)) {
|
||||
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
|
||||
}
|
||||
try {
|
||||
await requireRole(["staff", "admin"]);
|
||||
const { date } = await params;
|
||||
await removeClosedDay(date);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
if (res) return res;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { listClosedDays, setClosedDay } from "@/lib/db/closed-days";
|
||||
import { authErrorResponse, requireRole } from "@/lib/auth/session";
|
||||
import { isSameOrigin } from "@/lib/origin";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const from = searchParams.get("from");
|
||||
const to = searchParams.get("to");
|
||||
if (!from || !to) {
|
||||
return NextResponse.json({ error: "invalid_input" }, { status: 400 });
|
||||
}
|
||||
return NextResponse.json(await listClosedDays(from, to));
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!isSameOrigin(request)) {
|
||||
return NextResponse.json({ error: "Nicht erlaubt." }, { status: 403 });
|
||||
}
|
||||
try {
|
||||
await requireRole(["staff", "admin"]);
|
||||
const body = (await request.json()) as {
|
||||
closed_date: string;
|
||||
reason: string | null;
|
||||
translations?: Record<string, string | null>;
|
||||
};
|
||||
const closedDay = await setClosedDay(body.closed_date, body.reason, body.translations);
|
||||
return NextResponse.json(closedDay, { status: 201 });
|
||||
} catch (err) {
|
||||
const res = authErrorResponse(err);
|
||||
if (res) return res;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ const ALLOWED_TABLES: Record<string, { idColumn: string; translationTable: Trans
|
||||
sections: { idColumn: "key", translationTable: "section_translations" },
|
||||
allergens: { idColumn: "code", translationTable: "allergen_translations" },
|
||||
additives: { idColumn: "code", translationTable: "additive_translations" },
|
||||
closed_days: { idColumn: "closed_date", translationTable: "closed_day_translations" },
|
||||
};
|
||||
|
||||
// Unsere Sprachcodes -> DeepL target_lang. Sprachen ohne Eintrag hier sind
|
||||
|
||||
+25
-16
@@ -1,5 +1,6 @@
|
||||
import { query } from "@/lib/db";
|
||||
import { loadMenuEntries } from "@/lib/db/menu-entries";
|
||||
import { listClosedDays } from "@/lib/db/closed-days";
|
||||
import { addDays, entriesFor, formatPrice, mondayOf, todayISO } from "@/lib/menu";
|
||||
import { localized, SITE_TITLE } from "@/lib/types";
|
||||
import type { Section } from "@/lib/types";
|
||||
@@ -45,30 +46,38 @@ export async function GET() {
|
||||
// Wochennavigation anzeigt.
|
||||
const rangeEnd = addDays(monday, 11);
|
||||
|
||||
const [sections, entries] = await Promise.all([
|
||||
const [sections, entries, closedDays] = await Promise.all([
|
||||
query<Section>("select * from sections order by sort"),
|
||||
loadMenuEntries(monday, rangeEnd),
|
||||
listClosedDays(monday, rangeEnd),
|
||||
]);
|
||||
|
||||
const days = Array.from(new Set(entries.map((e) => e.menu_date))).sort();
|
||||
const days = Array.from(
|
||||
new Set([...entries.map((e) => e.menu_date), ...closedDays.map((c) => c.closed_date)])
|
||||
).sort();
|
||||
|
||||
const items = days
|
||||
.map((iso) => {
|
||||
const dayContent = sections
|
||||
.map((s) => {
|
||||
const dishes = entriesFor(entries, iso, s.key);
|
||||
if (dishes.length === 0) return "";
|
||||
const list = dishes
|
||||
.map(
|
||||
(e) =>
|
||||
`<li>${escapeXml(localized(e.dish, "de"))} – ${formatPrice(e.dish.price)}</li>`
|
||||
)
|
||||
const closed = closedDays.find((c) => c.closed_date === iso);
|
||||
const dayContent = closed
|
||||
? `<p><strong>Geschlossen</strong>${
|
||||
closed.reason ? ` – ${escapeXml(closed.reason)}` : ""
|
||||
}</p>`
|
||||
: sections
|
||||
.map((s) => {
|
||||
const dishes = entriesFor(entries, iso, s.key);
|
||||
if (dishes.length === 0) return "";
|
||||
const list = dishes
|
||||
.map(
|
||||
(e) =>
|
||||
`<li>${escapeXml(localized(e.dish, "de"))} – ${formatPrice(e.dish.price)}</li>`
|
||||
)
|
||||
.join("");
|
||||
return `<p><strong>${escapeXml(localized(s, "de"))} (${escapeXml(
|
||||
s.time_range
|
||||
)})</strong></p><ul>${list}</ul>`;
|
||||
})
|
||||
.join("");
|
||||
return `<p><strong>${escapeXml(localized(s, "de"))} (${escapeXml(
|
||||
s.time_range
|
||||
)})</strong></p><ul>${list}</ul>`;
|
||||
})
|
||||
.join("");
|
||||
if (!dayContent) return "";
|
||||
|
||||
const [y, m, d] = iso.split("-").map(Number);
|
||||
|
||||
+6
-1
@@ -7,5 +7,10 @@ export default async function InternPage({
|
||||
searchParams: Promise<{ woche?: string }>;
|
||||
}) {
|
||||
const { woche } = await searchParams;
|
||||
return <WeekEditor monday={mondayOf(woche)} />;
|
||||
return (
|
||||
<WeekEditor
|
||||
monday={mondayOf(woche)}
|
||||
translateEnabled={!!process.env.DEEPL_API_KEY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+44
-22
@@ -2,6 +2,7 @@ import Link from "next/link";
|
||||
import { query } from "@/lib/db";
|
||||
import { loadMenuEntries } from "@/lib/db/menu-entries";
|
||||
import { loadTranslations } from "@/lib/db/translations";
|
||||
import { listClosedDays } from "@/lib/db/closed-days";
|
||||
import { getSettings } from "@/lib/db/settings";
|
||||
import {
|
||||
addDays,
|
||||
@@ -43,6 +44,7 @@ export default async function Home({
|
||||
sectionTranslations,
|
||||
allergenTranslations,
|
||||
additiveTranslations,
|
||||
closedDays,
|
||||
] = await Promise.all([
|
||||
query<Omit<Section, "translations">>("select key, label, time_range, sort from sections order by sort"),
|
||||
loadMenuEntries(monday, friday),
|
||||
@@ -52,7 +54,9 @@ export default async function Home({
|
||||
loadTranslations("section_translations"),
|
||||
loadTranslations("allergen_translations"),
|
||||
loadTranslations("additive_translations"),
|
||||
listClosedDays(monday, friday),
|
||||
]);
|
||||
const closedDayFor = (iso: string) => closedDays.find((c) => c.closed_date === iso);
|
||||
|
||||
const sections: Section[] = sectionRows.map((s) => ({
|
||||
...s,
|
||||
@@ -130,6 +134,7 @@ export default async function Home({
|
||||
monday={monday}
|
||||
sections={sections}
|
||||
entries={entries}
|
||||
closedDays={closedDays}
|
||||
showLabeling={labelingDetailed}
|
||||
/>
|
||||
|
||||
@@ -155,13 +160,28 @@ export default async function Home({
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="section-head w-56" aria-hidden />
|
||||
{days.map((d) => (
|
||||
<th key={d.iso} className="day-head">
|
||||
<span className="font-bold">{d.weekday}</span>
|
||||
<br />
|
||||
{formatDateLong(d.iso, dict.dateLocale)}
|
||||
</th>
|
||||
))}
|
||||
{days.map((d) => {
|
||||
const closed = closedDayFor(d.iso);
|
||||
return (
|
||||
<th key={d.iso} className="day-head">
|
||||
<span className="font-bold">{d.weekday}</span>
|
||||
<br />
|
||||
{formatDateLong(d.iso, dict.dateLocale)}
|
||||
{closed && (
|
||||
<div className="mt-1 text-xs font-normal">
|
||||
<span className="rounded-full bg-red-100 text-red-700 px-2 py-0.5 font-semibold">
|
||||
{dict.publicMenu.closedLabel}
|
||||
</span>
|
||||
{localized(closed, locale) && (
|
||||
<div className="mt-0.5 opacity-90">
|
||||
{localized(closed, locale)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -173,21 +193,23 @@ export default async function Home({
|
||||
</th>
|
||||
{days.map((d) => (
|
||||
<td key={d.iso} className="cell">
|
||||
{entriesFor(entries, d.iso, s.key).map((e) => {
|
||||
const codes = dishCodes(e.dish);
|
||||
return (
|
||||
<div key={e.id} className="dish">
|
||||
<span className="font-bold">
|
||||
{localized(e.dish, locale)}
|
||||
</span>
|
||||
{labelingDetailed && codes && (
|
||||
<sup className="dish-codes">{codes}</sup>
|
||||
)}
|
||||
{" – "}
|
||||
{formatPrice(e.dish.price)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{closedDayFor(d.iso)
|
||||
? null
|
||||
: entriesFor(entries, d.iso, s.key).map((e) => {
|
||||
const codes = dishCodes(e.dish);
|
||||
return (
|
||||
<div key={e.id} className="dish">
|
||||
<span className="font-bold">
|
||||
{localized(e.dish, locale)}
|
||||
</span>
|
||||
{labelingDetailed && codes && (
|
||||
<sup className="dish-codes">{codes}</sup>
|
||||
)}
|
||||
{" – "}
|
||||
{formatPrice(e.dish.price)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user