Tabellen entzerrt, EAN-Codes mit Marke und Artikelname, Codes-Block nach unten

EAN-Codes:
- Das Kennzeichen "Artikel" ist weg; stattdessen stehen Marke und Artikelname
  zusammen ("Zweifel - Moutarde Original Chips"). Die Marke allein reichte nicht,
  um einen Code wiederzuerkennen. Hinweistext entsprechend angepasst.

Produktseite:
- "Weitere EAN-Codes" steht jetzt unterhalb von Formular und Chargen statt als
  dritte Spalte dazwischen. Die Codes braucht man selten und sie schoben die
  beiden wichtigen Bloecke auseinander.

Tabellen:
- "overflow-wrap: anywhere" galt fuer ALLE Zellen und zerriss Woerter mitten
  drin ("PROD UKTE", "555 Gram m"). Jetzt nur noch gezielt per Klasse.
- Kopfzeilen und Zahl-plus-Einheit brechen nicht mehr um.
- Eingaben in Tabellen sind kompakter (kleinere Polsterung, Zahlenfelder 8ch),
  Auswahlfelder richten sich nach ihrem Text statt 132px Mindestbreite -
  dadurch sind die Zeilen deutlich schlanker.
- Kennzeichen wie "niedrig" oder "abgelaufen" sitzen ueber .cell-row in einer
  Zeile mit dem Wert und rutschen nicht mehr je nach Zeilenhoehe herum.

Geprueft: "npm run build" laeuft durch. Backend unveraendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-23 07:50:42 +02:00
parent d804666a95
commit 32d49b47bd
5 changed files with 80 additions and 57 deletions

View File

