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:
@@ -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"} ≙
|
{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
|
||||||
|
|||||||
Reference in New Issue
Block a user