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" }; const BASE_SHORT_OF_KIND = { count: "Stk", weight: "g", volume: "ml" };
// Basiseinheit je Art die eigene Art fliegt aus der Zweiteinheit-Auswahl. // Basiseinheit je Art die eigene Art fliegt aus der Zweiteinheit-Auswahl.
const BASE_OF_KIND = { count: "piece", weight: "gram", volume: "milliliter" }; 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". // Ausgerechneter Wert als Kontrolle: „1 Stk ≈ 83,33 g".
const zweitHinweis = (() => { const zweitHinweis = (() => {
const f = zweitFaktor({ const f = zweitFaktor({
@@ -600,6 +634,7 @@ export default function ProductForm() {
if (!f) return ""; if (!f) return "";
return `1 ${BASE_SHORT_OF_KIND[selectedUnit?.kind] || "Einheit"} \u2248 ` return `1 ${BASE_SHORT_OF_KIND[selectedUnit?.kind] || "Einheit"} \u2248 `
+ `${fmt(f)} ${unitShort(form.secondary_base)}. ` + `${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."; + "Bestände bleiben beim Ändern unverändert nur ihre Umrechnung verschiebt sich.";
})(); })();
const baseShort = selectedUnit const baseShort = selectedUnit
@@ -799,9 +834,17 @@ export default function ProductForm() {
{/* Lebensmittel ODER Verbrauchsgegenstand: Einheit, Packung, MHD, {/* Lebensmittel ODER Verbrauchsgegenstand: Einheit, Packung, MHD,
Mindestbestand (mit Einheit), Gruppe, Mindestbestand je Lagerort. */} Mindestbestand (mit Einheit), Gruppe, Mindestbestand je Lagerort. */}
{foodLike && (<> {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"> <div className="row">
<label className="grow"> <label className="grow">
Einheit Gezählt wird in
<select value={form.unit_id} onChange={(e) => set("unit_id", e.target.value)} disabled={readOnly} required> <select value={form.unit_id} onChange={(e) => set("unit_id", e.target.value)} disabled={readOnly} required>
<option value=""> wählen </option> <option value=""> wählen </option>
{KIND_ORDER.filter((k) => units.some((u) => u.kind === k)).map((k) => ( {KIND_ORDER.filter((k) => units.some((u) => u.kind === k)).map((k) => (
@@ -813,13 +856,8 @@ export default function ProductForm() {
))} ))}
</select> </select>
</label> </label>
<label className="grow"> {/* Gebinde VOR der Packungsgröße: die Zeile darunter nennt es beim
Packungsgröße{baseShort ? ` (in ${baseShort})` : ""} Namen („1 Packung sind …"). */}
<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">
<label className="grow"> <label className="grow">
<span className="tip" title="Verwaltet unter Verwaltung → Gebinde, dort auch mit Mehrzahl."> <span className="tip" title="Verwaltet unter Verwaltung → Gebinde, dort auch mit Mehrzahl.">
Gebinde Gebinde
@@ -837,46 +875,56 @@ export default function ProductForm() {
)} )}
</select> </select>
</label> </label>
</div>
<div className="row">
<label className="grow"> <label className="grow">
<span className="tip" title="Voreinstellung beim Einlagern z.B. Konserven tragen oft nur „09/2026“."> 1 {gebindeName} sind
MHD-Angabe <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> </span>
<select value={form.date_precision} onChange={(e) => set("date_precision", e.target.value)} <select value={form.secondary_base} disabled={readOnly}
disabled={readOnly}> onChange={(e) => waehleZweitart(e.target.value)}>
<option value="day">Tagesdatum</option> <option value=""> keine </option>
<option value="month">nur Monat/Jahr</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> </select>
</label> </label>
</div> </div>
{/* Zweiteinheit: dieselbe Ware in der ANDEREN Art lesbar machen. {/* Der Satz traegt sich selbst eine Beschriftung davor waere nur
Links steht die Basiseinheit des Artikels fest, rechts die ein weiteres Wort zwischen zwei Zahlen. */}
Gegenart. Ohne Eintrag bleibt alles wie bisher getrennt. */} {form.secondary_base && (
<div className="row"> <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>
<div className="field-inline"> <div className="field-inline">
<input type="number" step="any" min="0" style={{ maxWidth: 90 }} <input type="number" step="any" min="0" style={{ maxWidth: 90 }}
placeholder="3" value={form.secondary_count} disabled={readOnly} placeholder="3" value={form.secondary_count} disabled={readOnly}
onChange={(e) => set("secondary_count", e.target.value)} /> onChange={(e) => set("secondary_count", e.target.value)} />
<span className="muted small" style={{ alignSelf: "center" }}> <span style={{ alignSelf: "center" }}>
{baseShort || "Einheiten"} &#8793; {baseShort || "Einheiten"} {zweitVerb}
</span> </span>
<input type="number" step="any" min="0" style={{ maxWidth: 90 }} <input type="number" step="any" min="0" style={{ maxWidth: 90 }}
placeholder="250" value={form.secondary_amount} disabled={readOnly} placeholder="250" value={form.secondary_amount} disabled={readOnly}
onChange={(e) => set("secondary_amount", e.target.value)} /> onChange={(e) => set("secondary_amount", e.target.value)} />
<select value={form.secondary_base} disabled={readOnly} <span style={{ alignSelf: "center" }}>{unitShort(form.secondary_base)}</span>
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>
</div> </div>
</label> </div>
</div> )}
{zweitHinweis && <p className="muted small mt-0">{zweitHinweis}</p>} {zweitHinweis && <p className="muted small mt-0">{zweitHinweis}</p>}
<div className="row"> <div className="row">
{/* Beim Anlegen gibt es den Artikel noch nicht, also auch keine {/* Beim Anlegen gibt es den Artikel noch nicht, also auch keine
@@ -904,6 +952,16 @@ export default function ProductForm() {
</div> </div>
</label> </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"> <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."> <span className="tip" title="Zählt Bestände mehrerer Artikel zusammen. Der EAN-Code dieses Artikels erscheint danach automatisch bei der Gruppe.">
Gruppe Gruppe