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,50 @@
|
||||
import "server-only";
|
||||
import { query, queryOne } from "@/lib/db";
|
||||
import { loadTranslationsFor, replaceTranslations } from "./translations";
|
||||
import type { ClosedDay } from "@/lib/types";
|
||||
|
||||
interface ClosedDayRow {
|
||||
closed_date: string;
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
/** Geschlossene Tage in einem Zeitraum (für Wochenplan/öffentliche Speisekarte/RSS). */
|
||||
export async function listClosedDays(fromISO: string, toISO: string): Promise<ClosedDay[]> {
|
||||
const rows = await query<ClosedDayRow>(
|
||||
"select closed_date, reason from closed_days where closed_date between $1 and $2",
|
||||
[fromISO, toISO]
|
||||
);
|
||||
if (rows.length === 0) return [];
|
||||
const translations = await loadTranslationsFor(
|
||||
"closed_day_translations",
|
||||
rows.map((r) => r.closed_date)
|
||||
);
|
||||
return rows.map((r) => ({
|
||||
...r,
|
||||
translations: translations.get(r.closed_date) ?? {},
|
||||
}));
|
||||
}
|
||||
|
||||
/** Tag als geschlossen markieren (oder bestehenden Grund aktualisieren). */
|
||||
export async function setClosedDay(
|
||||
date: string,
|
||||
reason: string | null,
|
||||
translations?: Record<string, string | null>
|
||||
): Promise<ClosedDay> {
|
||||
const row = (await queryOne<ClosedDayRow>(
|
||||
`insert into closed_days (closed_date, reason) values ($1, $2)
|
||||
on conflict (closed_date) do update set reason = excluded.reason
|
||||
returning closed_date, reason`,
|
||||
[date, reason]
|
||||
))!;
|
||||
if (translations) {
|
||||
await replaceTranslations("closed_day_translations", row.closed_date, translations);
|
||||
}
|
||||
const loaded = await loadTranslationsFor("closed_day_translations", [row.closed_date]);
|
||||
return { ...row, translations: loaded.get(row.closed_date) ?? {} };
|
||||
}
|
||||
|
||||
/** Tag wieder öffnen (löscht auch alle Übersetzungen des Grunds per Cascade). */
|
||||
export async function removeClosedDay(date: string): Promise<void> {
|
||||
await query("delete from closed_days where closed_date = $1", [date]);
|
||||
}
|
||||
@@ -11,13 +11,15 @@ export type TranslationTable =
|
||||
| "dish_translations"
|
||||
| "section_translations"
|
||||
| "allergen_translations"
|
||||
| "additive_translations";
|
||||
| "additive_translations"
|
||||
| "closed_day_translations";
|
||||
|
||||
const ID_COLUMN: Record<TranslationTable, string> = {
|
||||
dish_translations: "dish_id",
|
||||
section_translations: "section_key",
|
||||
allergen_translations: "allergen_code",
|
||||
additive_translations: "additive_code",
|
||||
closed_day_translations: "closed_date",
|
||||
};
|
||||
|
||||
/** Alle Übersetzungen einer Tabelle, gruppiert nach Eltern-ID. */
|
||||
|
||||
@@ -75,6 +75,7 @@ const de = {
|
||||
suggestDish: "Rezept vorschlagen",
|
||||
noEntry: "–",
|
||||
rssFeed: "RSS-Feed abonnieren",
|
||||
closedLabel: "Geschlossen",
|
||||
},
|
||||
weekEditor: {
|
||||
title: "Wochenplan",
|
||||
@@ -87,6 +88,11 @@ const de = {
|
||||
duplicateEntry: "Dieses Gericht ist an diesem Tag bereits zugewiesen.",
|
||||
saveError: "Speichern fehlgeschlagen: {message}",
|
||||
deleteError: "Löschen fehlgeschlagen: {message}",
|
||||
closeDay: "Tag schließen",
|
||||
reopenDay: "Wieder öffnen",
|
||||
closedLabel: "Geschlossen",
|
||||
closedReasonLabel: "Grund (optional)",
|
||||
closedReasonPlaceholder: "z. B. Weihnachtsfeiertage",
|
||||
},
|
||||
dishManager: {
|
||||
title: "Gerichte",
|
||||
|
||||
+10
-1
@@ -50,11 +50,20 @@ export interface DishSuggestion {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ClosedDay {
|
||||
closed_date: string;
|
||||
reason: string | null;
|
||||
translations: Record<string, string>;
|
||||
}
|
||||
|
||||
/** Übersetzten Namen für die aktuelle Sprache liefern, sonst Deutsch als Fallback. */
|
||||
export function localized(
|
||||
item: {
|
||||
name?: string;
|
||||
label?: string;
|
||||
// Grund eines geschlossenen Tages (s. ClosedDay) – optional, anders
|
||||
// als name/label bei den übrigen Inhaltstypen.
|
||||
reason?: string | null;
|
||||
// Neue, dynamische Form (Gerichte/Essenszeiten/Allergene/
|
||||
// Zusatzstoffe, s. lib/db/translations.ts) – Sprachcode -> Text.
|
||||
translations?: Record<string, string>;
|
||||
@@ -68,7 +77,7 @@ export function localized(
|
||||
},
|
||||
locale: string
|
||||
): string {
|
||||
const de = item.name ?? item.label ?? "";
|
||||
const de = item.name ?? item.label ?? item.reason ?? "";
|
||||
if (locale === "de") return de;
|
||||
if (item.translations) return item.translations[locale] || de;
|
||||
if (locale === "en") return item.name_en || item.label_en || de;
|
||||
|
||||
Reference in New Issue
Block a user