Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b3921c50e | ||
|
|
63870ba2e6 |
@@ -4,6 +4,17 @@ Alle nennenswerten Änderungen an diesem Projekt werden hier dokumentiert.
|
|||||||
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.1.0/),
|
||||||
Versionierung nach [Semantic Versioning](https://semver.org/lang/de/).
|
Versionierung nach [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [1.12.1] – 2026-07-20
|
||||||
|
|
||||||
|
### Geändert
|
||||||
|
- Bedienung geschlossener Tage im Wochenplan überarbeitet: Statt eines
|
||||||
|
Text-Links am Datum markiert jetzt ein Schloss-Symbol (🔓 zum
|
||||||
|
Schließen, 🔒 zeigt „geschlossen" an und öffnet per Klick wieder) den
|
||||||
|
Tag. Ein geschlossener Tag zeigt sowohl im Wochenplan als auch auf
|
||||||
|
der öffentlichen Speisekarte (Desktop-Tabelle) einen einzigen, über
|
||||||
|
alle Essenszeiten-Zeilen durchgehenden „Geschlossen"-Hinweis statt
|
||||||
|
einer Wiederholung in jeder einzelnen Zeile.
|
||||||
|
|
||||||
## [1.12.0] – 2026-07-20
|
## [1.12.0] – 2026-07-20
|
||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|||||||
@@ -102,6 +102,10 @@ body {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
.menu-table tr:nth-child(even) td.cell { background: var(--c-row-bg-alt); }
|
.menu-table tr:nth-child(even) td.cell { background: var(--c-row-bg-alt); }
|
||||||
|
.menu-table td.cell.closed-cell {
|
||||||
|
vertical-align: middle;
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
.menu-table td.cell .dish { margin-bottom: 0.35rem; }
|
.menu-table td.cell .dish { margin-bottom: 0.35rem; }
|
||||||
.menu-table td.cell .dish sup {
|
.menu-table td.cell .dish sup {
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
|
|||||||
+27
-23
@@ -160,42 +160,45 @@ export default async function Home({
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="section-head w-56" aria-hidden />
|
<th className="section-head w-56" aria-hidden />
|
||||||
{days.map((d) => {
|
{days.map((d) => (
|
||||||
const closed = closedDayFor(d.iso);
|
|
||||||
return (
|
|
||||||
<th key={d.iso} className="day-head">
|
<th key={d.iso} className="day-head">
|
||||||
<span className="font-bold">{d.weekday}</span>
|
<span className="font-bold">{d.weekday}</span>
|
||||||
<br />
|
<br />
|
||||||
{formatDateLong(d.iso, dict.dateLocale)}
|
{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>
|
</th>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sections.map((s) => (
|
{sections.map((s, sIdx) => (
|
||||||
<tr key={s.key}>
|
<tr key={s.key}>
|
||||||
<th className="section-head">
|
<th className="section-head">
|
||||||
<div>{localized(s, locale)}</div>
|
<div>{localized(s, locale)}</div>
|
||||||
<div className="time">{s.time_range}</div>
|
<div className="time">{s.time_range}</div>
|
||||||
</th>
|
</th>
|
||||||
{days.map((d) => (
|
{days.map((d) => {
|
||||||
|
const closed = closedDayFor(d.iso);
|
||||||
|
if (closed) {
|
||||||
|
if (sIdx > 0) return null;
|
||||||
|
return (
|
||||||
|
<td key={d.iso} className="cell closed-cell" rowSpan={sections.length}>
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1 py-4 text-red-700">
|
||||||
|
<span className="text-2xl" aria-hidden>
|
||||||
|
🔒
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">{dict.publicMenu.closedLabel}</span>
|
||||||
|
{localized(closed, locale) && (
|
||||||
|
<span className="text-sm px-2 text-center">
|
||||||
|
{localized(closed, locale)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<td key={d.iso} className="cell">
|
<td key={d.iso} className="cell">
|
||||||
{closedDayFor(d.iso)
|
{entriesFor(entries, d.iso, s.key).map((e) => {
|
||||||
? null
|
|
||||||
: entriesFor(entries, d.iso, s.key).map((e) => {
|
|
||||||
const codes = dishCodes(e.dish);
|
const codes = dishCodes(e.dish);
|
||||||
return (
|
return (
|
||||||
<div key={e.id} className="dish">
|
<div key={e.id} className="dish">
|
||||||
@@ -211,7 +214,8 @@ export default async function Home({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ export default function PublicMobileMenu({
|
|||||||
{day.weekday} · {formatDateLong(day.iso, dict.dateLocale)}
|
{day.weekday} · {formatDateLong(day.iso, dict.dateLocale)}
|
||||||
</h3>
|
</h3>
|
||||||
{closed ? (
|
{closed ? (
|
||||||
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
|
<div className="rounded-md border border-red-200 bg-red-50 px-3 py-3 text-sm text-red-700 text-center">
|
||||||
|
<span aria-hidden>🔒</span>{" "}
|
||||||
<span className="font-semibold">{dict.publicMenu.closedLabel}</span>
|
<span className="font-semibold">{dict.publicMenu.closedLabel}</span>
|
||||||
{localized(closed, locale) && <span> – {localized(closed, locale)}</span>}
|
{localized(closed, locale) && <span> – {localized(closed, locale)}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+60
-24
@@ -208,35 +208,46 @@ export default function WeekEditor({
|
|||||||
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
|
? dishes.filter((d) => localized(d, locale).toLowerCase().includes(query))
|
||||||
: dishes;
|
: dishes;
|
||||||
|
|
||||||
// Kleine Anzeige/Umschaltung unter dem Tagesdatum: "Geschlossen" +
|
// Kleines Schloss-Symbol neben dem Tagesdatum: 🔓 zum Schließen
|
||||||
// Wieder-öffnen-Button, sonst ein dezenter "Tag schließen"-Link.
|
// anklicken, 🔒 zeigt einen geschlossenen Tag und öffnet ihn per Klick
|
||||||
function renderClosureBadge(iso: string) {
|
// wieder – der Zustand des Symbols selbst ist die Beschriftung.
|
||||||
const closed = closedDayFor(iso);
|
function renderClosureToggle(iso: string) {
|
||||||
if (closed) {
|
const closed = Boolean(closedDayFor(iso));
|
||||||
return (
|
return (
|
||||||
<div className="mt-1 flex flex-wrap items-center justify-center gap-1 text-xs">
|
|
||||||
<span className="rounded-full bg-red-100 text-red-700 px-2 py-0.5 font-semibold">
|
|
||||||
{dict.weekEditor.closedLabel}
|
|
||||||
</span>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-gray-400 hover:text-[var(--c-primary)] underline"
|
className="mt-0.5 text-base leading-none align-middle opacity-70 hover:opacity-100 disabled:opacity-40"
|
||||||
onClick={() => reopenDay(iso)}
|
onClick={() => (closed ? reopenDay(iso) : startClosing(iso))}
|
||||||
disabled={closeBusy}
|
disabled={closeBusy}
|
||||||
|
title={closed ? dict.weekEditor.reopenDay : dict.weekEditor.closeDay}
|
||||||
|
aria-label={closed ? dict.weekEditor.reopenDay : dict.weekEditor.closeDay}
|
||||||
>
|
>
|
||||||
{dict.weekEditor.reopenDay}
|
{closed ? "🔒" : "🔓"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Banner, das bei einem geschlossenen Tag alle Essenszeiten-Zellen
|
||||||
|
// dieses Tages ersetzt (Desktop: eine über rowSpan verbundene Zelle,
|
||||||
|
// Mobil: anstelle der Essenszeiten-Karten) – "Bearbeiten" öffnet das
|
||||||
|
// Formular erneut, um nur den Grund anzupassen, ohne den Tag über das
|
||||||
|
// Symbol im Header erst wieder öffnen zu müssen.
|
||||||
|
function renderClosedBanner(iso: string, closed: ClosedDay) {
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-1 py-4 text-red-700">
|
||||||
|
<span className="text-2xl" aria-hidden>
|
||||||
|
🔒
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold">{dict.weekEditor.closedLabel}</span>
|
||||||
|
{closed.reason && <span className="text-sm px-2 text-center">{closed.reason}</span>}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-1 text-xs text-gray-400 hover:text-[var(--c-primary)] underline"
|
className="mt-1 text-xs text-gray-500 hover:text-[var(--c-primary)] underline"
|
||||||
onClick={() => startClosing(iso)}
|
onClick={() => startClosing(iso)}
|
||||||
>
|
>
|
||||||
{dict.weekEditor.closeDay}
|
{dict.common.edit}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,7 +341,6 @@ export default function WeekEditor({
|
|||||||
// unabhängig davon, welche Ansicht gerade rendert.
|
// unabhängig davon, welche Ansicht gerade rendert.
|
||||||
function renderCell(iso: string, sectionKey: string) {
|
function renderCell(iso: string, sectionKey: string) {
|
||||||
const isAdding = adding?.iso === iso && adding?.section === sectionKey;
|
const isAdding = adding?.iso === iso && adding?.section === sectionKey;
|
||||||
const closed = Boolean(closedDayFor(iso));
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{entriesFor(entries, iso, sectionKey).map((e) => {
|
{entriesFor(entries, iso, sectionKey).map((e) => {
|
||||||
@@ -353,7 +363,7 @@ export default function WeekEditor({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{closed ? null : isAdding ? (
|
{isAdding ? (
|
||||||
<div className="mt-2 space-y-1 text-left">
|
<div className="mt-2 space-y-1 text-left">
|
||||||
{selectedDishObj ? (
|
{selectedDishObj ? (
|
||||||
<div className="flex items-center gap-1 text-sm">
|
<div className="flex items-center gap-1 text-sm">
|
||||||
@@ -484,9 +494,19 @@ export default function WeekEditor({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-bold text-[var(--c-primary)] mb-1">
|
<h2 className="font-bold text-[var(--c-primary)] mb-1">
|
||||||
{selectedDay.weekday} · {formatDateLong(selectedDay.iso, dict.dateLocale)}
|
{selectedDay.weekday} · {formatDateLong(selectedDay.iso, dict.dateLocale)}{" "}
|
||||||
|
{renderClosureToggle(selectedDay.iso)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mb-2">{renderClosureBadge(selectedDay.iso)}</div>
|
{(() => {
|
||||||
|
const closed = closedDayFor(selectedDay.iso);
|
||||||
|
if (closed) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md border border-red-200 bg-red-50">
|
||||||
|
{renderClosedBanner(selectedDay.iso, closed)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{sections.map((s) => (
|
{sections.map((s) => (
|
||||||
<div key={s.key} className="rounded-md border border-gray-200 overflow-hidden">
|
<div key={s.key} className="rounded-md border border-gray-200 overflow-hidden">
|
||||||
@@ -500,6 +520,8 @@ export default function WeekEditor({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Ab "sm": klassische Wochentabelle */}
|
{/* Ab "sm": klassische Wochentabelle */}
|
||||||
@@ -512,24 +534,38 @@ export default function WeekEditor({
|
|||||||
<th key={d.iso} className="day-head">
|
<th key={d.iso} className="day-head">
|
||||||
<span className="font-bold">{d.weekday}</span>
|
<span className="font-bold">{d.weekday}</span>
|
||||||
<br />
|
<br />
|
||||||
{formatDateLong(d.iso, dict.dateLocale)}
|
{formatDateLong(d.iso, dict.dateLocale)} {renderClosureToggle(d.iso)}
|
||||||
{renderClosureBadge(d.iso)}
|
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sections.map((s) => (
|
{sections.map((s, sIdx) => (
|
||||||
<tr key={s.key}>
|
<tr key={s.key}>
|
||||||
<th className="section-head">
|
<th className="section-head">
|
||||||
<div>{localized(s, locale)}</div>
|
<div>{localized(s, locale)}</div>
|
||||||
<div className="time">{s.time_range}</div>
|
<div className="time">{s.time_range}</div>
|
||||||
</th>
|
</th>
|
||||||
{days.map((d) => (
|
{days.map((d) => {
|
||||||
|
const closed = closedDayFor(d.iso);
|
||||||
|
if (closed) {
|
||||||
|
if (sIdx > 0) return null;
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
key={d.iso}
|
||||||
|
className="cell closed-cell"
|
||||||
|
rowSpan={sections.length}
|
||||||
|
>
|
||||||
|
{renderClosedBanner(d.iso, closed)}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
<td key={d.iso} className="cell">
|
<td key={d.iso} className="cell">
|
||||||
{renderCell(d.iso, s.key)}
|
{renderCell(d.iso, s.key)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "canteen-menu",
|
"name": "canteen-menu",
|
||||||
"version": "1.12.0",
|
"version": "1.12.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"predev": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",
|
"predev": "node --env-file-if-exists=.env.local db/migrate.mjs && node --env-file-if-exists=.env.local scripts/bootstrap-admin.mjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user