diff --git a/web/src/components/BarcodeList.jsx b/web/src/components/BarcodeList.jsx index 72cb96c..2ebe66d 100644 --- a/web/src/components/BarcodeList.jsx +++ b/web/src/components/BarcodeList.jsx @@ -11,11 +11,12 @@ import Icon from "./Icon"; * * onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt. */ -/** Innerhalb einer Gruppe unterscheidet die Marke besser als der Name – - * in der Gruppe "Mehl" heisst jeder Artikel irgendwie "…mehl". Fehlt die - * Marke, bleibt der Name als Rueckfall. */ +/** Marke und Artikelname zusammen – die Marke allein reichte nicht, um einen + * Code wiederzuerkennen ("Zweifel" gibt es mehrfach), der Name allein auch + * nicht (in "Mehl" heisst alles irgendwie Mehl). Fehlt eines von beiden, + * bleibt das andere stehen. */ function besitzer(b) { - return b.product_brand || b.product_name || ""; + return [b.product_brand, b.product_name].filter(Boolean).join(" · "); } export default function BarcodeList({ @@ -45,8 +46,7 @@ export default function BarcodeList({
  • {b.code} - Artikel - {besitzer(b)} + {besitzer(b)}
  • ))} @@ -57,10 +57,7 @@ export default function BarcodeList({ {b.product_name ? ( /* Gehört der Code zu einem Artikel, sagt dessen Marke bereits, worum es geht – eine Notiz waere hier nur Dopplung. */ - <> - Artikel - {besitzer(b)} - + {besitzer(b)} ) : ( /* Codes ohne Artikel haben keine Marke. Hier ist die Notiz die einzige Angabe, woran man den Code wiedererkennt. */ diff --git a/web/src/pages/Groups.jsx b/web/src/pages/Groups.jsx index 3b99fc0..fed1ac5 100644 --- a/web/src/pages/Groups.jsx +++ b/web/src/pages/Groups.jsx @@ -105,14 +105,16 @@ export default function Groups() { return ( - {isAdmin ? ( - { - const v = e.target.value.trim(); - if (v && v !== g.name) patch(g, { name: v }); - }} /> - ) : {g.name}} - {low && niedrig} + + {isAdmin ? ( + { + const v = e.target.value.trim(); + if (v && v !== g.name) patch(g, { name: v }); + }} /> + ) : {g.name}} + {low && niedrig} + {g.product_count} {fmt(g.stock)} {g.min_stock_unit_name || ""} @@ -173,10 +175,10 @@ export default function Groups() { barcodes={selected.barcodes || []} productBarcodes={selected.product_barcodes || []} disabled={!isAdmin} - hint={"Codes der zugeordneten Artikel stehen automatisch hier, gekennzeichnet mit " + - "„Artikel“ und der Marke; entfernen lassen sie sich nur, indem der Artikel " + - "die Gruppe wechselt. Zusätzlich sind Codes möglich, die noch zu keinem " + - "Artikel gehören – für die gibt es eine Notiz, weil ihnen die Marke fehlt. " + + hint={"Codes der zugeordneten Artikel stehen automatisch hier, mit Marke und " + + "Artikelnamen; entfernen lassen sie sich nur, indem der Artikel die Gruppe " + + "wechselt. Zusätzlich sind Codes möglich, die noch zu keinem Artikel " + + "gehören – für die gibt es eine Notiz, weil ihnen der Artikelname fehlt. " + "Wird so ein Code beim Einlagern gescannt, landet der neu angelegte Artikel " + "automatisch hier."} onAdd={async (body) => { diff --git a/web/src/pages/ProductForm.jsx b/web/src/pages/ProductForm.jsx index 642723e..f67d91e 100644 --- a/web/src/pages/ProductForm.jsx +++ b/web/src/pages/ProductForm.jsx @@ -387,28 +387,6 @@ export default function ProductForm() { {readOnly &&

    Nur Administratoren können Produkte bearbeiten.

    } - {!isNew && product && ( -
    -

    Weitere EAN-Codes

    - { - try { - setProduct(await api.addProductBarcode(id, body)); - } catch (err) { setError(err.message); } - }} - onDelete={async (code) => { - try { - await api.deleteProductBarcode(id, code); - setProduct(await api.getProduct(id)); - } catch (err) { setError(err.message); } - }} - /> -
    - )} - {!isNew && ( )} + + {/* Unterhalb der beiden Spalten: die Codes braucht man selten, sie sollen + Formular und Chargen nicht auseinanderschieben. */} + {!isNew && product && ( +
    +

    Weitere EAN-Codes

    + { + try { + setProduct(await api.addProductBarcode(id, body)); + } catch (err) { setError(err.message); } + }} + onDelete={async (code) => { + try { + await api.deleteProductBarcode(id, code); + setProduct(await api.getProduct(id)); + } catch (err) { setError(err.message); } + }} + /> +
    + )} ); } diff --git a/web/src/pages/Products.jsx b/web/src/pages/Products.jsx index a8f66a8..9ed6906 100644 --- a/web/src/pages/Products.jsx +++ b/web/src/pages/Products.jsx @@ -100,12 +100,14 @@ export default function Products() { : } - {p.name} - {p.expired_count > 0 && ( - - {p.expired_count} abgelaufen - - )} + + {p.name} + {p.expired_count > 0 && ( + + {p.expired_count} abgelaufen + + )} + {p.brand || "–"} {p.category_name || "–"} @@ -119,10 +121,14 @@ export default function Products() { {fmt(p.stock / (p.unit_factor || 1))} {p.unit_name || unitShort(p.base_unit)} - {fmt(p.stock / unitCount(p))} - {p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "} - {countLabel(p)} - {low && niedrig} + + + {fmt(p.stock / unitCount(p))} + {p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "} + {countLabel(p)} + + {low && niedrig} + Details diff --git a/web/src/styles.css b/web/src/styles.css index f591042..6b63ae2 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -203,7 +203,9 @@ td .field-inline { margin-bottom: 0; flex-wrap: wrap; } .field-inline > input { flex: 1 1 auto; min-width: 78px; } .field-inline > select { flex: 0 0 auto; width: auto; min-width: 132px; } /* Auswahlfelder in Tabellen richten sich nach ihrem Text statt nach der Zelle. */ -td select { width: auto; min-width: 132px; max-width: 100%; } +/* Auswahlfelder richten sich nach ihrem Text. Die fruehere Mindestbreite von + 132px machte schmale Spalten (z.B. "Einheit") unnoetig breit. */ +td select { width: auto; min-width: 0; max-width: 100%; } /* Button-Paare (z.B. Speichern/Abbrechen) gleich breit halten. */ .btn-pair { display: flex; gap: var(--sp-2); flex-wrap: wrap; justify-content: flex-end; } .btn-pair .btn { flex: 1 1 auto; min-width: 104px; justify-content: center; } @@ -242,14 +244,28 @@ td select { width: auto; min-width: 132px; max-width: 100%; } .table-wrap { max-width: 100%; container-type: inline-size; } .table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } .table th, .table td { - text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--border); - overflow-wrap: anywhere; + text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); } +/* Frueher stand hier "overflow-wrap: anywhere" fuer ALLE Zellen. Das zerriss + Woerter mitten drin ("PROD UKTE", "555 Gram m"), sobald eine Spalte knapp + wurde. Umbrechen nur noch dort, wo wirklich lange Zeichenketten stehen. */ +.break-anywhere { overflow-wrap: anywhere; } +.nowrap { white-space: nowrap; } .table thead th { color: var(--muted); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-strong); + white-space: nowrap; } +/* Werte aus Zahl + Einheit gehoeren zusammen und duerfen nicht umbrechen. */ +.table td.num { white-space: nowrap; } +/* Eingaben in Tabellen kompakt halten - mit voller Feldhoehe blaeht sich jede + Zeile auf und die Spalten werden unnoetig breit. */ +.table td input, .table td select { padding: 5px 8px; font-size: 0.85rem; } +.table td input[type="number"] { width: 8ch; min-width: 0; } +/* Zelle mit Eingabe plus Kennzeichen (z.B. "niedrig"): eine Zeile, damit das + Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */ +.cell-row { display: flex; align-items: center; gap: 6px; } .table tbody tr:last-child td { border-bottom: none; } .table tbody tr:hover { background: var(--surface-2); } .num { text-align: right; font-variant-numeric: tabular-nums; }