Stammdaten-Export/Import (JSON) + Lagerort-Eltern bleibt beim Anlegen

- Backend: services/master_data.py exportiert/importiert Kategorien (inkl.
  eigener Felder), Lagerorte, Einheiten und Gebinde als JSON – MIT IDs, damit
  gedruckte QR-Codes und Verweise nach dem Wiederherstellen passen. Import-Modus
  skip (Vorhandenes lassen) oder overwrite (per ID aktualisieren); Namens-
  Konflikte werden uebersprungen, nicht abgebrochen. Postgres-Sequenzen werden
  danach angehoben. Endpunkte GET /export/master-data, POST /import/master-data.
  5 neue Tests, Suite 162 gruen.
- Web: neue Karte "Stammdaten sichern & wiederherstellen (JSON)" in Import/Export.
- Web: beim Lagerort-Anlegen bleibt der uebergeordnete Ort in der Auswahl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-27 11:17:14 +02:00
parent 636982f6c7
commit 6e48cd0a7a
6 changed files with 371 additions and 2 deletions

View File

@@ -75,6 +75,11 @@ export default function Transfer() {
const [locations, setLocations] = useState([]);
const [locBusy, setLocBusy] = useState(false);
const [locInfo, setLocInfo] = useState(null);
// Stammdaten-Sicherung (Kategorien/Lagerorte/Einheiten/Gebinde als JSON).
const mdFileRef = useRef(null);
const [mdMode, setMdMode] = useState("skip");
const [mdBusy, setMdBusy] = useState(false);
const [mdInfo, setMdInfo] = useState(null);
useEffect(() => {
api.listCategories().then(setCategories).catch(() => {});
@@ -93,6 +98,33 @@ export default function Transfer() {
URL.revokeObjectURL(url);
}
async function exportMasterData() {
setError(null); setMdInfo(null); setMdBusy(true);
try {
const data = await api.exportMasterData();
downloadCsv(JSON.stringify(data, null, 2), "vorrania-stammdaten.json");
setMdInfo("Stammdaten exportiert.");
} catch (err) { setError(err.message); } finally { setMdBusy(false); }
}
async function importMasterData(e) {
e.preventDefault();
const f = mdFileRef.current?.files?.[0];
if (!f) return;
setError(null); setMdInfo(null); setMdBusy(true);
try {
const data = JSON.parse(await f.text());
const rep = await api.importMasterData(data, mdMode);
const summe = (o) => Object.values(o || {}).reduce((a, b) => a + b, 0);
setMdInfo(`Import fertig: ${summe(rep.created)} angelegt, ${summe(rep.updated)} aktualisiert, ${summe(rep.skipped)} übersprungen.`);
if (mdFileRef.current) mdFileRef.current.value = "";
api.listLocations().then(setLocations).catch(() => {});
api.listCategories().then(setCategories).catch(() => {});
} catch (err) {
setError(err instanceof SyntaxError ? "Datei ist kein gültiges JSON." : err.message);
} finally { setMdBusy(false); }
}
function exportLocationLabels() {
setError(null);
setLocInfo(null);
@@ -294,6 +326,41 @@ export default function Transfer() {
</section>
</div>
<section className="card">
<div className="card-head"><Icon name="box" /><h2>Stammdaten sichern &amp; wiederherstellen (JSON)</h2></div>
<p className="muted small mt-0">
Kategorien (inkl. eigener Felder), Lagerorte, Einheiten und Gebinde als <strong>eine
JSON-Datei</strong>. Die <strong>IDs kommen mit</strong> so passen gedruckte
Lagerort-QR-Codes nach dem Wiederherstellen noch und Verweise bleiben gültig. Gut für
Umzug auf einen neuen Server oder als Sicherung. (Artikel/Bestände: siehe Backup als
JSON" oben.)
</p>
<div className="grid-2">
<div>
<button className="btn primary" disabled={mdBusy} onClick={exportMasterData}>
<Icon name="download" size={16} />Stammdaten exportieren
</button>
</div>
<form onSubmit={importMasterData}>
<label>
Datei (JSON)
<input ref={mdFileRef} type="file" accept="application/json,.json" />
</label>
<label>
Bei bereits vergebener ID
<select value={mdMode} onChange={(e) => setMdMode(e.target.value)}>
<option value="skip">Vorhandenes behalten (nur Neues anlegen)</option>
<option value="overwrite">Überschreiben (per ID aktualisieren)</option>
</select>
</label>
<button className="btn" disabled={mdBusy}>
<Icon name="checkin" size={16} />{mdBusy ? "Arbeite" : "Stammdaten importieren"}
</button>
</form>
</div>
{mdInfo && <p className="muted small">{mdInfo}</p>}
</section>
<section className="card">
<div className="card-head"><Icon name="tag" /><h2>QR-Etiketten-Export (P-touch &amp; Co.)</h2></div>
<p className="muted small mt-0">