Tabellen werden bei wenig Platz zu Karten statt seitwaerts zu scrollen
Waagerechtes Scrollen in einem Block war die falsche Antwort auf zu wenig
Platz - man sieht nicht, dass es weitergeht, und auf dem Trackpad ist es
muehsam. Das Scrollen ist ersatzlos weg.
Stattdessen stellt sich jede Tabelle unter 700px verfuegbarer Breite auf
Karten um: eine Karte je Zeile, jede Angabe mit ihrer Beschriftung davor.
Die Beschriftung kommt aus data-label an der Zelle, die Kopfzeile wird
ausgeblendet. Zellen ohne Beschriftung (Vorschaubild, Aktionsknoepfe) bleiben
ohne eigene Zeile.
Ausgeloest wird das per Container-Abfrage, nicht per Bildschirmbreite. Damit
richtet sich die Darstellung nach dem Platz, den die Tabelle wirklich hat -
sie stellt sich also auch um, wenn das Fenster nur die halbe Bildschirmbreite
einnimmt oder ein Block danebensteht.
Dazu die Aufteilung neben dem Block: Die Tabelle hat mehr Spalten als der
Block und bekommt jetzt den groesseren Anteil (1,5 zu 1, Block mindestens
340px). Vorher waren beide gleich breit, wodurch die Tabelle unnoetig frueh in
die Kartenansicht gerutscht waere, obwohl daneben Platz frei war.
Durchgerechnet: Bei 1920 voller Breite bleibt es eine Tabelle (802px) mit
komfortablem Block daneben (534px). Bei halber Bildschirmbreite stapelt sich
die Seite und die Tabelle wird zur Karte. In keinem Fall wird etwas
abgeschnitten und nirgends muss seitwaerts gescrollt werden.
Lange Zeichenketten in Zellen brechen jetzt um (overflow-wrap), da kein
Scrollen mehr auffaengt, was hinauslaeuft.
Dabei aufgefallen: In der Produkttabelle stand nach dem Ergaenzen der
Kategorie-Spalte noch colSpan={7} statt 8 - die Zeile "Keine Produkte
gefunden" ueberspannte die Tabelle nicht mehr ganz. Alle colSpan-Angaben sind
jetzt gegen die Spaltenzahl gegengeprueft.
Getestet: Web-Build laeuft durch, Spaltenzahl und colSpan stimmen in allen
fuenf betroffenen Tabellen ueberein, die Umbruchpunkte sind durchgerechnet.
Die Darstellung im Browser habe ich nicht selbst angesehen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,7 @@ export default function Categories() {
|
||||
<tbody>
|
||||
{tree.map((c) => (
|
||||
<tr key={c.id}>
|
||||
<td>
|
||||
<td data-label="Kategorie">
|
||||
<span style={{ display: "flex", alignItems: "center", gap: 8,
|
||||
paddingLeft: c.depth * 22 }}>
|
||||
{c.depth > 0 && <span className="muted">↳</span>}
|
||||
@@ -119,7 +119,7 @@ export default function Categories() {
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="num muted">{c.product_count}</td>
|
||||
<td data-label="Artikel" className="num muted">{c.product_count}</td>
|
||||
<td className="num">
|
||||
{isAdmin && (
|
||||
<button className="btn-icon danger" onClick={() => remove(c)} title="Löschen">
|
||||
|
||||
@@ -84,15 +84,15 @@ export default function Dashboard() {
|
||||
<tbody>
|
||||
{expiring.map((it) => (
|
||||
<tr key={it.lot_id} className={it.days_left < 0 ? "row-danger" : "row-warn"}>
|
||||
<td>{it.product_name}</td>
|
||||
<td className="num">
|
||||
<td data-label="Produkt">{it.product_name}</td>
|
||||
<td data-label="Menge" className="num">
|
||||
{articlePrimary(it)}
|
||||
{articleSecondary(it) && (
|
||||
<div className="muted small">{articleSecondary(it)}</div>
|
||||
)}
|
||||
</td>
|
||||
<td>{formatBestBefore(it.best_before, it.best_before_precision)}</td>
|
||||
<td>
|
||||
<td data-label="MHD">{formatBestBefore(it.best_before, it.best_before_precision)}</td>
|
||||
<td data-label="Frist">
|
||||
<span className={`badge ${it.days_left < 0 ? "danger" : "warn"}`}>
|
||||
{relativeExpiry(it.days_left)}
|
||||
</span>
|
||||
@@ -120,16 +120,16 @@ export default function Dashboard() {
|
||||
<tbody>
|
||||
{groupShopping.map((it) => (
|
||||
<tr key={`g${it.group_id}`}>
|
||||
<td><span className="badge accent">Gruppe</span> {it.name}</td>
|
||||
<td className="num">{fmt(it.stock)} {it.unit_name}</td>
|
||||
<td className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
||||
<td data-label="Bedarf"><span className="badge accent">Gruppe</span> {it.name}</td>
|
||||
<td data-label="Bestand" className="num">{fmt(it.stock)} {it.unit_name}</td>
|
||||
<td data-label="Fehlt" className="num strong">{fmt(it.deficit)} {it.unit_name}</td>
|
||||
</tr>
|
||||
))}
|
||||
{shopping.map((it) => (
|
||||
<tr key={`p${it.product_id}`}>
|
||||
<td>{it.name}</td>
|
||||
<td className="num">{amountText(it.stock, it.package_size, it.base_unit)}</td>
|
||||
<td className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
|
||||
<td data-label="Bedarf">{it.name}</td>
|
||||
<td data-label="Bestand" className="num">{amountText(it.stock, it.package_size, it.base_unit)}</td>
|
||||
<td data-label="Fehlt" className="num strong">{amountText(it.deficit, it.package_size, it.base_unit)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function Groups() {
|
||||
const low = g.min_stock != null && g.stock < g.min_stock;
|
||||
return (
|
||||
<tr key={g.id}>
|
||||
<td>
|
||||
<td data-label="Gruppe">
|
||||
{isAdmin ? (
|
||||
<input defaultValue={g.name} style={{ marginTop: 0, minWidth: 130 }}
|
||||
onBlur={(e) => {
|
||||
@@ -114,9 +114,9 @@ export default function Groups() {
|
||||
) : <span className="strong">{g.name}</span>}
|
||||
{low && <span className="badge warn" style={{ marginLeft: 6 }}>niedrig</span>}
|
||||
</td>
|
||||
<td className="num muted">{g.product_count}</td>
|
||||
<td className="num">{fmt(g.stock)} {g.min_stock_unit_name || ""}</td>
|
||||
<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="Mindestbestand">
|
||||
{isAdmin ? (
|
||||
<input type="number" step="any" defaultValue={g.min_stock ?? ""}
|
||||
style={{ marginTop: 0, minWidth: 90 }}
|
||||
@@ -128,7 +128,7 @@ export default function Groups() {
|
||||
}} />
|
||||
) : (g.min_stock != null ? fmt(g.min_stock) : "–")}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Einheit">
|
||||
{isAdmin ? (
|
||||
<select value={g.min_stock_unit_id ?? ""} style={{ marginTop: 0 }}
|
||||
onChange={(e) => patch(g, {
|
||||
@@ -139,7 +139,7 @@ export default function Groups() {
|
||||
</select>
|
||||
) : (g.min_stock_unit_name || "–")}
|
||||
</td>
|
||||
<td className="num">
|
||||
<td data-label="EAN-Codes" className="num">
|
||||
<button className="link-btn" onClick={() => setSelectedId(g.id)}>
|
||||
{/* Beide Herkünfte zählen, sonst steht bei einer Gruppe
|
||||
mit Artikeln irreführend eine 0. */}
|
||||
|
||||
@@ -38,21 +38,21 @@ export default function History() {
|
||||
<tbody>
|
||||
{movements.map((m) => (
|
||||
<tr key={m.id}>
|
||||
<td className="muted">{new Date(m.created_at).toLocaleString("de-DE")}</td>
|
||||
<td>
|
||||
<td data-label="Zeitpunkt" className="muted">{new Date(m.created_at).toLocaleString("de-DE")}</td>
|
||||
<td data-label="Aktion">
|
||||
<span className={`badge ${m.type === "in" ? "ok" : m.type === "out" ? "warn" : ""}`}>
|
||||
<Icon name={m.type === "in" ? "checkin" : m.type === "out" ? "checkout" : "edit"} size={13} />
|
||||
{TYPE_LABEL[m.type] || m.type}
|
||||
</span>
|
||||
</td>
|
||||
<td>{m.product_name}</td>
|
||||
<td className="num">
|
||||
<td data-label="Produkt">{m.product_name}</td>
|
||||
<td data-label="Menge" className="num">
|
||||
{articlePrimary(m)}
|
||||
{articleSecondary(m) && (
|
||||
<div className="muted small">{articleSecondary(m)}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="muted">{m.username || "–"}</td>
|
||||
<td data-label="Benutzer" className="muted">{m.username || "–"}</td>
|
||||
</tr>
|
||||
))}
|
||||
{movements.length === 0 && (
|
||||
|
||||
@@ -541,7 +541,7 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
|
||||
const editing = editId === l.id;
|
||||
return (
|
||||
<tr key={l.id} className={cls}>
|
||||
<td className="num">
|
||||
<td data-label="Menge" className="num">
|
||||
{editing ? (
|
||||
<div className="field-inline" style={{ justifyContent: "flex-end" }}>
|
||||
<input type="number" step="any" min="0" value={draft.quantity}
|
||||
@@ -566,7 +566,7 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="MHD">
|
||||
{editing ? (
|
||||
<input type={draft.precision === "month" ? "month" : "date"}
|
||||
value={draft.best_before}
|
||||
@@ -584,7 +584,7 @@ function LotsCard({ product, lots, baseShort, warnDays, isAdmin, imageUrl, onCha
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
<td className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</td>
|
||||
<td data-label="Eingelagert" className="muted">{new Date(l.created_at).toLocaleDateString("de-DE")}</td>
|
||||
<td className="num">
|
||||
{isAdmin && (editing ? (
|
||||
<div className="btn-pair">
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function Products() {
|
||||
? <img className="thumb" src={p.image_url} alt="" />
|
||||
: <span className="thumb thumb-fallback"><Icon name="box" size={16} /></span>}
|
||||
</td>
|
||||
<td className="strong">
|
||||
<td data-label="Name" className="strong">
|
||||
{p.name}
|
||||
{p.expired_count > 0 && (
|
||||
<span className="badge danger" style={{ marginLeft: 6 }}>
|
||||
@@ -107,18 +107,18 @@ export default function Products() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="muted">{p.brand || "–"}</td>
|
||||
<td className="muted">{p.category_name || "–"}</td>
|
||||
<td>
|
||||
<td data-label="Marke" className="muted">{p.brand || "–"}</td>
|
||||
<td data-label="Kategorie" className="muted">{p.category_name || "–"}</td>
|
||||
<td data-label="Einheit">
|
||||
{p.unit_name || unitShort(p.base_unit)}
|
||||
{p.package_size
|
||||
? ` · ${p.package_label || "Packung"} ${fmt(p.package_size)} ${unitShort(p.base_unit)}`
|
||||
: ""}
|
||||
</td>
|
||||
<td className="num">
|
||||
<td data-label="Bestand" className="num">
|
||||
{fmt(p.stock / (p.unit_factor || 1))} {p.unit_name || unitShort(p.base_unit)}
|
||||
</td>
|
||||
<td className="num">
|
||||
<td data-label="Einheiten" className="num">
|
||||
{fmt(p.stock / unitCount(p))}
|
||||
{p.min_stock != null ? ` / ${fmt(p.min_stock / unitCount(p))}` : ""}{" "}
|
||||
{countLabel(p)}
|
||||
@@ -129,7 +129,7 @@ export default function Products() {
|
||||
);
|
||||
})}
|
||||
{products.length === 0 && (
|
||||
<tr><td colSpan={7} className="empty">Keine Produkte gefunden.</td></tr>
|
||||
<tr><td colSpan={8} className="empty">Keine Produkte gefunden.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function Transfer() {
|
||||
<section className="card">
|
||||
<div className="card-head"><Icon name="package" /><h2>CSV-Aufbau</h2></div>
|
||||
<div className="table-wrap">
|
||||
<table className="table">
|
||||
<table className="table table-compact">
|
||||
<thead><tr><th>Spalte</th><th>Bedeutung</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td className="strong">barcode</td><td>optional; dient zum Wiedererkennen vorhandener Produkte</td></tr>
|
||||
|
||||
@@ -70,9 +70,9 @@ export default function Units() {
|
||||
<tbody>
|
||||
{units.map((u) => (
|
||||
<tr key={u.id}>
|
||||
<td className="strong">{u.name}{u.is_builtin && <span className="badge" style={{ marginLeft: 6 }}>eingebaut</span>}</td>
|
||||
<td className="muted">{KIND_LABEL[u.kind] || u.kind}</td>
|
||||
<td className="num">{fmt(u.factor)} {baseUnitOfKind[u.kind]}</td>
|
||||
<td data-label="Name" className="strong">{u.name}{u.is_builtin && <span className="badge" style={{ marginLeft: 6 }}>eingebaut</span>}</td>
|
||||
<td data-label="Art" className="muted">{KIND_LABEL[u.kind] || u.kind}</td>
|
||||
<td data-label="Faktor" className="num">{fmt(u.factor)} {baseUnitOfKind[u.kind]}</td>
|
||||
<td className="num">
|
||||
{!u.is_builtin && (
|
||||
<button className="btn-icon danger" onClick={() => remove(u)} title="Löschen">
|
||||
|
||||
@@ -79,10 +79,10 @@ export default function Users() {
|
||||
<tbody>
|
||||
{users.map((u) => (
|
||||
<tr key={u.id}>
|
||||
<td className="strong">
|
||||
<td data-label="Name" className="strong">
|
||||
{u.username}{u.id === me.id && <span className="muted"> (du)</span>}
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="Rolle">
|
||||
<select value={u.role} onChange={(e) => changeRole(u, e.target.value)}
|
||||
disabled={u.id === me.id}>
|
||||
<option value="user">Benutzer</option>
|
||||
|
||||
Reference in New Issue
Block a user