// Einstellungen — Datensicherung (Backup & Restore) // Export all rf-* data (from IndexedDB via RFDB) as a single JSON file, and import one back. const { useState: useStateBkp, useEffect: useEffectBkp, useRef: useRefBkp, useMemo: useMemoBkp } = React; // Keys we treat as "your data". Everything written by useLocal in interactive.jsx // is prefixed with rf-, so we just enumerate localStorage at export time. // All values are JSON-encoded (useLocal does JSON.stringify on write) so we // can safely roundtrip them through JSON.parse → JSON.stringify. const RF_PREFIX = 'rf-'; const LAST_EXPORT_KEY = 'rf-last-export'; const CLEAN_SLATE_KEY = 'rf-clean-slate-v1'; // wiping this would re-run first-boot purge after import const BACKUP_VERSION = 1; const BACKUP_APP = 'RentFlow Manager'; // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── function collectBackup() { const data = {}; if (window.RFDB) { // Read from IndexedDB in-memory cache (authoritative source) window.RFDB.keys().forEach(k => { if (k === LAST_EXPORT_KEY) return; if (k === CLEAN_SLATE_KEY) return; const raw = window.RFDB.getRaw(k); if (raw !== null) data[k] = raw; }); } else { // Fallback: iterate localStorage for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (!k || !k.startsWith(RF_PREFIX)) continue; if (k === LAST_EXPORT_KEY) continue; if (k === CLEAN_SLATE_KEY) continue; data[k] = localStorage.getItem(k); } } 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 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'), rentals: len('rf-rentals-v2'), events: len('rf-events-v2'), emails: len('rf-emails-v1'), protocols: len('rf-protocols'), tasks: tasksLen, docs: genDocsCount, transactions: len('rf-transactions'), hasCompany: !!d['rf-company'], 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 }) { 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 && (
)}
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 snapshot = useMemoBkp(() => collectBackup(), [tick]); const stats = useMemoBkp(() => countItems(snapshot), [snapshot]); const size = useMemoBkp(() => bytesize(snapshot), [snapshot]); const lastExport = (() => { try { // Try RFDB first, fall back to localStorage if (window.RFDB) { const v = window.RFDB.get(LAST_EXPORT_KEY); if (v) return v; } const raw = localStorage.getItem(LAST_EXPORT_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } })(); const lastExportFmt = fmtDateTimeDE(lastExport && lastExport.at); // ── Export const doExport = () => { const payload = collectBackup(); downloadJSON(backupFilename(), payload); const exportMeta = { at: payload.exportedAt }; if (window.RFDB) window.RFDB.set(LAST_EXPORT_KEY, exportMeta); else { try { localStorage.setItem(LAST_EXPORT_KEY, JSON.stringify(exportMeta)); } 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 const applyImport = () => { if (!pending) return; const { parsed } = pending; try { if (window.RFDB) { // Import via RFDB (writes to IndexedDB + localStorage in sync) window.RFDB.importAll(parsed.data); // Mark clean-slate done so the first-boot purge doesn't re-wipe seeds. window.RFDB.set(CLEAN_SLATE_KEY, '1'); } else { // Fallback: direct localStorage const toDel = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith(RF_PREFIX) && k !== LAST_EXPORT_KEY) toDel.push(k); } toDel.forEach(k => localStorage.removeItem(k)); Object.entries(parsed.data).forEach(([k, v]) => { if (!k.startsWith(RF_PREFIX)) return; const out = typeof v === 'string' ? v : JSON.stringify(v); localStorage.setItem(k, out); }); try { localStorage.setItem(CLEAN_SLATE_KEY, '1'); } catch {} } setPending(null); toast && toast('Sicherung wird geladen…'); // Reload to re-hydrate all React state from the new data. setTimeout(() => window.location.reload(), 500); } catch (err) { console.error(err); toast && toast('Import fehlgeschlagen'); } }; // ── Hard reset: a TRUE factory wipe. Everything under rf-* is removed so no // user data survives. Functional document templates (protocols / quick-texts) // fall back to their in-script defaults; the company profile falls back to a // BLANK profile (DEFAULT_COMPANY no longer carries demo identity) — so no old // data can "pop back up" after a reset. const doReset = () => { setResetOpen(false); try { if (window.RFDB) { // Remove all rf-* keys via RFDB (clears both IndexedDB and localStorage) window.RFDB.keys().forEach(k => window.RFDB.remove(k)); // Re-set the clean-slate flag so the first-boot purge doesn't run again. window.RFDB.set(CLEAN_SLATE_KEY, '1'); } else { const toDel = []; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k && k.startsWith(RF_PREFIX)) toDel.push(k); } toDel.forEach(k => localStorage.removeItem(k)); try { localStorage.setItem(CLEAN_SLATE_KEY, '1'); } catch {} } toast && toast('Auf Werkszustand zurückgesetzt'); setTimeout(() => window.location.reload(), 400); } catch { toast && toast('Zurücksetzen fehlgeschlagen'); } }; 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={doReset}/>
); } Object.assign(window, { BackupView });