Web: Einheiten-Felder als zusammenhaengender Block

Im Grillwurst-Fall stand dieselbe Zahl zweimal im Formular, mit zwei
Bedeutungen und in identischer Aufmachung: "Packungsgroesse 3" (1 Packung = 3
Stueck) und "Zweiteinheit 3 Stk ≙ 250 g" (3 Stueck wiegen 250 g). Dazu sass der
Zweiteinheit-Block zwischen Gebinde/MHD und Mindestbestand/Gruppe, hiess nach
einem Fachbegriff und benutzte ≙ statt eines Satzes.

Jetzt steht alles zur Einheit unter einer Ueberschrift und liest sich als Satz:
"Gezaehlt wird in Stueck", "Gebinde Packung", "1 Packung sind 3 Stueck". Das
Gebinde steht dafuer vor der Packungsgroesse - die Zeile darunter nennt es beim
Namen.

Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen / Stueckzahl)
und beginnt mit einer einzigen Auswahl; erst danach erscheint der Satz "3 Stk
wiegen 250 g" mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim
Waehlen der Zielart EINMALIG mit der Packungsgroesse vorbelegt - zu tippen
bleibt nur die 250. Bewusst keine dauerhafte Kopplung: wer die Packung spaeter
auf 4 aendert, hat dieselbe Wurst, und ein mitgezogener Wert wuerde
stillschweigend behaupten, sie sei leichter geworden.

MHD-Angabe wandert aus dem Einheiten-Block zur Gruppen-Zeile; sie stand nur
zufaellig neben dem Gebinde.

Reine Oberflaeche - Datenmodell, API und Backend-Tests unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-08-16 09:19:08 +02:00
parent 71d52917e4
commit 37bc7663af

View File

