import { useCallback, useEffect, useState } from "react"; import { api } from "../api"; import Icon from "./Icon"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import { asTree } from "../categoryTree"; /** * Gespeicherte Zusammenstellungen für den Etikettenexport. * * Ersetzt die beiden früheren Abschnitte mit festen Spalten. Der Gewinn sind * nicht die Vorlagen an sich, sondern die frei wählbaren Spalten: Erst dadurch * kommen eigene Felder wie eine Kabellänge oder die Kapazität einer Powerbank * überhaupt aufs Etikett. * * Die CSV baut der Server – dort liegen die Feldwerte, und dort lässt sich das * Ergebnis prüfen. */ const ARTEN = [ { wert: "items", label: "Einzelstücke", hinweis: "Ein Etikett je Stück, mit UID und QR." }, { wert: "products", label: "Artikel", hinweis: "Ein Etikett je Artikel statt je Stück." }, { wert: "locations", label: "Lagerorte", hinweis: "Ein Etikett je Lagerort, mit dessen QR." }, ]; const TRENNZEICHEN = [ { wert: ",", label: "Komma" }, { wert: ";", label: "Semikolon" }, { wert: "\t", label: "Tabulator" }, ]; export default function EtikettenVorlagen({ onError }) { const confirm = useConfirm(); const toast = useToast(); const [vorlagen, setVorlagen] = useState(null); const [offen, setOffen] = useState(null); // id der bearbeiteten Vorlage const [busy, setBusy] = useState(false); const laden = useCallback(async () => { try { setVorlagen(await api.etikettenVorlagen()); } catch (err) { onError(err.message); setVorlagen([]); } }, [onError]); useEffect(() => { laden(); }, [laden]); async function anlegen() { const name = await confirm({ title: "Neue Vorlage", message: "Wie soll sie heißen? Zum Beispiel „Kabelexport“.", confirmLabel: "Anlegen", prompt: true, }); if (!name || typeof name !== "string") return; setBusy(true); try { const neu = await api.etikettenVorlageAnlegen({ name: name.trim(), art: "items", spalten: ["qr", "uid", "produkt"], kategorie_ids: [], trennzeichen: ",", }); await laden(); setOffen(neu.id); toast(`Vorlage „${neu.name}“ angelegt.`); } catch (err) { onError(err.message); } finally { setBusy(false); } } async function loeschen(v) { const ok = await confirm({ title: `Vorlage „${v.name}“ löschen?`, message: "Die hinterlegte P-touch-Datei geht mit verloren.", confirmLabel: "Löschen", danger: true, }); if (!ok) return; try { await api.etikettenVorlageLoeschen(v.id); if (offen === v.id) setOffen(null); await laden(); } catch (err) { onError(err.message); } } async function csv(v) { setBusy(true); try { await api.etikettenCsv(v.id, v.name); } catch (err) { onError(err.message); } finally { setBusy(false); } } return (

Etiketten-Vorlagen

Eine Vorlage hält fest, worauf sie sich bezieht, welche Spalten in welcher Reihenfolge exportiert werden und wie gefiltert wird – samt eigener Felder. Dazu lässt sich die P-touch-Datei (.lbx) hinterlegen.

{vorlagen == null &&
Lädt…
} {vorlagen && vorlagen.length === 0 && (

Noch keine Vorlage.

)}
); } /** Bearbeiten einer einzelnen Vorlage. */ function VorlageEditor({ vorlage, onGespeichert, onError }) { const toast = useToast(); const [art, setArt] = useState(vorlage.art); const [spalten, setSpalten] = useState(vorlage.spalten); const [katIds, setKatIds] = useState(vorlage.kategorie_ids); const [trenn, setTrenn] = useState(vorlage.trennzeichen); const [moeglich, setMoeglich] = useState([]); const [kategorien, setKategorien] = useState([]); const [busy, setBusy] = useState(false); useEffect(() => { api.listCategories().then(setKategorien).catch(() => {}); }, []); // Welche Spalten es gibt, hängt von Bezugsart UND Kategorien ab – die eigenen // Felder kommen schließlich von dort. useEffect(() => { let aktiv = true; api.etikettenSpalten(art, katIds) .then((s) => { if (aktiv) setMoeglich(s); }) .catch((err) => onError(err.message)); return () => { aktiv = false; }; }, [art, katIds, onError]); const titelVon = (schluessel) => moeglich.find((m) => m.schluessel === schluessel)?.titel || schluessel; const offen = moeglich.filter((m) => !spalten.includes(m.schluessel)); function verschieben(index, richtung) { const ziel = index + richtung; if (ziel < 0 || ziel >= spalten.length) return; const kopie = [...spalten]; [kopie[index], kopie[ziel]] = [kopie[ziel], kopie[index]]; setSpalten(kopie); } async function speichern() { setBusy(true); try { await api.etikettenVorlageAendern(vorlage.id, { art, spalten, kategorie_ids: katIds, trennzeichen: trenn, }); await onGespeichert(); toast("Vorlage gespeichert."); } catch (err) { onError(err.message); } finally { setBusy(false); } } async function dateiWaehlen(e) { const datei = e.target.files?.[0]; e.target.value = ""; // damit dieselbe Datei erneut gewählt werden kann if (!datei) return; setBusy(true); try { await api.etikettenDateiHochladen(vorlage.id, datei); await onGespeichert(); toast("P-touch-Vorlage hinterlegt."); } catch (err) { onError(err.message); } finally { setBusy(false); } } // Einzelstuecke gibt es nur bei Gegenstaenden; Artikel kann es in beiden // Welten geben, dort also nicht einschraenken. const baum = asTree(kategorien).filter( (c) => art === "products" || c.tracking === "object", ); return (

{ARTEN.find((a) => a.wert === art)?.hinweis}

{art !== "locations" && ( <> Kategorien (leer = alle)
{baum.map((c) => ( ))}

Eigene Felder hängen am Artikel, nicht am einzelnen Stück – alle Stücke eines Artikels tragen denselben Wert.

)}
Spalten (in dieser Reihenfolge) {spalten.length === 0 && (

Noch keine gewählt – es greifen dann die Standardspalten.

)}
    {spalten.map((s, i) => (
  • {titelVon(s)}
  • ))}
Verfügbar {offen.length === 0 &&

Alle Spalten sind gewählt.

}
    {offen.map((m) => (
  • ))}
{vorlage.datei_name && ( )}

Eine .lbx merkt sich den Pfad zu ihrer Datenquelle. Auf einem fremden Rechner muss die CSV in P-touch einmal neu verknüpft werden – die Datei dabeizuhaben erspart trotzdem das Suchen.

); }