From 636982f6c73054183e6674b250e5aff459c078c9 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 27 Jul 2026 10:56:33 +0200 Subject: [PATCH] =?UTF-8?q?Web:=20Lagerort-Etiketten-CSV-Export=20(P-touch?= =?UTF-8?q?),=20wie=20bei=20Einzelst=C3=BCcken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Abschnitt in Import/Export: exportiert je Lagerort eine Zeile (QR-Inhalt = Link auf den Ort, ID, Name, Pfad "Hedingen → Keller") als CSV mit demselben wählbaren Trennzeichen wie der Einzelstück-Export und derselben Formel-Injektion-Härtung. Client-seitig aus den Lagerorten gebaut. Co-Authored-By: Claude Opus 4.8 --- web/src/pages/Transfer.jsx | 86 +++++++++++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Transfer.jsx b/web/src/pages/Transfer.jsx index 863a7ff..5b33e4d 100644 --- a/web/src/pages/Transfer.jsx +++ b/web/src/pages/Transfer.jsx @@ -26,6 +26,36 @@ function buildLabelCsv(rows, origin, delim = ",") { return "" + lines.join("\r\n"); } +// Etiketten-CSV für Lagerorte (QR-Inhalt = Link auf den Ort). Gleiches Prinzip +// wie bei den Einzelstücken; „Pfad" nennt die Ober-Lagerorte (Hedingen → Keller). +function buildLocationCsv(locations, origin, delim = ",") { + const byId = Object.fromEntries(locations.map((l) => [l.id, l])); + const pfad = (l) => { + const teile = [l.name]; + const gesehen = new Set([l.id]); + let cur = l.parent_id; + while (cur != null && byId[cur] && !gesehen.has(cur)) { + gesehen.add(cur); + teile.unshift(byId[cur].name); + cur = byId[cur].parent_id; + } + return teile.join(" → "); + }; + const head = ["QR-Inhalt", "ID", "Lagerort", "Pfad"]; + const delimRe = delim === "\t" ? "\\t" : delim; + const needsQuote = new RegExp(`[${delimRe}"\\n\\r]`); + const esc = (v) => { + let s = String(v ?? ""); + if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`; + return needsQuote.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + const lines = [head.map(esc).join(delim)]; + for (const l of locations) { + lines.push([`${origin}/l/${l.id}`, l.id, l.name, pfad(l)].map(esc).join(delim)); + } + return "" + lines.join("\r\n"); +} + export default function Transfer() { const confirm = useConfirm(); const fileRef = useRef(null); @@ -41,8 +71,43 @@ export default function Transfer() { const [labelInfo, setLabelInfo] = useState(null); // Gemerktes Trennzeichen (Standard Komma – P-touch mag das lieber). const [labelDelim, setLabelDelim] = useState(() => localStorage.getItem("label_delim") || ","); + // Lagerort-Etiketten. + const [locations, setLocations] = useState([]); + const [locBusy, setLocBusy] = useState(false); + const [locInfo, setLocInfo] = useState(null); - useEffect(() => { api.listCategories().then(setCategories).catch(() => {}); }, []); + useEffect(() => { + api.listCategories().then(setCategories).catch(() => {}); + api.listLocations().then(setLocations).catch(() => {}); + }, []); + + function downloadCsv(csv, filename) { + 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 = filename; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } + + function exportLocationLabels() { + setError(null); + setLocInfo(null); + setLocBusy(true); + try { + if (!locations.length) { setLocInfo("Keine Lagerorte vorhanden."); return; } + downloadCsv(buildLocationCsv(locations, window.location.origin, labelDelim), + "vorrania-lagerort-etiketten.csv"); + setLocInfo(`${locations.length} Lagerort-Etikett(en) exportiert.`); + } catch (err) { + setError(err.message); + } finally { + setLocBusy(false); + } + } function changeDelim(v) { setLabelDelim(v); @@ -288,6 +353,25 @@ export default function Transfer() { {labelInfo &&

{labelInfo}

} +
+

Lagerort-Etiketten-Export (P-touch & Co.)

+

+ Je Lagerort eine Zeile: QR-Inhalt, ID, Lagerort und der{" "} + Pfad (z.B. „Hedingen → Keller"). In der Etiketten-Software als{" "} + Datenbank verknüpfen. Verwendet dasselbe Trennzeichen wie oben. + Ein gescannter Lagerort-QR öffnet den Ort in der App/Weboberfläche. +

+
+ + + {locations.length} Lagerort{locations.length === 1 ? "" : "e"} + +
+ {locInfo &&

{locInfo}

} +
+

CSV-Aufbau