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:
@@ -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 & 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 & Co.)</h2></div>
|
||||
<p className="muted small mt-0">
|
||||
|
||||
Reference in New Issue
Block a user