Web: Lagerort-Etiketten-CSV-Export (P-touch), wie bei Einzelstücken

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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 10:56:33 +02:00
parent 625b6ee263
commit 636982f6c7

View File

@@ -26,6 +26,36 @@ function buildLabelCsv(rows, origin, delim = ",") {
return "" + lines.join("\r\n"); 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() { export default function Transfer() {
const confirm = useConfirm(); const confirm = useConfirm();
const fileRef = useRef(null); const fileRef = useRef(null);
@@ -41,8 +71,43 @@ export default function Transfer() {
const [labelInfo, setLabelInfo] = useState(null); const [labelInfo, setLabelInfo] = useState(null);
// Gemerktes Trennzeichen (Standard Komma P-touch mag das lieber). // Gemerktes Trennzeichen (Standard Komma P-touch mag das lieber).
const [labelDelim, setLabelDelim] = useState(() => localStorage.getItem("label_delim") || ","); 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) { function changeDelim(v) {
setLabelDelim(v); setLabelDelim(v);
@@ -288,6 +353,25 @@ export default function Transfer() {
{labelInfo && <p className="muted small">{labelInfo}</p>} {labelInfo && <p className="muted small">{labelInfo}</p>}
</section> </section>
<section className="card">
<div className="card-head"><Icon name="location" /><h2>Lagerort-Etiketten-Export (P-touch &amp; Co.)</h2></div>
<p className="muted small mt-0">
Je Lagerort eine Zeile: <strong>QR-Inhalt</strong>, ID, Lagerort und der{" "}
<strong>Pfad</strong> (z.B. „Hedingen → Keller"). In der Etiketten-Software als{" "}
<strong>Datenbank</strong> verknüpfen. Verwendet dasselbe Trennzeichen wie oben.
Ein gescannter Lagerort-QR öffnet den Ort in der App/Weboberfläche.
</p>
<div className="field-inline">
<button className="btn primary" disabled={locBusy || !locations.length} onClick={exportLocationLabels}>
<Icon name="download" size={16} />{locBusy ? "Exportiere…" : "Lagerort-Etiketten-CSV herunterladen"}
</button>
<span className="muted small" style={{ alignSelf: "center" }}>
{locations.length} Lagerort{locations.length === 1 ? "" : "e"}
</span>
</div>
{locInfo && <p className="muted small">{locInfo}</p>}
</section>
<section className="card"> <section className="card">
<div className="card-head"><Icon name="package" /><h2>CSV-Aufbau</h2></div> <div className="card-head"><Icon name="package" /><h2>CSV-Aufbau</h2></div>
<div className="table-wrap"> <div className="table-wrap">