From 93e487a1b055aaaa49e9d5ae18cf80a48ce2dcc6 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 22 Jul 2026 15:04:01 +0200 Subject: [PATCH] Datumsformat konfigurierbar (Einstellungen -> Darstellung) MHD wurde in den Ansichten noch roh als YYYY-MM-DD ausgegeben. Jetzt: - Neue Einstellung date_format (23.12.2026 | 23.12.26 | 23. Dezember 2026 | 2026-12-23 | 12/23/2026) mit Live-Beispiel auf der Einstellungsseite. - SettingsProvider stellt Format und formatDate() app-weit bereit und laedt die Einstellungen nach dem Login. - Verwendet in Uebersicht (bald ablaufend), Produktdetail (Chargen) und Auslagern (Chargenauswahl). Datumsfelder zur Eingabe bleiben ISO, wie es das HTML-Datumsfeld verlangt. Co-Authored-By: Claude Opus 4.8 --- web/src/main.jsx | 5 +++- web/src/pages/CheckOut.jsx | 6 +++-- web/src/pages/Dashboard.jsx | 4 +++- web/src/pages/ProductForm.jsx | 4 +++- web/src/pages/Settings.jsx | 27 +++++++++++++++++++-- web/src/settings.jsx | 45 +++++++++++++++++++++++++++++++++++ web/src/units.js | 28 ++++++++++++++++++++++ 7 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 web/src/settings.jsx diff --git a/web/src/main.jsx b/web/src/main.jsx index 0896a56..031b583 100644 --- a/web/src/main.jsx +++ b/web/src/main.jsx @@ -3,13 +3,16 @@ import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; import { AuthProvider } from "./auth"; +import { SettingsProvider } from "./settings"; import "./styles.css"; ReactDOM.createRoot(document.getElementById("root")).render( - + + + diff --git a/web/src/pages/CheckOut.jsx b/web/src/pages/CheckOut.jsx index 13f2b26..f0b3b10 100644 --- a/web/src/pages/CheckOut.jsx +++ b/web/src/pages/CheckOut.jsx @@ -1,9 +1,11 @@ import { useEffect, useState } from "react"; import { api } from "../api"; import Icon from "../components/Icon"; +import { useSettings } from "../settings"; import { buildUnitOptions, fmt, isExpired, unitShort } from "../units"; export default function CheckOut() { + const { formatDate } = useSettings(); const [barcode, setBarcode] = useState(""); const [search, setSearch] = useState(""); const [results, setResults] = useState([]); @@ -159,7 +161,7 @@ export default function CheckOut() { {lots.map((l) => ( ))} @@ -170,7 +172,7 @@ export default function CheckOut() {
Gewählte Charge: {lotAmount(selectedLot.quantity)} - {selectedLot.best_before ? ` · MHD ${selectedLot.best_before}` : " · ohne MHD"} + {selectedLot.best_before ? ` · MHD ${formatDate(selectedLot.best_before)}` : " · ohne MHD"} {isExpired(selectedLot.best_before) ? " (abgelaufen)" : ""}
)} diff --git a/web/src/pages/Dashboard.jsx b/web/src/pages/Dashboard.jsx index 7c9c533..76979e8 100644 --- a/web/src/pages/Dashboard.jsx +++ b/web/src/pages/Dashboard.jsx @@ -2,9 +2,11 @@ import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api"; import Icon from "../components/Icon"; +import { useSettings } from "../settings"; import { amountText, articlePrimary, articleSecondary, fmt, relativeExpiry } from "../units"; export default function Dashboard() { + const { formatDate } = useSettings(); const [expiring, setExpiring] = useState([]); const [shopping, setShopping] = useState([]); const [groupShopping, setGroupShopping] = useState([]); @@ -89,7 +91,7 @@ export default function Dashboard() {
{articleSecondary(it)}
)} - {it.best_before} + {formatDate(it.best_before)} {relativeExpiry(it.days_left)} diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index 7d038b1..31423b6 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -5,6 +5,7 @@ import { useAuth } from "../auth"; import BarcodeList from "../components/BarcodeList"; import Icon from "../components/Icon"; import { guessGroup } from "../offUtils"; +import { useSettings } from "../settings"; import { daysUntil, expiryRowClass, fmt, isExpired, relativeExpiry, unitShort } from "../units"; const EMPTY = { @@ -377,6 +378,7 @@ export default function ProductForm() { /** Chargen-Karte mit Bearbeiten/Löschen. Bearbeitet wird in der Produkteinheit. */ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onChanged, onError }) { + const { formatDate } = useSettings(); const [editId, setEditId] = useState(null); const [draft, setDraft] = useState({ quantity: "", best_before: "" }); // In welcher Einheit die Menge bearbeitet wird: "unit" oder "package". @@ -501,7 +503,7 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha style={{ marginTop: 0, minWidth: 150 }} /> ) : ( <> - {l.best_before || "–"} + {l.best_before ? formatDate(l.best_before) : "–"} {cls && ( diff --git a/web/src/pages/Settings.jsx b/web/src/pages/Settings.jsx index 6c99470..0ac7bae 100644 --- a/web/src/pages/Settings.jsx +++ b/web/src/pages/Settings.jsx @@ -1,11 +1,15 @@ import { useEffect, useState } from "react"; import { api } from "../api"; import Icon from "../components/Icon"; +import { DATE_FORMAT_KEY, useSettings } from "../settings"; +import { DATE_FORMATS, formatDate } from "../units"; const EXPIRY_KEY = "expiry_warning_days"; export default function Settings() { + const { reload } = useSettings(); const [days, setDays] = useState(""); + const [dateFormat, setDateFormat] = useState("de"); const [error, setError] = useState(null); const [info, setInfo] = useState(null); const [busy, setBusy] = useState(false); @@ -13,8 +17,8 @@ export default function Settings() { async function load() { try { const settings = await api.listSettings(); - const row = settings.find((s) => s.key === EXPIRY_KEY); - setDays(row ? row.value : "7"); + setDays(settings.find((s) => s.key === EXPIRY_KEY)?.value ?? "7"); + setDateFormat(settings.find((s) => s.key === DATE_FORMAT_KEY)?.value ?? "de"); } catch (err) { setError(err.message); } @@ -27,6 +31,8 @@ export default function Settings() { setError(null); setInfo(null); setBusy(true); try { await api.setSetting(EXPIRY_KEY, String(parseInt(days, 10))); + await api.setSetting(DATE_FORMAT_KEY, dateFormat); + await reload(); setInfo("Gespeichert."); } catch (err) { setError(err.message); @@ -35,6 +41,9 @@ export default function Settings() { } } + // Beispiel mit einem festen Datum, damit man die Wirkung sofort sieht. + const preview = formatDate("2026-12-23", dateFormat); + return (
@@ -57,6 +66,20 @@ export default function Settings() { Chargen, deren Mindesthaltbarkeitsdatum innerhalb dieser Frist liegt (oder bereits überschritten ist), erscheinen auf der Übersicht unter „Bald ablaufend".

+ +
+

Darstellung

+
+ +

Beispiel: {preview}

+
diff --git a/web/src/settings.jsx b/web/src/settings.jsx new file mode 100644 index 0000000..b884c5d --- /dev/null +++ b/web/src/settings.jsx @@ -0,0 +1,45 @@ +import { createContext, useCallback, useContext, useEffect, useState } from "react"; +import { api } from "./api"; +import { useAuth } from "./auth"; +import { formatDate } from "./units"; + +export const DATE_FORMAT_KEY = "date_format"; +const DEFAULT_FORMAT = "de"; + +const SettingsContext = createContext({ + dateFormat: DEFAULT_FORMAT, + formatDate: (iso) => formatDate(iso, DEFAULT_FORMAT), + reload: () => {}, +}); + +/** Lädt die anzeigerelevanten Einstellungen einmal nach dem Login. */ +export function SettingsProvider({ children }) { + const { user } = useAuth(); + const [dateFormat, setDateFormat] = useState(DEFAULT_FORMAT); + + const reload = useCallback(async () => { + try { + const rows = await api.listSettings(); + const row = rows.find((r) => r.key === DATE_FORMAT_KEY); + setDateFormat(row?.value || DEFAULT_FORMAT); + } catch { + /* Einstellungen sind optional – Standard bleibt bestehen. */ + } + }, []); + + useEffect(() => { + if (user) reload(); + }, [user, reload]); + + const value = { + dateFormat, + formatDate: (iso) => formatDate(iso, dateFormat), + reload, + }; + + return {children}; +} + +export function useSettings() { + return useContext(SettingsContext); +} diff --git a/web/src/units.js b/web/src/units.js index a3cbce8..b601c04 100644 --- a/web/src/units.js +++ b/web/src/units.js @@ -53,6 +53,34 @@ export function isExpired(dateStr) { return d != null && d < 0; } +// Auswählbare Datumsformate (Einstellungen -> Darstellung). +export const DATE_FORMATS = [ + { value: "de", label: "23.12.2026" }, + { value: "de-short", label: "23.12.26" }, + { value: "long", label: "23. Dezember 2026" }, + { value: "iso", label: "2026-12-23" }, + { value: "us", label: "12/23/2026" }, +]; + +// Formatiert ein ISO-Datum (YYYY-MM-DD) nach dem gewählten Format. +export function formatDate(iso, format = "de") { + if (!iso) return "–"; + const date = new Date(`${iso}T00:00:00`); + if (Number.isNaN(date.getTime())) return iso; + switch (format) { + case "iso": + return iso; + case "de-short": + return date.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "2-digit" }); + case "long": + return date.toLocaleDateString("de-DE", { day: "numeric", month: "long", year: "numeric" }); + case "us": + return date.toLocaleDateString("en-US"); + default: + return date.toLocaleDateString("de-DE"); + } +} + // MHD lesbar als Zeitspanne: "in 3 Tagen", "in 1 Woche", "vor 2 Wochen". // Deutsch nach "in"/"vor" steht im Dativ: 1 Tag / 3 Tagen, 1 Woche / 2 Wochen. export function relativeExpiry(daysLeft) {