@@ -590,6 +590,40 @@ export default function ProductForm() {
const BASE_SHORT_OF_KIND = { count: "Stk", weight: "g", volume: "ml" };
// Basiseinheit je Art die eigene Art fliegt aus der Zweiteinheit-Auswahl.
const BASE_OF_KIND = { count: "piece", weight: "gram", volume: "milliliter" };
// Die Umrechnung heißt nach dem, was sie liefert „Zweiteinheit" musste man
// erst übersetzen. Ohne gewählte Zielart ein neutraler Titel.
const ZWEIT_TITEL = { gram: "Gewicht", milliliter: "Volumen", piece: "Stückzahl" };
// Verb passend zur Zielart und zur Anzahl: „1 Stück wiegt" / „3 Stück wiegen".
const ZWEIT_VERB = {
gram: ["wiegt", "wiegen"], milliliter: ["fasst", "fassen"], piece: ["ist", "sind"],
};
const zweitTitel = ZWEIT_TITEL[form.secondary_base] || "Umrechnung";
const zweitVerb = (() => {
const paar = ZWEIT_VERB[form.secondary_base];
if (!paar) return "";
return Number(form.secondary_count) === 1 ? paar[0] : paar[1];
})();
// Das Gebinde beim Namen nennen: „1 Packung sind …". Ohne Auswahl der
// Standard, sonst stünde dort „1 sind 3 Stück".
const gebindeName = form.package_label.trim() || "Packung";
// Zielart wählen: die linke Zahl einmalig mit der Packungsgröße vorbelegen
// im Normalfall ist sie genau das („1 Packung = 3 Stück, und die wiegen
// 250 g"). BEWUSST keine dauerhafte Kopplung: wer die Packung später auf 4
// ändert, hat dieselbe Wurst, und ein mitgezogener Wert würde stillschweigend
// behaupten, sie sei leichter geworden.
function waehleZweitart(basis) {
if (basis && form.secondary_count === "") {
setForm((f) => ({
...f,
secondary_base: basis,
secondary_count: f.package_size !== "" ? f.package_size : "1",
}));
return;
}
set("secondary_base", basis);
}
// Ausgerechneter Wert als Kontrolle: „1 Stk ≈ 83,33 g".
const zweitHinweis = (() => {
const f = zweitFaktor({
@@ -600,6 +634,7 @@ export default function ProductForm() {
if (!f) return "";
return `1 ${BASE_SHORT_OF_KIND[selectedUnit?.kind] || "Einheit"} \u2248 `
+ `${fmt(f)} ${unitShort(form.secondary_base)}. `
+ "Damit zählt der Artikel auch in Gruppen, die in dieser Einheit rechnen. "
+ "Bestände bleiben beim Ändern unverändert nur ihre Umrechnung verschiebt sich.";
})();
const baseShort = selectedUnit
@@ -799,9 +834,17 @@ export default function ProductForm() {
{/* Lebensmittel ODER Verbrauchsgegenstand: Einheit, Packung, MHD,
Mindestbestand (mit Einheit), Gruppe, Mindestbestand je Lagerort. */}
{foodLike && (<>
{/* Alles rund um Einheit, Packung und Umrechnung steht zusammen und
liest sich als Satz. Vorher lagen die Felder verstreut, und die
Packungsgroesse stand als blanke Zahl neben einer zweiten, gleich
aussehenden Zahl aus der Umrechnung das war nicht zu trennen. */}
<div className="card-head" style={{ marginBottom: "var(--sp-1)" }}>
<Icon name="package" size={16} />
<h3 style={{ margin: 0 }}>Einheit und Menge</h3>
</div>
<div className="row">
<label className="grow">
Einheit
Gezählt wird in
<select value={form.unit_id} onChange={(e) => set("unit_id", e.target.value)} disabled={readOnly} required>
<option value=""> wählen </option>
{KIND_ORDER.filter((k) => units.some((u) => u.kind === k)).map((k) => (
@@ -813,13 +856,8 @@ export default function ProductForm() {
))}
</select>
</label>
<label className="grow">
Packungsgröße{baseShort ? ` (in ${baseShort})` : ""}
<input type="number" step="any" value={form.package_size}
onChange={(e) => set("package_size", e.target.value)} disabled={readOnly} placeholder="z.B. 500" />
</label>
</div>
<div className="row">
{/* Gebinde VOR der Packungsgröße: die Zeile darunter nennt es beim
Namen („1 Packung sind …"). */}
<label className="grow">
<span className="tip" title="Verwaltet unter Verwaltung → Gebinde, dort auch mit Mehrzahl.">
Gebinde
@@ -837,46 +875,56 @@ export default function ProductForm() {
)}
</select>
</label>
</div>
<div className="row">
<label className="grow">
<span className="tip" title="Voreinstellung beim Einlagern z.B. Konserven tragen oft nur „09/2026“.">
MHD-Angabe
1 {gebindeName} sind
<div className="field-inline">
<input type="number" step="any" style={{ maxWidth: 120 }} value={form.package_size}
onChange={(e) => set("package_size", e.target.value)} disabled={readOnly} placeholder="z.B. 500" />
<span className="muted small" style={{ alignSelf: "center" }}>{baseShort}</span>
</div>
</label>
</div>
{/* Umrechnung in die ANDERE Art: „3 Stück wiegen 250 g". Solange
keine Zielart gewählt ist, steht hier nur eine Auswahl. */}
<div className="card-head" style={{ marginBottom: "var(--sp-1)", marginTop: "var(--sp-2)" }}>
<Icon name="split" size={16} />
<h3 style={{ margin: 0 }}>{zweitTitel} (optional)</h3>
</div>
<div className="row">
<label className="grow" style={{ maxWidth: 260 }}>
<span className="tip" title="Zweite Lesart desselben Artikels damit zählt er auch in Gruppen, die in einer anderen Einheit rechnen.">
Umrechnen in
</span>
<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 value={form.secondary_base} disabled={readOnly}
onChange={(e) => waehleZweitart(e.target.value)}>
<option value=""> keine </option>
{BASE_UNITS
.filter((b) => !selectedUnit || b.value !== BASE_OF_KIND[selectedUnit.kind])
.map((b) => <option key={b.value} value={b.value}>{b.label}</option>)}
</select>
</label>
</div>
{/* Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen.
Links steht die Basiseinheit des Artikels fest, rechts die
Gegenart. Ohne Eintrag bleibt alles wie bisher getrennt. */}
<div className="row">
<label className="grow">
<span className="tip" title="Zusätzliche Lesart desselben Artikels. Damit zählt er auch in Gruppen der anderen Art und lässt sich darin ein- und auslagern. Das Gebinde bleibt davon unberührt.">
Zweiteinheit (optional)
</span>
{/* Der Satz traegt sich selbst eine Beschriftung davor waere nur
ein weiteres Wort zwischen zwei Zahlen. */}
{form.secondary_base && (
<div className="row">
<div className="field-inline">
<input type="number" step="any" min="0" style={{ maxWidth: 90 }}
placeholder="3" value={form.secondary_count} disabled={readOnly}
onChange={(e) => set("secondary_count", e.target.value)} />
<span className="muted small" style={{ alignSelf: "center" }}>
{baseShort || "Einheiten"} &#8793;
<span style={{ alignSelf: "center" }}>
{baseShort || "Einheiten"} {zweitVerb}
</span>
<input type="number" step="any" min="0" style={{ maxWidth: 90 }}
placeholder="250" value={form.secondary_amount} disabled={readOnly}
onChange={(e) => set("secondary_amount", e.target.value)} />
<select value={form.secondary_base} disabled={readOnly}
style={{ marginTop: 0, maxWidth: 150 }}
onChange={(e) => set("secondary_base", e.target.value)}>
<option value=""> keine </option>
{BASE_UNITS
.filter((b) => !selectedUnit || b.value !== BASE_OF_KIND[selectedUnit.kind])
.map((b) => <option key={b.value} value={b.value}>{b.label}</option>)}
</select>
<span style={{ alignSelf: "center" }}>{unitShort(form.secondary_base)}</span>
</div>
</label>
</div>
</div>
)}
{zweitHinweis && <p className="muted small mt-0">{zweitHinweis}</p>}
<div className="row">
{/* Beim Anlegen gibt es den Artikel noch nicht, also auch keine
@@ -904,6 +952,16 @@ export default function ProductForm() {
</div>
</label>
)}
<label className="grow">
<span className="tip" title="Voreinstellung beim Einlagern z.B. Konserven tragen oft nur „09/2026“.">
MHD-Angabe
</span>
<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>
</label>
<label className="grow">
<span className="tip" title="Zählt Bestände mehrerer Artikel zusammen. Der EAN-Code dieses Artikels erscheint danach automatisch bei der Gruppe.">
Gruppe