// Desktop Dokumente — three tabs:
// • Vorlagen — printable PDF templates (Mietvertrag/Rechnung/Übergabeprotokoll), letterhead preview
// • Schnelltexte — WhatsApp/Kleinanzeigen text templates with copy-to-clipboard
// • Archiv — folder-organized archive of all generated docs, linked to rentals
//
// All generated documents are stored in the `docs` array (app-level state) and
// shown on the corresponding Mietvorgang detail pane.
const DEFAULT_TEMPLATES = [
{ id: 'vertrag', name: 'Mietvertrag', kind: 'vertrag', icon: 'fileText', color: '#007AFF', builtIn: true,
title: 'Mietvertrag',
fields: {
mietgegenstand: 'Vermietet wird {{equipment}}{{zubehoer_satz}} Verwendungszweck: {{zweck}}.',
mietdauer: 'Vom {{start}} ({{startzeit}} Uhr, Abholung) bis {{ende}} ({{endezeit}} Uhr, Rückgabe) — {{tage}}.',
pflichten: 'Der Mieter verpflichtet sich, den Mietgegenstand pfleglich zu behandeln und in ordnungsgemäßem Zustand zurückzugeben. Schäden sind unverzüglich zu melden.',
},
},
{ id: 'rechnung', name: 'Rechnung', kind: 'rechnung', icon: 'invoice', color: '#34C759', builtIn: true,
title: 'Rechnung',
fields: {
zahlungshinweis: 'Zahlbar innerhalb 14 Tagen ohne Abzug auf folgendes Konto:\n{{iban}} · Verwendungszweck: {{rechnungsnr}}',
},
},
{ id: 'protokoll', name: 'Übergabeprotokoll', kind: 'protokoll', icon: 'doc', color: '#AF52DE', builtIn: true,
title: 'Übergabeprotokoll',
fields: {
zustandsoptionen: 'Einwandfrei | Gebrauchsspuren | Mängel (s. Notiz)',
},
},
];
const TOKENS_LIST = [
['mieter', 'Name des Mieters'], ['adresse', 'Anschrift'], ['telefon', 'Telefon'], ['email', 'E-Mail'], ['ausweis', 'Ausweis-Nr.'],
['equipment', 'Equipment-Name'], ['zubehoer', 'Zubehör (kommagetrennt)'], ['zweck', 'Verwendungszweck'],
['start', 'Startdatum'], ['ende', 'Enddatum'], ['startzeit', 'Abholzeit'], ['endezeit', 'Rückgabezeit'], ['tage', 'Mietdauer (Tage)'],
['summe', 'Mietsumme'], ['tagespreis', 'Tagespreis'], ['kaution', 'Kaution'],
['firma', 'Firma'], ['iban', 'IBAN'], ['datum', 'Heutiges Datum'],
['rechnungsnr', 'Rechnungsnummer'], ['vertragsnr', 'Vertragsnummer'],
];
function buildTokens(r, eq, company) {
const days = daysBetween(r.start, r.end);
return {
mieter: r.tenantName, adresse: r.address || '—', telefon: r.phone || '—', email: r.email || '—', ausweis: r.idCard || '—',
equipment: eq ? eq.name : r.equipmentName,
zubehoer: eq && eq.accessories && eq.accessories.length ? eq.accessories.join(', ') : '',
zubehoer_satz: eq && eq.accessories && eq.accessories.length ? ` inkl. Zubehör: ${eq.accessories.join(', ')}.` : '.',
zweck: r.purpose || 'allgemeine Nutzung',
start: fmtDateDE(r.start), ende: fmtDateDE(r.end),
startzeit: r.startTime || '—', endezeit: r.endTime || '—',
tage: days + ' Tag' + (days > 1 ? 'e' : ''),
summe: rentalTotal(r) + ' €', tagespreis: r.dailyRate + ' €', kaution: r.deposit + ' €',
firma: company.name, iban: company.iban, datum: fmtDateDE(todayISO()),
rechnungsnr: 'RE-2026-' + r.id.replace(/\D/g, '').padStart(3, '0'),
vertragsnr: 'MV-2026-' + r.id.replace(/\D/g, '').padStart(3, '0'),
};
}
const sub = (txt, tok) => (txt || '').replace(/\{\{(\w+)\}\}/g, (_, k) => tok[k] !== undefined ? tok[k] : `{{${k}}}`);
// ── Top-level screen with left-side tabs ──
function ScreenDocuments({ go, nav, rentals, equipment, company, docs, setDocs }) {
const t = useTheme();
const [tab, setTab] = useState((nav && nav.docsTab) || 'vorlagen');
const [initialRentalId, setInitialRentalId] = useState(null);
const [initialDocId, setInitialDocId] = useState(null);
const [templates, setTemplates] = useLocal('rf.docTemplates.v1', DEFAULT_TEMPLATES);
useEffect(() => {
if (!nav) return;
if (nav.docsTab) setTab(nav.docsTab);
if (nav.rentalId) setInitialRentalId(nav.rentalId);
if (nav.docId) setInitialDocId(nav.docId);
}, [nav]);
const TABS = [
{ id: 'vorlagen', label: 'Vorlagen', sub: 'Verträge · Rechnungen', icon: icons.fileText, color: '#007AFF' },
{ id: 'schnelltexte', label: 'Schnelltexte', sub: 'WhatsApp · Inserate', icon: icons.mail, color: '#34C759' },
{ id: 'archiv', label: 'Archiv', sub: `${docs.length} Dokumente`, icon: icons.archive, color: '#AF52DE' },
];
return (
<>
x.id === tab).sub}>
[x.id, x.label, x.id === 'archiv' ? docs.length : null])}
/>
{/* Active tab content */}
{tab === 'vorlagen' && }
{tab === 'schnelltexte' && }
{tab === 'archiv' && }
>
);
}
// ── Vorlagen tab — generate printable PDFs ──
function DocsVorlagen({ rentals, equipment, company, templates, setTemplates, docs, setDocs, initialRentalId }) {
const t = useTheme();
const toast = useToast();
const [selRental, setSelRental] = useState(initialRentalId || (rentals[0] ? rentals[0].id : null));
const [selTpl, setSelTpl] = useState(templates[0] ? templates[0].id : null);
const [editing, setEditing] = useState(false);
const [confirmDel, setConfirmDel] = useState(null);
const [zoom, setZoom] = useState(1);
const [rentalMenu, setRentalMenu] = useState(false);
const fileRef = useRef(null);
const scrollRef = useRef(null);
const rentalBtnRef = useRef(null);
useEffect(() => { if (initialRentalId) setSelRental(initialRentalId); }, [initialRentalId]);
const r = rentals.find((x) => x.id === selRental);
const eq = r && equipment.find((e) => e.id === r.equipmentId);
const tpl = templates.find((x) => x.id === selTpl) || templates[0];
// Mousewheel zoom on the preview pane — centered around the cursor
useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const onWheel = (e) => {
// Cmd/Ctrl + wheel = zoom. Plain wheel scrolls normally.
if (!(e.metaKey || e.ctrlKey)) return;
e.preventDefault();
const delta = -e.deltaY * 0.0018;
setZoom((z) => {
const next = Math.max(0.4, Math.min(2.5, z + delta));
return Math.round(next * 100) / 100;
});
};
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
}, []);
// Close rental dropdown when clicking outside
useEffect(() => {
if (!rentalMenu) return;
const close = (e) => {
if (rentalBtnRef.current && !rentalBtnRef.current.contains(e.target)) setRentalMenu(false);
};
document.addEventListener('mousedown', close);
return () => document.removeEventListener('mousedown', close);
}, [rentalMenu]);
const patchTpl = (patch) => setTemplates((prev) => prev.map((x) => x.id === tpl.id ? { ...x, ...patch } : x));
const patchField = (k, v) => patchTpl({ fields: { ...tpl.fields, [k]: v } });
const resetTpl = () => {
const def = DEFAULT_TEMPLATES.find((d) => d.id === tpl.id);
if (def) { setTemplates((prev) => prev.map((x) => x.id === tpl.id ? def : x)); toast('Vorlage zurückgesetzt'); }
};
const deleteTpl = (id) => {
setTemplates((prev) => prev.filter((x) => x.id !== id));
if (selTpl === id) setSelTpl(templates[0] ? templates[0].id : null);
toast('Vorlage gelöscht');
};
const onUpload = (e) => {
const file = e.target.files && e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const isText = /\.(txt|md|html?)$/i.test(file.name);
const body = isText ? String(reader.result) :
`Sehr geehrte/r {{mieter}},\n\nhiermit bestätigen wir die Vermietung von {{equipment}} im Zeitraum {{start}} – {{ende}}.\n\nMietsumme: {{summe}}\nKaution: {{kaution}}\n\nMit freundlichen Grüßen\n{{firma}}`;
const id = 'tpl-' + Date.now();
const name = file.name.replace(/\.[^.]+$/, '');
setTemplates((prev) => [...prev, {
id, name, kind: 'custom', icon: 'fileText', color: '#FF9500', builtIn: false,
title: name, fileName: file.name,
fields: { body },
}]);
setSelTpl(id); setEditing(true);
toast(`„${file.name}" als Vorlage importiert`);
};
if (/\.(txt|md|html?)$/i.test(file.name)) reader.readAsText(file); else reader.readAsArrayBuffer(file);
e.target.value = '';
};
const saveToArchive = () => {
if (!r || !tpl) return;
const tok = buildTokens(r, eq, company);
const number = tpl.kind === 'rechnung' ? tok.rechnungsnr : tpl.kind === 'vertrag' ? tok.vertragsnr : null;
const doc = {
id: 'd-' + Date.now(),
kind: tpl.kind,
format: 'pdf',
title: number ? `${tpl.name} ${number}` : `${tpl.name} · ${r.tenantName}`,
rentalId: r.id,
templateId: tpl.id,
number,
createdISO: todayISO(),
};
setDocs((prev) => [doc, ...prev]);
toast('Im Archiv gespeichert');
};
const standardTpls = templates.filter((x) => x.builtIn);
const customTpls = templates.filter((x) => !x.builtIn);
const renderTplRow = (d) => {
const sel = selTpl === d.id;
return (
setSelTpl(d.id)} scale={0.99} hoverBg={sel ? undefined : t.cardAlt}>
{d.name}
{d.builtIn ? 'Standard-Vorlage' : (d.fileName || 'Eigene Vorlage')}
{!d.builtIn && (
{ e.stopPropagation(); setConfirmDel(d); }} scale={0.9}>
)}
);
};
const zoomPct = Math.round(zoom * 100);
const zoomBtn = (label, onClick, disabled) => (
{label}
);
return (
{/* ── Sidebar: templates ── */}
Standard
{standardTpls.map(renderTplRow)}
{customTpls.length > 0 && (
<>
Eigene
{customTpls.map(renderTplRow)}
>
)}
{/* ── Editor (when bearbeiten is on) ── */}
{editing && tpl && (
setConfirmDel(tpl)}/>
)}
{/* ── Preview pane ── */}
{/* Action bar */}
{r && tpl && (
{/* Mietvorgang dropdown */}
setRentalMenu((v) => !v)} scale={0.98}>
{r.tenantName}
{eq ? eq.name : r.equipmentName}
{rentalMenu && (
{rentals.map((x) => {
const e = equipment.find((q) => q.id === x.equipmentId);
const sel = x.id === selRental;
return (
{ setSelRental(x.id); setRentalMenu(false); }} scale={0.99} hoverBg={sel ? undefined : t.cardAlt}>
{x.tenantName}
{e ? e.name : x.equipmentName}
);
})}
)}
{/* Zoom controls */}
{zoomBtn('−', () => setZoom((z) => Math.max(0.4, Math.round((z - 0.1) * 100) / 100)), zoom <= 0.4)}
setZoom(1)} scale={0.96}>
{zoomPct}%
{zoomBtn('+', () => setZoom((z) => Math.min(2.5, Math.round((z + 0.1) * 100) / 100)), zoom >= 2.5)}
setEditing((v) => !v)}>{editing ? 'Fertig' : 'Bearbeiten'}
{!editing &&
toast('Druckdialog geöffnet')}>Drucken }
{!editing &&
Archiv }
{!editing &&
{ saveToArchive(); toast('PDF exportiert'); }}>Als PDF }
)}
{/* Scrollable paper area */}
{ if (confirmDel.builtIn) resetTpl(); else deleteTpl(confirmDel.id); }}
onClose={() => setConfirmDel(null)}/>
);
}
// ── Template editor (left panel when "Bearbeiten" is on) ──
function TemplateEditor({ t, tpl, onPatchTpl, onPatchField, onReset, onDelete }) {
const fieldDefs = tpl.kind === 'vertrag' ? [
{ key: 'mietgegenstand', label: '§1 Mietgegenstand', rows: 3 },
{ key: 'mietdauer', label: '§2 Mietdauer', rows: 2 },
{ key: 'pflichten', label: '§4 Pflichten des Mieters', rows: 4 },
] : tpl.kind === 'rechnung' ? [
{ key: 'zahlungshinweis', label: 'Zahlungshinweis (Fußzeile)', rows: 3 },
] : tpl.kind === 'protokoll' ? [
{ key: 'zustandsoptionen', label: 'Zustands-Optionen (mit | getrennt)', rows: 2 },
] : [
{ key: 'body', label: 'Dokument-Inhalt', rows: 16 },
];
const ta = {
width: '100%', fontFamily: 'inherit', fontSize: 13, lineHeight: 1.55,
padding: '10px 12px', border: `1px solid ${t.sep}`, borderRadius: 9,
background: t.inputBg, color: t.text, resize: 'vertical', outline: 'none',
boxSizing: 'border-box',
};
return (
Vorlage bearbeiten
onPatchTpl({ name: e.target.value, title: e.target.value })}
style={{ width: '100%', fontSize: 19, fontWeight: 700, color: t.text, background: 'transparent', border: 'none', outline: 'none', padding: '2px 0', letterSpacing: -0.3, boxSizing: 'border-box' }}/>
{tpl.builtIn ? 'Standard-Vorlage' : (tpl.fileName || 'Eigene Vorlage')}
{fieldDefs.map((f) => (
))}
Verfügbare Platzhalter
{TOKENS_LIST.map(([k, lab]) => (
{`{{${k}}}`}
))}
Platzhalter werden beim Erstellen automatisch durch die Daten des gewählten Mietvorgangs ersetzt.
{tpl.builtIn ? (
Zurücksetzen
) : (
Löschen
)}
);
}
// A4-ish paper document
function DocPaper({ tpl, rental, eq, company }) {
const r = rental;
const days = daysBetween(r.start, r.end);
const net = Math.round(rentalTotal(r) / 1.19);
const vat = rentalTotal(r) - net;
const ink = '#1a1a1a', soft = '#666', line = '#e0e0e0';
const page = { width: 720, minHeight: 980, background: '#fff', color: ink, padding: '56px 60px', boxShadow: '0 12px 40px rgba(0,0,0,0.18)', borderRadius: 3, fontSize: 13, lineHeight: 1.6 };
const H = ({ children }) => {children}
;
const Label = ({ children }) => {children}
;
const tok = buildTokens(r, eq, company);
const f = tpl.fields || {};
const Letterhead = () => (
{company.logo &&
}
{company.name}
{company.street} · {company.city}
{company.email}
{company.phone}
USt-IdNr. {company.ust}
);
if (tpl.kind === 'vertrag') return (
{tpl.title || 'Mietvertrag'}
Vertrags-Nr. {tok.vertragsnr} · {tok.datum}
Vermieter {company.name}
{company.owner}{'\n'}{company.street}{'\n'}{company.city}
Mieter {r.tenantName}
{r.address || '—'}{'\n'}{r.phone}{'\n'}Ausweis: {r.idCard || '—'}
§1 Mietgegenstand
{sub(f.mietgegenstand, tok)}
§2 Mietdauer
{sub(f.mietdauer, tok)}
§3 Mietpreis & Kaution
1 ? ` × ${r.quantity}` : ''}`} v={`${rentalTotal(r)} €`} line={line}/>
Gesamt fällig {rentalTotal(r) + r.deposit} €
§4 Pflichten des Mieters
{sub(f.pflichten, tok)}
{['Ort, Datum · Vermieter', 'Ort, Datum · Mieter'].map((l) =>
)}
);
if (tpl.kind === 'rechnung') return (
{tpl.title || 'Rechnung'} {tok.rechnungsnr}
Datum: {tok.datum}
Leistungszeitraum:
{fmtRange(r.start, r.end)}
Rechnung an {r.tenantName}
{r.address || '—'}
{['Pos.', 'Beschreibung', 'Tage', 'Einzel', 'Summe'].map((h, i) => 1 ? 'right' : 'left', padding: '8px 4px', fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: soft }}>{h} )}
1
{eq ? eq.name : r.equipmentName}
{r.purpose || 'Vermietung'}
{days}
{r.dailyRate} €
{rentalTotal(r)} €
{sub(f.zahlungshinweis, tok)}
Status: {r.paymentStatus === 'bezahlt' ? 'Bezahlt' : 'Offen'}
);
if (tpl.kind === 'protokoll') {
const zustOpts = (f.zustandsoptionen || 'Einwandfrei | Gebrauchsspuren | Mängel (s. Notiz)').split('|').map((s) => s.trim()).filter(Boolean);
return (
{tpl.title || 'Übergabeprotokoll'}
{eq ? eq.name : r.equipmentName} · {r.tenantName}
Übergabe (Abholung) {fmtDateDE(r.start)} · {r.startTime} Uhr
Rückgabe {fmtDateDE(r.end)} · {r.endTime} Uhr
Mitgeführtes Zubehör
{(eq && eq.accessories && eq.accessories.length ? eq.accessories : ['Gerät', 'Stromkabel']).map((a, i) => (
))}
Zustand bei Übergabe
Notizen
{['Unterschrift Vermieter', 'Unterschrift Mieter'].map((l) =>
)}
);
}
// custom user-uploaded template
return (
{tpl.title || tpl.name}
{tok.datum} · {r.tenantName}
{sub(f.body, tok)}
{['Ort, Datum · Vermieter', 'Ort, Datum · Mieter'].map((l) =>
)}
);
}
function Line({ k, v, line }) {
return {k} {v}
;
}
// ── Firmenprofil ──
// Tabs (left rail) → Sections (right pane). All edits go through `form` and
// commit to `company` via the "Speichern" button in the toolbar when dirty.
function ScreenProfile({ company, setCompany, dark, toggleDark, data }) {
const t = useTheme();
const toast = useToast();
const merged = { ...DEFAULT_COMPANY, ...(company || {}) };
const [form, setForm] = useState(merged);
useEffect(() => setForm({ ...DEFAULT_COMPANY, ...(company || {}) }), [company]);
const set = (k, v) => setForm((f) => ({ ...f, [k]: v }));
const setNum = (k, v, dflt = 0) => setForm((f) => ({ ...f, [k]: v === '' ? '' : (isNaN(Number(v)) ? dflt : Number(v)) }));
const dirty = JSON.stringify(form) !== JSON.stringify(merged);
const card = { background: t.card, borderRadius: 16, boxShadow: t.shadowCard, padding: 24 };
const logoRef = useRef(null);
// ── Datensicherung ── persistent last-backup metadata + import/reset confirm state ──
const d = data || {};
const [lastBackup, setLastBackup] = useLocal('rf-d-last-backup', null); // { dateISO, size, counts }
const [confirmReset, setConfirmReset] = useState(false);
const [pendingImport, setPendingImport] = useState(null); // { payload, name, size }
const importRef = useRef(null);
const onPickLogo = (e) => {
const file = e.target.files && e.target.files[0]; if (!file) return;
if (!/^image\//.test(file.type)) { toast('Bitte eine Bilddatei wählen'); e.target.value = ''; return; }
const reader = new FileReader();
reader.onload = () => { set('logo', String(reader.result)); toast('Logo aktualisiert – jetzt speichern'); };
reader.readAsDataURL(file);
e.target.value = '';
};
// Tab navigation
const TABS = [
{ id: 'stammdaten', label: 'Stammdaten', icon: icons.building, sub: 'Firma & Kontakt' },
{ id: 'bank', label: 'Bank & Steuer', icon: icons.euro, sub: 'IBAN, USt, MwSt.' },
{ id: 'logistik', label: 'Logistik', icon: icons.truck, sub: 'Lager & km-Sätze' },
{ id: 'dokumente', label: 'Dokumente', icon: icons.fileText, sub: 'Nummern & Fristen' },
{ id: 'erinnerungen', label: 'Erinnerungen', icon: icons.bell, sub: 'Push & Vorlauf' },
{ id: 'datensicherung',label: 'Datensicherung',icon: icons.archive, sub: 'Export & Import' },
{ id: 'darstellung', label: 'Darstellung', icon: icons.moon, sub: 'Hell · Dunkel' }];
const [tab, setTab] = useState('stammdaten');
// Local Toggle (mirrors the rentals screen pattern)
const Toggle = ({ on, onClick }) => (
);
const Row = ({ title, sub, children, last }) => (
);
const NumInput = ({ value, onChange, suffix, width = 110 }) => (
{suffix && {suffix} }
);
// ── Sections ──────────────────────────────────────────────────────────
const StammdatenSection = (
<>
logoRef.current && logoRef.current.click()} scale={0.96}>
{form.logo ? (
) : (
)}
{form.name}
{form.owner}
logoRef.current && logoRef.current.click()}>{form.logo ? 'Logo ändern' : 'Logo hochladen'}
{form.logo && set('logo', '')}>Entfernen }
set('name', e.target.value)}/>
set('owner', e.target.value)}/>
set('street', e.target.value)}/>
set('city', e.target.value)}/>
set('email', e.target.value)}/>
set('phone', e.target.value)}/>
set('website', e.target.value)} placeholder="www.example.de"/>
Register & Steuer-IDs
set('taxNumber', e.target.value)} placeholder="143/256/12345"/>
set('ust', e.target.value)} placeholder="DE123456789"/>
set('handelsregister', e.target.value)} placeholder="HRB 123456"/>
set('amtsgericht', e.target.value)} placeholder="München"/>
>
);
const BankSection = (
<>
Bankverbindung
set('iban', e.target.value)} placeholder="DE12 3456 7890 1234 5678 90"/>
set('bic', e.target.value)} placeholder="BYLADEM1MUC"/>
set('bankName', e.target.value)} placeholder="Bayerische Landesbank"/>
Umsatzsteuer
set('kleinunternehmer', !form.kleinunternehmer)}/>
set('mwstSatz', Number(v))} options={[['19', '19 %'], ['7', '7 %'], ['0', '0 %']]} size="sm"/>
>
);
const LogistikSection = (
<>
Lager & Startadresse
set('lagerAddress', e.target.value)} placeholder="Bahnhofstr. 18, 82110 Germering"/>
Lieferpreise
setNum('kmRate', e.target.value.replace(',', '.'), 0)} suffix="€/km" width={90}/>
setNum('pauschale', e.target.value, 0)} suffix="€" width={90}/>
set('tripFactor', Number(v))} options={[['1', '1×'], ['2', '2×'], ['4', '4×']]} size="sm"/>
💡
Beispiel 10 km: {(10 * (Number(form.tripFactor) || 1) * (Number(form.kmRate) || 0) + (Number(form.pauschale) || 0)).toFixed(2).replace('.', ',')} €
· {String(form.kmRate).replace('.', ',')} €/km × {form.tripFactor} Fahrten + {form.pauschale} € Pauschale
>
);
const DokumenteSection = (
<>
Nummern-Schemata
set('invoiceScheme', e.target.value)} placeholder="RE-{YYYY}-{####}"/>
set('contractScheme', e.target.value)} placeholder="MV-{YYYY}-{####}"/>
Nächste Rechnung: {String(form.invoiceScheme || '').replace('{YYYY}', new Date().getFullYear()).replace('{####}', '0042')}
Standard-Konditionen
setNum('paymentDays', e.target.value, 14)} suffix="Tage" width={70}/>
setNum('defaultDeposit', e.target.value, 0)} suffix="€" width={90}/>
>
);
const ErinnerungenSection = (
<>
Push-Benachrichtigungen
macOS-Mitteilungen, wenn etwas zu tun ist.
{form.remindContract && (
setNum('remindContractDays', e.target.value, 3)} suffix="Tage" width={70}/>
)}
set('remindContract', !form.remindContract)}/>
set('remindReturn', !form.remindReturn)}/>
set('remindMaintenance', !form.remindMaintenance)}/>
{form.remindInvoice && (
setNum('remindInvoiceDays', e.target.value, 7)} suffix="Tage" width={70}/>
)}
set('remindInvoice', !form.remindInvoice)}/>
>
);
const DarstellungSection = (
<>
Erscheinungsbild
{ if ((v === 'dark') !== dark) toggleDark(); }} options={[['light', '☀︎ Hell'], ['dark', '☾ Dunkel']]}/>
Über
RentFlow Manager · Desktop-Edition für macOS. Verwaltung von Vermietungen, Equipment, Terminen und Finanzen — synchron zur mobilen App.
>
);
// ────────────── Datensicherung helpers ──────────────
const fmtBytes = (n) => {
if (!n || n < 1024) return (n || 0) + ' B';
if (n < 1024 * 1024) return (n / 1024).toFixed(1).replace('.', ',') + ' KB';
return (n / (1024 * 1024)).toFixed(2).replace('.', ',') + ' MB';
};
const fmtBackupDate = (iso) => {
if (!iso) return '';
const d2 = new Date(iso);
const today = new Date(); today.setHours(0,0,0,0);
const day0 = new Date(d2); day0.setHours(0,0,0,0);
const diff = Math.round((today - day0) / 86400000);
const time = String(d2.getHours()).padStart(2, '0') + ':' + String(d2.getMinutes()).padStart(2, '0');
if (diff === 0) return 'Heute, ' + time;
if (diff === 1) return 'Gestern, ' + time;
return fmtDateDE(fmtDateISO(d2)) + ', ' + time;
};
const protokolleCount = (d.docs || []).filter((x) => x.kind === 'protokoll').length;
const dokumenteCount = (d.docs || []).filter((x) => x.kind !== 'protokoll').length;
const aufgabenCount = ((d.tasks && d.tasks.items) || []).filter((x) => !x.done).length;
const buildBackupPayload = () => ({
schema: 'rentflow-backup',
version: 1,
app: 'RentFlow Manager Desktop',
createdISO: new Date().toISOString(),
company: form,
equipment: d.equipment || [],
rentals: d.rentals || [],
events: d.events || [],
tasks: d.tasks || { items: [], autoDone: {} },
categories:d.categories|| [],
docs: d.docs || [],
blocked: d.blocked || [],
});
const currentPayloadSize = useMemo(() => {
try { return new Blob([JSON.stringify(buildBackupPayload())]).size; } catch (e) { return 0; }
}, [d.equipment, d.rentals, d.events, d.tasks, d.categories, d.docs, d.blocked, form]);
const doExport = () => {
const payload = buildBackupPayload();
const json = JSON.stringify(payload, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-');
a.href = url; a.download = 'rentflow-backup-' + stamp + '.json';
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1500);
const counts = {
equipment: (d.equipment || []).length, rentals: (d.rentals || []).length,
events: (d.events || []).length, docs: (d.docs || []).length,
tasks: ((d.tasks && d.tasks.items) || []).length, blocked: (d.blocked || []).length,
};
setLastBackup({ dateISO: new Date().toISOString(), size: blob.size, counts });
toast('Backup heruntergeladen');
};
const onPickImport = (e) => {
const file = e.target.files && e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const payload = JSON.parse(String(reader.result));
if (!payload || payload.schema !== 'rentflow-backup') throw new Error('Ungültige Backup-Datei');
setPendingImport({ payload, name: file.name, size: file.size });
} catch (err) {
toast('Datei ist kein gültiges RentFlow-Backup');
}
};
reader.readAsText(file);
e.target.value = '';
};
const applyImport = () => {
if (!pendingImport) return;
const p = pendingImport.payload;
if (p.equipment && d.setEquipment) d.setEquipment(p.equipment);
if (p.rentals && d.setRentals) d.setRentals(p.rentals);
if (p.events && d.setEvents) d.setEvents(p.events);
if (p.tasks && d.setTasks) d.setTasks(p.tasks);
if (p.categories && d.setCategories) d.setCategories(p.categories);
if (p.docs && d.setDocs) d.setDocs(p.docs);
if (p.blocked && d.setBlocked) d.setBlocked(p.blocked);
if (p.company) setCompany({ ...DEFAULT_COMPANY, ...p.company });
setPendingImport(null);
toast('Daten wiederhergestellt');
};
const doReset = () => {
if (d.setEquipment) d.setEquipment(SEED_EQUIPMENT);
if (d.setRentals) d.setRentals(SEED_RENTALS);
if (d.setEvents) d.setEvents(SEED_EVENTS);
if (d.setTasks) d.setTasks({ items: [], autoDone: {} });
if (d.setCategories) d.setCategories(['Tontechnik', 'Anhänger', 'Licht', 'Sonstiges']);
if (d.setDocs) d.setDocs(typeof SEED_DOCS !== 'undefined' ? SEED_DOCS : []);
if (d.setBlocked) d.setBlocked([]);
setCompany(DEFAULT_COMPANY);
setLastBackup(null);
setConfirmReset(false);
toast('Alle Daten zurückgesetzt');
};
const DataRow = ({ label, value, last }) => (
);
const hasBackup = !!lastBackup;
const DatensicherungSection = (
<>
{/* Status card */}
{hasBackup ? 'Letzte Sicherung' : 'Noch keine Sicherung'}
{hasBackup ? (fmtBackupDate(lastBackup.dateISO) + ' · ' + fmtBytes(lastBackup.size)) : 'Erstelle ein Backup um deine Daten zu sichern.'}
{hasBackup && (
setLastBackup(null)}>Verwerfen
)}
{/* Current data */}
{/* Sicherung erstellen */}
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.
{/* Sicherung wiederherstellen */}
Sicherung wiederherstellen
importRef.current && importRef.current.click()} scale={0.99} style={{ marginTop: 6 }} hoverBg={t.chip}>
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
Alle Daten zurücksetzen
Löscht lokal gespeicherte Daten und stellt die Beispieldaten wieder her.
setConfirmReset(true)} scale={0.97}>
Zurücksetzen
>
);
const SECTION_MAP = {
stammdaten: StammdatenSection,
bank: BankSection,
logistik: LogistikSection,
dokumente: DokumenteSection,
erinnerungen: ErinnerungenSection,
datensicherung: DatensicherungSection,
darstellung: DarstellungSection,
};
return (
<>
x.id === tab)?.label || 'Firmenprofil'}>
{dirty && setForm(merged)}>Verwerfen }
{dirty && { setCompany(form); toast('Gespeichert'); }}>Speichern }
{/* Left rail */}
{TABS.map((tb) => {
const sel = tab === tb.id;
return (
setTab(tb.id)} scale={0.99} hoverBg={sel ? undefined : t.chip} style={{ borderRadius: 10, marginBottom: 2 }}>
);
})}
{/* Right pane */}
{SECTION_MAP[tab]}
>
);
}
Object.assign(window, { ScreenDocuments, ScreenProfile, DEFAULT_TEMPLATES, DocPaper, buildTokens });