Files
Vorrania/web/src/pages/Transfer.jsx
Scarriffle 22b6cdffc5 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>
2026-07-22 22:00:44 +02:00

174 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useRef, useState } from "react";
import { api } from "../api";
import { useConfirm } from "../confirm";
import Icon from "../components/Icon";
export default function Transfer() {
const confirm = useConfirm();
const fileRef = useRef(null);
const [file, setFile] = useState(null);
const [mode, setMode] = useState("add");
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [busy, setBusy] = useState(false);
const MODES = {
add: "Nur hinzufügen es wird nichts gelöscht.",
replace_listed:
"Bestand der Produkte ersetzen, die in der Datei vorkommen. Ideal für eine Inventur: exportieren, korrigieren, zurückspielen.",
replace_all:
"Alle Bestände vorher leeren und komplett neu aufbauen. Für die vollständige Wiederherstellung aus einem Backup.",
};
async function download(kind) {
setError(null);
try {
if (kind === "csv") await api.exportCsv();
else await api.exportJson();
} catch (err) {
setError(err.message);
}
}
async function doImport(e) {
e.preventDefault();
if (!file) return;
if (mode !== "add") {
const warning =
mode === "replace_all"
? "ALLE Bestände werden vorher gelöscht und aus der Datei neu aufgebaut. Fortfahren?"
: "Für alle Produkte in der Datei werden die vorhandenen Chargen gelöscht und ersetzt. Fortfahren?";
if (!(await confirm({
title: "Vorhandene Bestände ersetzen?",
message: warning,
confirmLabel: "Ersetzen",
danger: true,
}))) return;
}
setError(null);
setResult(null);
setBusy(true);
try {
setResult(await api.importStock(file, mode));
setFile(null);
if (fileRef.current) fileRef.current.value = "";
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
}
return (
<div>
<div className="page-head">
<div>
<h1>Import / Export</h1>
<div className="sub">Bestände samt Chargen und MHD sichern oder einspielen</div>
</div>
</div>
{error && <div className="alert error"><Icon name="alert" size={16} />{error}</div>}
<div className="grid-2">
<section className="card">
<div className="card-head"><Icon name="checkout" /><h2>Export</h2></div>
<p className="muted small mt-0">
<strong>CSV</strong> enthält eine Zeile je Charge und lässt sich in Excel oder
LibreOffice bearbeiten. <strong>JSON</strong> ist ein vollständiges Backup inklusive
Einheiten, Gruppen und Lagerorten.
</p>
<div className="field-inline">
<button className="btn primary" onClick={() => download("csv")}>
<Icon name="package" size={16} />Bestand als CSV
</button>
<button className="btn" onClick={() => download("json")}>
<Icon name="box" size={16} />Backup als JSON
</button>
</div>
</section>
<section className="card">
<div className="card-head"><Icon name="checkin" /><h2>Import</h2></div>
<p className="muted small mt-0">
CSV oder JSON auswählen. Unbekannte Produkte, Gruppen und Lagerorte werden immer
angelegt was mit vorhandenen Beständen passiert, bestimmt der Modus.
</p>
<form onSubmit={doImport}>
<label>
Datei
<input ref={fileRef} type="file" accept=".csv,.json,text/csv,application/json"
onChange={(e) => setFile(e.target.files?.[0] || null)} />
</label>
<label>
Modus
<select value={mode} onChange={(e) => setMode(e.target.value)}>
<option value="add">Nur hinzufügen</option>
<option value="replace_listed">Bestände der enthaltenen Produkte ersetzen</option>
<option value="replace_all">Alle Bestände ersetzen</option>
</select>
</label>
<p className={`muted small mt-0 ${mode === "replace_all" ? "hint-danger" : ""}`}>
{MODES[mode]}
</p>
<button className="btn primary" disabled={!file || busy}>
{busy ? "Importiere…" : "Importieren"}
</button>
</form>
{result && (
<div className="alert ok" style={{ marginTop: "var(--sp-4)" }}>
<Icon name="check" size={16} />
<span>
Import abgeschlossen ({result.format?.toUpperCase()}):{" "}
{result.products_created} Produkt(e) angelegt, {result.lots_added} Charge(n) ergänzt
{result.products_cleared ? `, ${result.products_cleared} Produkt(e) vorher geleert` : ""}
{result.units_created ? `, ${result.units_created} Einheit(en) angelegt` : ""}.
</span>
</div>
)}
{result?.errors?.length > 0 && (
<div className="alert warn">
<Icon name="alert" size={16} />
<span>
{result.errors.length} Zeile(n) übersprungen:
<ul style={{ margin: "6px 0 0", paddingLeft: 18 }}>
{result.errors.slice(0, 10).map((e, i) => <li key={i}>{e}</li>)}
</ul>
{result.errors.length > 10 && <div> und {result.errors.length - 10} weitere.</div>}
</span>
</div>
)}
</section>
</div>
<section className="card">
<div className="card-head"><Icon name="package" /><h2>CSV-Aufbau</h2></div>
<div className="table-wrap">
<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>
<tr><td className="strong">name</td><td>Produktname (Pflicht, wenn kein Barcode passt)</td></tr>
<tr><td className="strong">marke</td><td>optional</td></tr>
<tr><td className="strong">einheit</td><td>Produkteinheit, z.B. Gramm, Kilogramm, Stück</td></tr>
<tr><td className="strong">packungsgroesse</td><td>Basiseinheiten je Gebinde, z.B. 195</td></tr>
<tr><td className="strong">gebinde</td><td>Bezeichnung, z.B. Glas, Tüte, Flasche</td></tr>
<tr><td className="strong">gruppe</td><td>optional; wird bei Bedarf angelegt</td></tr>
<tr><td className="strong">mindestbestand</td><td>optional, in Basiseinheiten</td></tr>
<tr><td className="strong">menge</td><td>Menge dieser Charge</td></tr>
<tr><td className="strong">menge_einheit</td><td>Einheit der Menge: Gebinde-Bezeichnung oder z.B. Gramm</td></tr>
<tr><td className="strong">mhd</td><td>Datum, z.B. 2026-12-23 oder 23.12.2026 leer erlaubt</td></tr>
<tr><td className="strong">lagerort</td><td>optional; wird bei Bedarf angelegt</td></tr>
</tbody>
</table>
</div>
<p className="muted small">
Am einfachsten: einmal exportieren, die Datei als Vorlage nehmen und ergänzen.
Trennzeichen ist ein Semikolon; Komma wird ebenfalls erkannt.
</p>
</section>
</div>
);
}