@@ -11,11 +11,12 @@ import Icon from "./Icon";
* *
* onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt. * onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt.
*/ */
/** Innerhalb einer Gruppe unterscheidet die Marke besser als der Name /** Marke und Artikelname zusammen die Marke allein reichte nicht, um einen
* in der Gruppe "Mehl" heisst jeder Artikel irgendwie "…mehl". Fehlt die * Code wiederzuerkennen ("Zweifel" gibt es mehrfach), der Name allein auch
* Marke, bleibt der Name als Rueckfall. */ * nicht (in "Mehl" heisst alles irgendwie Mehl). Fehlt eines von beiden,
* bleibt das andere stehen. */
function besitzer(b) { function besitzer(b) {
return b.product_brand || b.product_name || ""; return [b.product_brand, b.product_name].filter(Boolean).join(" · ");
} }
export default function BarcodeList({ export default function BarcodeList({
@@ -45,8 +46,7 @@ export default function BarcodeList({
<li key={`p-${b.code}`}> <li key={`p-${b.code}`}>
<span className="code-row"> <span className="code-row">
<code className="strong">{b.code}</code> <code className="strong">{b.code}</code>
<span className="badge">Artikel</span> <span className="muted small owner" title={besitzer(b)}>{besitzer(b)}</span>
<span className="muted small owner" title={b.product_name}>{besitzer(b)}</span>
</span> </span>
</li> </li>
))} ))}
@@ -57,10 +57,7 @@ export default function BarcodeList({
{b.product_name ? ( {b.product_name ? (
/* Gehört der Code zu einem Artikel, sagt dessen Marke bereits, /* Gehört der Code zu einem Artikel, sagt dessen Marke bereits,
worum es geht eine Notiz waere hier nur Dopplung. */ worum es geht eine Notiz waere hier nur Dopplung. */
<> <span className="muted small owner" title={besitzer(b)}>{besitzer(b)}</span>
<span className="badge">Artikel</span>
<span className="muted small owner" title={b.product_name}>{besitzer(b)}</span>
</>
) : ( ) : (
/* Codes ohne Artikel haben keine Marke. Hier ist die Notiz die /* Codes ohne Artikel haben keine Marke. Hier ist die Notiz die
einzige Angabe, woran man den Code wiedererkennt. */ einzige Angabe, woran man den Code wiedererkennt. */

View File

@@ -105,14 +105,16 @@ export default function Groups() {
return ( return (
<tr key={g.id}> <tr key={g.id}>
<td data-label="Gruppe"> <td data-label="Gruppe">
<span className="cell-row">
{isAdmin ? ( {isAdmin ? (
<input defaultValue={g.name} style={{ marginTop: 0, minWidth: 130 }} <input defaultValue={g.name} style={{ marginTop: 0, minWidth: 120 }}
onBlur={(e) => { onBlur={(e) => {
const v = e.target.value.trim(); const v = e.target.value.trim();
if (v && v !== g.name) patch(g, { name: v }); if (v && v !== g.name) patch(g, { name: v });
}} /> }} />
) : <span className="strong">{g.name}</span>} ) : <span className="strong">{g.name}</span>}
{low && <span className="badge warn" style={{ marginLeft: 6 }}>niedrig</span>} {low && <span className="badge warn">niedrig</span>}
</span>
</td> </td>
<td data-label="Produkte" className="num muted">{g.product_count}</td> <td data-label="Produkte" className="num muted">{g.product_count}</td>
<td data-label="Bestand" className="num">{fmt(g.stock)} {g.min_stock_unit_name || ""}</td> <td data-label="Bestand" className="num">{fmt(g.stock)} {g.min_stock_unit_name || ""}</td>
@@ -173,10 +175,10 @@ export default function Groups() {
barcodes={selected.barcodes || []} barcodes={selected.barcodes || []}
productBarcodes={selected.product_barcodes || []} productBarcodes={selected.product_barcodes || []}
disabled={!isAdmin} disabled={!isAdmin}
hint={"Codes der zugeordneten Artikel stehen automatisch hier, gekennzeichnet mit " + hint={"Codes der zugeordneten Artikel stehen automatisch hier, mit Marke und " +
"Artikel“ und der Marke; entfernen lassen sie sich nur, indem der Artikel " + "Artikelnamen; entfernen lassen sie sich nur, indem der Artikel die Gruppe " +
"die Gruppe wechselt. Zusätzlich sind Codes möglich, die noch zu keinem " + "wechselt. Zusätzlich sind Codes möglich, die noch zu keinem Artikel " +
"Artikel gehören für die gibt es eine Notiz, weil ihnen die Marke fehlt. " + "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 " + "Wird so ein Code beim Einlagern gescannt, landet der neu angelegte Artikel " +
"automatisch hier."} "automatisch hier."}
onAdd={async (body) => { onAdd={async (body) => {

View File

@@ -387,6 +387,25 @@ export default function ProductForm() {
{readOnly && <p className="muted small mt-0">Nur Administratoren können Produkte bearbeiten.</p>} {readOnly && <p className="muted small mt-0">Nur Administratoren können Produkte bearbeiten.</p>}
</form> </form>
{!isNew && (
<LotsCard
product={product}
lots={lots}
baseShort={baseShort}
warnDays={warnDays}
isAdmin={isAdmin}
imageUrl={form.image_url}
onChanged={async () => {
setLots(await api.listLots(id));
setProduct(await api.getProduct(id));
}}
onError={setError}
/>
)}
</div>
{/* Unterhalb der beiden Spalten: die Codes braucht man selten, sie sollen
Formular und Chargen nicht auseinanderschieben. */}
{!isNew && product && ( {!isNew && product && (
<section className="card"> <section className="card">
<div className="card-head"><Icon name="search" /><h2>Weitere EAN-Codes</h2></div> <div className="card-head"><Icon name="search" /><h2>Weitere EAN-Codes</h2></div>
@@ -408,23 +427,6 @@ export default function ProductForm() {
/> />
</section> </section>
)} )}
{!isNew && (
<LotsCard
product={product}
lots={lots}
baseShort={baseShort}
warnDays={warnDays}
isAdmin={isAdmin}
imageUrl={form.image_url}
onChanged={async () => {
setLots(await api.listLots(id));
setProduct(await api.getProduct(id));
}}
onError={setError}
/>
)}
</div>
</div> </div>
); );
} }

View File

@@ -100,12 +100,14 @@ export default function Products() {
: <span className="thumb thumb-fallback"><Icon name="box" size={16} /></span>} : <span className="thumb thumb-fallback"><Icon name="box" size={16} /></span>}
</td> </td>
<td data-label="Name" className="strong"> <td data-label="Name" className="strong">
{p.name} <span className="cell-row">
<span>{p.name}</span>
{p.expired_count > 0 && ( {p.expired_count > 0 && (
<span className="badge danger" style={{ marginLeft: 6 }}> <span className="badge danger nowrap">
<Icon name="alert" size={12} />{p.expired_count} abgelaufen <Icon name="alert" size={12} />{p.expired_count} abgelaufen
</span> </span>
)} )}
</span>
</td> </td>
<td data-label="Marke" className="muted">{p.brand || ""}</td> <td data-label="Marke" className="muted">{p.brand || ""}</td>
<td data-label="Kategorie" className="muted">{p.category_name || ""}</td> <td data-label="Kategorie" className="muted">{p.category_name || ""}</td>
@@ -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 / (p.unit_factor || 1))} {p.unit_name || unitShort(p.base_unit)}
</td> </td>
<td data-label="Einheiten" className="num"> <td data-label="Einheiten" className="num">
<span className="cell-row" style={{ justifyContent: "flex-end" }}>
<span>
{fmt(p.stock / unitCount(p))} {fmt(p.stock / unitCount(p))}
{p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "} {p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "}
{countLabel(p)} {countLabel(p)}
{low && <span className="badge warn" style={{ marginLeft: 6 }}>niedrig</span>} </span>
{low && <span className="badge warn">niedrig</span>}
</span>
</td> </td>
<td className="num"><Link to={`/products/${p.id}`}>Details</Link></td> <td className="num"><Link to={`/products/${p.id}`}>Details</Link></td>
</tr> </tr>

View File

@@ -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 > input { flex: 1 1 auto; min-width: 78px; }
.field-inline > select { flex: 0 0 auto; width: auto; min-width: 132px; } .field-inline > select { flex: 0 0 auto; width: auto; min-width: 132px; }
/* Auswahlfelder in Tabellen richten sich nach ihrem Text statt nach der Zelle. */ /* 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. */ /* 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 { 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; } .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-wrap { max-width: 100%; container-type: inline-size; }
.table { width: 100%; border-collapse: collapse; font-size: 0.875rem; } .table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.table th, .table td { .table th, .table td {
text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--border); text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border);
overflow-wrap: anywhere;
} }
/* 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 { .table thead th {
color: var(--muted); font-weight: 600; font-size: 0.72rem; color: var(--muted); font-weight: 600; font-size: 0.72rem;
text-transform: uppercase; letter-spacing: 0.04em; text-transform: uppercase; letter-spacing: 0.04em;
border-bottom: 1px solid var(--border-strong); 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:last-child td { border-bottom: none; }
.table tbody tr:hover { background: var(--surface-2); } .table tbody tr:hover { background: var(--surface-2); }
.num { text-align: right; font-variant-numeric: tabular-nums; } .num { text-align: right; font-variant-numeric: tabular-nums; }