MHD nur mit Monat und Jahr in Web-UI und iOS-App

Setzt den Backend-Umbau in beiden Oberflaechen um.

Die Genauigkeit gilt jeweils fuer den ganzen Einlager-Vorgang und nicht je
Charge: Auf einer Packung steht entweder ein Tagesdatum oder nur Monat/Jahr,
gemischt kommt das nicht vor. Vorbelegt wird sie aus dem Produkt, laesst sich
aber im Vorgang umstellen.

Web-UI: Umschalter im Kopf der Chargenliste; das Eingabefeld wechselt zwischen
type="date" und type="month". Im Produktformular gibt es das neue Feld
"MHD-Angabe" neben der Gebinde-Bezeichnung. Anzeige laeuft ueber den
Settings-Context, damit das eingestellte Datumsformat erhalten bleibt und
Monatsangaben ueberall als "09/2026" erscheinen (Auslagern, Uebersicht,
Chargentabelle). Die Abgelaufen-Warnung prueft bei Monatsangaben gegen den
Monatsletzten - sonst haette eine Packung schon am Monatsersten als abgelaufen
gegolten.

iOS: Auswahl "Tagesdatum / nur Monat/Jahr" im Einlagern-Formular und beim
Anlegen eines Artikels. SwiftUI hat keinen DatePicker ohne Tag, deshalb ein
eigener MonthYearPicker aus zwei Auswahlfeldern; die Jahresliste reicht zwei
Jahre zurueck (bereits abgelaufene Ware) und fuenfzehn nach vorn (Konserven).
Die Chargenauswahl beim Auslagern zeigt Monatsangaben ebenfalls ohne Tag.

Getestet: Web-Build (vite) und iOS-Geraetebuild laufen fehlerfrei durch, die
App ist auf dem iPhone installiert. Das Verhalten in der Oberflaeche ist noch
nicht von Hand durchgeklickt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 17:57:09 +02:00
parent eed7c9e1de
commit 30307d6cfb
13 changed files with 2090 additions and 25 deletions

View File

@@ -6,11 +6,13 @@ 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";
import {
daysUntil, expiryRowClass, fmt, fromMonthInput, isExpired, relativeExpiry, toMonthInput, unitShort,
} from "../units";
const EMPTY = {
barcode: "", name: "", brand: "", image_url: "",
unit_id: "", package_size: "", package_label: "", min_stock: "", group_id: "",
unit_id: "", package_size: "", package_label: "", date_precision: "day", min_stock: "", group_id: "",
};
// Auswahl für die Gebinde-Bezeichnung ("Packung" ist der leere Standardwert).
@@ -143,6 +145,7 @@ export default function ProductForm() {
barcode: p.barcode || "", name: p.name, brand: p.brand || "",
image_url: p.image_url || "", unit_id: uid,
package_size: p.package_size ?? "", package_label: p.package_label || "",
date_precision: p.date_precision === "month" ? "month" : "day",
min_stock: p.min_stock != null ? p.min_stock / f : "",
group_id: p.group_id ?? "",
});
@@ -176,6 +179,7 @@ export default function ProductForm() {
unit_id: form.unit_id === "" ? null : Number(form.unit_id),
package_size: form.package_size === "" ? null : Number(form.package_size),
package_label: form.package_label.trim() || null,
date_precision: form.date_precision || "day",
min_stock: minBase,
min_stock_unit_id: sel === "package" || sel === "" ? null : Number(sel),
min_stock_in_packages: sel === "package",
@@ -291,7 +295,17 @@ export default function ProductForm() {
)}
</select>
</label>
<div className="grow" />
<label className="grow">
MHD-Angabe
<select value={form.date_precision} onChange={(e) => set("date_precision", e.target.value)}
disabled={readOnly}>
<option value="day">Tagesdatum</option>
<option value="month">nur Monat/Jahr</option>
</select>
<span className="muted small">
Voreinstellung beim Einlagern z.B. Konserven tragen oft nur 09/2026.
</span>
</label>
</div>
<div className="row">
<label className="grow">
@@ -378,9 +392,9 @@ 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 { formatBestBefore } = useSettings();
const [editId, setEditId] = useState(null);
const [draft, setDraft] = useState({ quantity: "", best_before: "" });
const [draft, setDraft] = useState({ quantity: "", best_before: "", precision: "day" });
// In welcher Einheit die Menge bearbeitet wird: "unit" oder "package".
const [editUnit, setEditUnit] = useState("unit");
@@ -410,7 +424,14 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
const mode = pkgSize > 0 ? "package" : "unit";
setEditId(l.id);
setEditUnit(mode);
setDraft({ quantity: l.quantity / editFactorOf(mode), best_before: l.best_before || "" });
setDraft({
quantity: l.quantity / editFactorOf(mode),
best_before:
l.best_before_precision === "month"
? toMonthInput(l.best_before)
: l.best_before || "",
precision: l.best_before_precision === "month" ? "month" : "day",
});
}
function changeEditUnit(newMode) {
@@ -426,7 +447,10 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
try {
await api.updateLot(l.id, {
quantity: Number(draft.quantity) * editFactor,
best_before: draft.best_before || null,
best_before:
(draft.precision === "month" ? fromMonthInput(draft.best_before) : draft.best_before) ||
null,
best_before_precision: draft.precision || "day",
});
setEditId(null);
await onChanged();
@@ -498,12 +522,13 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
</td>
<td>
{editing ? (
<input type="date" value={draft.best_before}
<input type={draft.precision === "month" ? "month" : "date"}
value={draft.best_before}
onChange={(e) => setDraft({ ...draft, best_before: e.target.value })}
style={{ marginTop: 0, minWidth: 150 }} />
) : (
<>
{l.best_before ? formatDate(l.best_before) : ""}
{l.best_before ? formatBestBefore(l.best_before, l.best_before_precision) : ""}
{cls && (
<span className={`badge ${cls === "row-danger" ? "danger" : "warn"}`}
style={{ marginLeft: 6 }}>