// Einstellungen — Datensicherung (Backup & Restore) // Liest/schreibt ausschließlich über window.RFDB (reine Server-Anbindung, // kein lokaler Cache — siehe db.js). Export/Import laufen komplett gegen // den Server; ein Import wartet auf dessen Bestätigung, bevor irgendetwas // als "fertig" gilt. const { useState: useStateBkp, useEffect: useEffectBkp, useRef: useRefBkp, useMemo: useMemoBkp } = React; const RF_PREFIX = 'rf-'; const LAST_EXPORT_KEY = 'rf-last-export'; const BACKUP_VERSION = 2; const BACKUP_APP = 'RentFlow Manager'; // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── function collectBackup() { const data = {}; if (window.RFDB) { window.RFDB.keys().forEach(k => { if (k === LAST_EXPORT_KEY) return; const raw = window.RFDB.getRaw(k); if (raw !== null) data[k] = raw; }); } return { app: BACKUP_APP, version: BACKUP_VERSION, exportedAt: new Date().toISOString(), data, }; } function countItems(parsed) { // parsed.data is the { 'rf-rentals-v2': , ... } payload. // Old backups stored already-parsed objects; new backups store the raw // stringified form. Tolerate both. const rawD = (parsed && parsed.data) || {}; const d = {}; Object.entries(rawD).forEach(([k, v]) => { if (typeof v === 'string') { try { d[k] = JSON.parse(v); } catch { d[k] = v; } } else { d[k] = v; } }); const len = (k) => Array.isArray(d[k]) ? d[k].length : 0; const tasksLen = d['rf-tasks'] && Array.isArray(d['rf-tasks'].items) ? d['rf-tasks'].items.length : 0; const workflowsLen = (() => { const w = d['rf-workflows']; return w && typeof w === 'object' ? Object.keys(w).length : 0; })(); const genDocsCount = (() => { const g = d['rf-gendocs']; if (!g || typeof g !== 'object') return 0; return Object.values(g).reduce((s, v) => s + (Array.isArray(v) ? v.length : 0), 0); })(); return { equipment: len('rf-equipment-v2'), accessories: len('rf-accessories'), rentals: len('rf-rentals-v2'), events: len('rf-events-v2'), emails: len('rf-emails-v1'), protocols: len('rf-protocols'), quicktexts: len('rf-quicktexts-v2'), workflows: workflowsLen, reviews: len('rf-reviews'), tasks: tasksLen, docs: genDocsCount, transactions: len('rf-transactions'), hasCompany: !!d['rf-company'], hasLogistik: !!d['rf-logistik'], installedAt: d['rf-installed-at'] || null, keyCount: Object.keys(rawD).length, }; } function bytesize(obj) { try { return new Blob([JSON.stringify(obj)]).size; } catch { return 0; } } function fmtBytes(n) { if (!n) return '—'; if (n < 1024) return n + ' B'; if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace('.', ',') + ' KB'; return (n / (1024 * 1024)).toFixed(2).replace('.', ',') + ' MB'; } function fmtDateTimeDE(iso) { if (!iso) return null; try { const d = new Date(iso); if (isNaN(d.getTime())) return null; const pad = (n) => String(n).padStart(2, '0'); return `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} · ${pad(d.getHours())}:${pad(d.getMinutes())}`; } catch { return null; } } function downloadJSON(filename, payload) { const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } function backupFilename() { const d = new Date(); const pad = (n) => String(n).padStart(2, '0'); return `rentflow-backup-${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}.json`; } // ───────────────────────────────────────────────────────────── // Mini stat row inside the export summary card // ───────────────────────────────────────────────────────────── function StatLine({ label, value, t, last }) { return (
{label} {value}
); } // ───────────────────────────────────────────────────────────── // Confirm-import sheet contents // ───────────────────────────────────────────────────────────── function ImportConfirm({ open, parsed, fileName, t, onCancel, onConfirm, busy }) { if (!open) return null; const stats = parsed ? countItems(parsed) : null; const when = parsed ? fmtDateTimeDE(parsed.exportedAt) : null; return (
Sicherung importieren?
Alle aktuellen Daten werden ersetzt. Diese Aktion kann nicht rückgängig gemacht werden.
{fileName || 'backup.json'}
{when &&
Erstellt am {when}
}
{stats && (
)}
{busy ? 'Wird importiert…' : 'Jetzt überschreiben & importieren'}
Abbrechen
); } // ───────────────────────────────────────────────────────────── // Main view // ───────────────────────────────────────────────────────────── function BackupView({ t, onBack, onClose, toast }) { // Tick to refresh sizes / stats after operations const [tick, setTick] = useStateBkp(0); const refresh = () => setTick(x => x + 1); const fileRef = useRefBkp(null); const [pending, setPending] = useStateBkp(null); // { parsed, fileName } const [resetOpen, setResetOpen] = useStateBkp(false); const [busy, setBusy] = useStateBkp(false); // Import/Reset läuft gerade (wartet auf Server) const snapshot = useMemoBkp(() => collectBackup(), [tick]); const stats = useMemoBkp(() => countItems(snapshot), [snapshot]); const size = useMemoBkp(() => bytesize(snapshot), [snapshot]); const lastExport = window.RFDB ? window.RFDB.get(LAST_EXPORT_KEY) : null; const lastExportFmt = fmtDateTimeDE(lastExport && lastExport.at); // ── Export const doExport = () => { const payload = collectBackup(); downloadJSON(backupFilename(), payload); if (window.RFDB) { window.RFDB.set(LAST_EXPORT_KEY, { at: payload.exportedAt }).catch(() => {}); } refresh(); toast && toast('Sicherung exportiert'); }; // ── Import: file picker const openPicker = () => { fileRef.current && fileRef.current.click(); }; const onFile = (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; // allow re-pick of the same file if (!f) return; const reader = new FileReader(); reader.onload = (ev) => { try { const parsed = JSON.parse(ev.target.result); if (!parsed || typeof parsed !== 'object' || !parsed.data || typeof parsed.data !== 'object') { toast && toast('Datei ist kein gültiges Backup'); return; } // Must contain at least one rf- key const rfKeys = Object.keys(parsed.data).filter(k => k.startsWith(RF_PREFIX)); if (rfKeys.length === 0) { toast && toast('Datei enthält keine RentFlow-Daten'); return; } setPending({ parsed, fileName: f.name }); } catch { toast && toast('Datei konnte nicht gelesen werden'); } }; reader.onerror = () => toast && toast('Datei konnte nicht gelesen werden'); reader.readAsText(f); }; // ── Restore confirmed — läuft komplett server-seitig (reset + bulk) und // wartet auf die Bestätigung, BEVOR neu geladen wird. Kein Reload "auf gut // Glück" mehr, der den Import durch einen noch nicht fertigen Server-Stand // wieder überschreiben könnte. const applyImport = () => { if (!pending || busy || !window.RFDB) return; const { parsed } = pending; setBusy(true); window.RFDB.importAll(parsed.data) .then(() => { setPending(null); toast && toast('Sicherung wiederhergestellt'); // Server hat den Import jetzt bestätigt — sicher, hier neu zu laden. window.location.reload(); }) .catch((err) => { console.error(err); setBusy(false); toast && toast('Import fehlgeschlagen — bitte erneut versuchen'); // Sheet bleibt offen, damit der Nutzer es erneut versuchen kann, // statt stillschweigend einen halbfertigen Stand zu übernehmen. }); }; // ── Hard reset: a TRUE factory wipe. Läuft server-seitig; erst NACH // Bestätigung durch den Server wird neu geladen. const doReset = () => { if (busy || !window.RFDB) return; setResetOpen(false); setBusy(true); window.RFDB.resetAll() .then(() => { toast && toast('Auf Werkszustand zurückgesetzt'); window.location.reload(); }) .catch((err) => { console.error(err); setBusy(false); toast && toast('Zurücksetzen fehlgeschlagen — bitte erneut versuchen'); }); }; return (
{/* Hero: status / last export */}
{lastExportFmt ? 'Letzte Sicherung' : 'Noch keine Sicherung'}
{lastExportFmt ? lastExportFmt : 'Erstelle ein Backup um deine Daten zu sichern.'}
{/* Datenbestand */} Aktueller Datenbestand
{/* Export */} Sicherung erstellen
Backup exportieren
Als .json-Datei herunterladen
Enthält alle Mietverträge, Equipment, Termine, Dokumente, Protokolle und das Firmenprofil. Bewahre die Datei an einem sicheren Ort auf — z.B. iCloud Drive. {/* Import */} Sicherung wiederherstellen
Backup importieren
.json-Datei vom Gerät auswählen
Beim Import werden alle aktuellen Daten in der App ersetzt. Du wirst vorher gefragt. {/* Gefahrenzone */} Gefahrenzone setResetOpen(true)} scale={0.95}>
Werkszustand
} />
{/* Sheets */} setPending(null)} onConfirm={applyImport}/> setResetOpen(false)} onConfirm={busy ? () => {} : doReset}/>
); } Object.assign(window, { BackupView });