import { useEffect, useRef, useState } from "react"; import { api } from "../api"; import { useConfirm } from "../confirm"; import Icon from "../components/Icon"; import { asTree } from "../categoryTree"; // Baut die Etiketten-CSV mit wählbarem Trennzeichen (BOM für Excel-Umlaute). // QR-Inhalt = Link aufs Stück. function buildLabelCsv(rows, origin, delim = ",") { const head = ["QR-Inhalt", "UID", "Produkt", "Marke", "Kategorie", "Lagerort"]; const delimRe = delim === "\t" ? "\\t" : delim; const needsQuote = new RegExp(`[${delimRe}"\\n\\r]`); const esc = (v) => { let s = String(v ?? ""); // Formel-Injektion verhindern: Beginnt eine Zelle mit = + - @ (oder Tab/CR), // koennte Excel/P-touch sie als Formel ausfuehren. Ein vorangestelltes // Apostroph macht sie zu reinem Text. if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`; return needsQuote.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; const lines = [head.map(esc).join(delim)]; for (const r of rows) { lines.push([`${origin}/i/${r.uid}`, r.uid, r.product, r.brand, r.category, r.location] .map(esc).join(delim)); } return "" + lines.join("\r\n"); } 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); // Etiketten-Export: Kategorieauswahl (leer = alle). const [categories, setCategories] = useState([]); const [selCats, setSelCats] = useState(() => new Set()); const [labelBusy, setLabelBusy] = useState(false); const [labelInfo, setLabelInfo] = useState(null); // Gemerktes Trennzeichen (Standard Komma – P-touch mag das lieber). const [labelDelim, setLabelDelim] = useState(() => localStorage.getItem("label_delim") || ","); useEffect(() => { api.listCategories().then(setCategories).catch(() => {}); }, []); function changeDelim(v) { setLabelDelim(v); localStorage.setItem("label_delim", v); } function toggleCat(id, on) { setSelCats((prev) => { const neu = new Set(prev); if (on) neu.add(id); else neu.delete(id); return neu; }); } async function exportLabels() { setError(null); setLabelInfo(null); setLabelBusy(true); try { const rows = await api.labelRows([...selCats]); if (!rows.length) { setLabelInfo("Keine Einzelstücke in der Auswahl."); return; } const csv = buildLabelCsv(rows, window.location.origin, labelDelim); const blob = new Blob([csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "vorrania-etiketten.csv"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); setLabelInfo(`${rows.length} Einzelstück(e) exportiert.`); } catch (err) { setError(err.message); } finally { setLabelBusy(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 (
CSV enthält eine Zeile je Charge und lässt sich in Excel oder LibreOffice bearbeiten. JSON ist ein vollständiges Backup inklusive Einheiten, Gruppen und Lagerorten.
CSV oder JSON auswählen. Unbekannte Produkte, Gruppen und Lagerorte werden immer angelegt – was mit vorhandenen Beständen passiert, bestimmt der Modus.
{result && (Je Einzelstück eine Zeile: QR-Inhalt, UID, Produkt, Marke, Kategorie, Lagerort. In der Etiketten-Software (z.B. P-touch) als Datenbank{" "} verknüpfen – sie erzeugt QR-Code und Text automatisch. Kategorien wählen (Unterkategorien sind enthalten); ohne Auswahl kommen alle Einzelstücke.
{labelInfo}
}| Spalte | Bedeutung |
|---|---|
| barcode | optional; dient zum Wiedererkennen vorhandener Produkte |
| name | Produktname (Pflicht, wenn kein Barcode passt) |
| marke | optional |
| einheit | Produkteinheit, z.B. Gramm, Kilogramm, Stück |
| packungsgroesse | Basiseinheiten je Gebinde, z.B. 195 |
| gebinde | Bezeichnung, z.B. Glas, Tüte, Flasche |
| gruppe | optional; wird bei Bedarf angelegt |
| mindestbestand | optional, in Basiseinheiten |
| menge | Menge dieser Charge |
| menge_einheit | Einheit der Menge: Gebinde-Bezeichnung oder z.B. Gramm |
| mhd | Datum, z.B. 2026-12-23 oder 23.12.2026 – leer erlaubt |
| lagerort | optional; wird bei Bedarf angelegt |
Am einfachsten: einmal exportieren, die Datei als Vorlage nehmen und ergänzen. Trennzeichen ist ein Semikolon; Komma wird ebenfalls erkannt.