// Einstellungen sheet — full multi-section settings UI (mobile adaptation). // Also exports the PDF rendering utilities used elsewhere (buildDocHTML, printDocHTML, TemplatePreview). const { useState, useEffect, useRef } = React; // ───────────────────────────────────────────────────────────── // Defaults // ───────────────────────────────────────────────────────────── const DEFAULT_COMPANY = { // Stammdaten name: '', tagline: '', owner: '', street: '', cityPostal: '', address: '', // derived, used by buildDocHTML phone: '', email: '', web: '', taxId: '', vatId: '', hrb: '', amtsgericht: '', // Bank & Steuer iban: '', bic: '', bank: '', kleinunternehmer: true, defaultVat: 19, // Logo + PDF logo: '', template: 'modern', accent: '#1f6feb', // Dokumente invoicePattern: 'RE-{YYYY}-{####}', contractPattern: 'MV-{YYYY}-{####}', invoiceCounter: 1, paymentDays: 14, defaultDeposit: 200, footer: 'Vielen Dank für Ihren Auftrag. Zahlbar ohne Abzug innerhalb der Zahlungsfrist.', // Erinnerungen remContractEnd: { on: true, days: 3 }, remReturnOverdue: { on: true }, remMaintDue: { on: true }, remInvoiceOverdue: { on: true, days: 7 }, remRentalStart: { on: true, days: 1 }, remPickupTime: { on: true, hours: 2 }, }; const PDF_TEMPLATES = [ { id: 'modern', label: 'Modern', desc: 'Farbakzent, klare Linien' }, { id: 'klassisch', label: 'Klassisch', desc: 'Serifen, zentriert' }, { id: 'minimal', label: 'Minimal', desc: 'Schlicht, schwarz-weiß' }, ]; const ACCENT_OPTIONS = ['#1f6feb', '#0a7d4d', '#b23b2e', '#6b4bb3', '#1a1a1a', '#c2761a']; const esc = (s) => String(s == null ? '' : s) .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); const euro = (n) => Number(n || 0).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'; // ───────────────────────────────────────────────────────────── // PDF document HTML (used for on-screen preview + print) // ───────────────────────────────────────────────────────────── function buildDocHTML(company, doc) { const c = { ...DEFAULT_COMPANY, ...(company || {}) }; // Compose address from split fields if available const addrLines = [c.street, c.cityPostal].filter(Boolean); const addr = addrLines.length ? addrLines.join('\n') : (c.address || ''); const tpl = c.template || 'modern'; const logo = c.logo ? `` : `
${esc(c.name)}
${c.tagline ? `
${esc(c.tagline)}
` : ''}`; const contactLine = [c.phone, c.email, c.web].filter(Boolean).map(esc).join(' · '); const legalLine = [ c.taxId ? 'St.-Nr.: ' + esc(c.taxId) : '', c.vatId ? 'USt-IdNr.: ' + esc(c.vatId) : '', c.hrb ? esc(c.hrb) : '', ].filter(Boolean).join(' · '); const bankLine = [ c.bank ? esc(c.bank) : '', c.iban ? 'IBAN: ' + esc(c.iban) : '', c.bic ? 'BIC: ' + esc(c.bic) : '', ].filter(Boolean).join(' · '); const head = `
${logo}${c.logo && c.tagline ? `
${esc(c.tagline)}
` : ''}
${doc.number ? `
${esc(doc.number)}
` : ''}
${esc(doc.date || '')}
`; const recipient = (doc.tenantName || doc.address) ? `
Empfänger
${esc(doc.tenantName || '')}
${esc(doc.address || '')}
` : ''; let middle = ''; if (doc.kind === 'invoice' && doc.invoice) { const iv = doc.invoice; const rows = (iv.items || []).map(it => ` ${esc(it.desc)}${it.sub ? `
${esc(it.sub)}
` : ''} ${esc(it.qty)} ${euro(it.unit)} ${euro(it.sum)} `).join(''); const showTax = !c.kleinunternehmer; middle = ` ${rows}
BeschreibungMengeEinzelSumme
Zwischensumme${euro(iv.subtotal)}
${iv.discount > 0 ? `
Rabatt− ${euro(iv.discount)}
` : ''} ${showTax ? `
Nettobetrag${euro(iv.net)}
zzgl. ${c.defaultVat || 19} % MwSt.${euro(iv.tax)}
` : ''}
Gesamtbetrag${euro(iv.total)}
${!showTax ? `
Kein Ausweis der USt. gemäß § 19 UStG (Kleinunternehmerregelung).
` : ''}
Zahlungsziel: ${c.paymentDays || 14} Tage netto.${c.footer ? ' ' + esc(c.footer) : ''}
${bankLine ? `
${bankLine}
` : ''}
`; } else { middle = `
${esc(doc.bodyText != null ? doc.bodyText : doc.text || '')}
`; if (doc.signature) { middle += `
Ort, Datum
Unterschrift Mieter
Unterschrift Vermieter
`; } } const title = doc.kind === 'invoice' ? 'Rechnung' : (doc.title || 'Dokument'); const footerContact = `
${esc(c.name)}${addr ? '
' + esc(addr).replaceAll('\n', '
') : ''}
${contactLine ? contactLine + '
' : ''}${legalLine}
`; return `
${tpl === 'modern' ? '
' : ''} ${head}

${esc(title)}

${recipient} ${middle} ${doc.kind === 'invoice' ? footerContact : ''}
`; } function printDocHTML(html) { const root = document.getElementById('print-root'); if (!root) return; root.innerHTML = `
${html}
`; const done = () => { root.innerHTML = ''; window.removeEventListener('afterprint', done); }; window.addEventListener('afterprint', done); setTimeout(() => window.print(), 60); } function fileToScaledDataURL(file, maxDim, cb) { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { let { width, height } = img; const scale = Math.min(1, maxDim / Math.max(width, height)); width = Math.round(width * scale); height = Math.round(height * scale); const cv = document.createElement('canvas'); cv.width = width; cv.height = height; const ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); cb(cv.toDataURL('image/png')); }; img.onerror = () => cb(''); img.src = e.target.result; }; reader.onerror = () => cb(''); reader.readAsDataURL(file); } function TemplatePreview({ company, scale = 0.42 }) { const sampleDoc = { kind: 'invoice', number: 'RE-2026-118', date: '01.06.2026', tenantName: 'Lena Wagner', address: 'Seestr. 9\n82211 Herrsching', invoice: { items: [{ desc: 'JBL EON 712 (Aktivlautsprecher)', sub: '13.06.–14.06.2026', qty: '2 Tage', unit: 45, sum: 90 }], subtotal: 90, discount: 10, net: 67.23, tax: 12.77, total: 80, }, }; const html = buildDocHTML(company, sampleDoc); return (
); } // ───────────────────────────────────────────────────────────── // Shared UI primitives for the settings sheet // ───────────────────────────────────────────────────────────── function SubHeader({ title, onBack, onClose, t }) { return (
{onBack ? (
Einstellungen
) :
}
{title}
Fertig
); } function GroupLabel({ children, t }) { return (
{children}
); } function GroupCard({ children, t }) { return (
{children}
); } function HintLine({ children, t }) { return (
{children}
); } // Plain text/number input row (label above, input below) — inside a GroupCard. function InputRow({ label, value, onChange, placeholder, type = 'text', suffix, t, hint, last }) { return (
{label &&
{label}
}
onChange(e.target.value)} placeholder={placeholder} type={type} style={{ flex: 1, padding: '10px 12px', borderRadius: 10, border: `0.5px solid ${t.inputBorder}`, background: t.inputBg, fontSize: 14, color: t.text, outline: 'none', fontFamily: 'inherit', boxSizing: 'border-box', minWidth: 0, }} /> {suffix && {suffix}}
{hint &&
{hint}
}
); } // iOS-style switch function Toggle({ on, onChange, t }) { return ( onChange(!on)} scale={0.94}>
); } // Row with title + optional subtitle + right-side accessory (toggle / segmented / value) function ListRow({ title, sub, right, t, last, padding = '12px 14px' }) { return (
{title}
{sub &&
{sub}
}
{right}
); } // Compact segmented control function Segmented({ options, value, onChange, t }) { return (
{options.map(o => { const sel = o.value === value; return ( onChange(o.value)} scale={0.96}>
{o.label}
); })}
); } // Small inline numeric input for "X Tage" right-side accessory function NumberPill({ value, onChange, t, width = 48 }) { return ( { const n = e.target.value.replace(/[^\d]/g, ''); onChange(n === '' ? '' : Number(n)); }} inputMode="numeric" style={{ width, padding: '6px 8px', borderRadius: 8, border: `0.5px solid ${t.inputBorder}`, background: t.inputBg, fontSize: 14, color: t.text, outline: 'none', fontFamily: 'inherit', textAlign: 'center', boxSizing: 'border-box', fontVariantNumeric: 'tabular-nums', }}/> ); } // Build "next" number from a pattern: replaces {YYYY}, {YY}, {####}, {###}, {##} placeholders. function previewNumber(pattern, counter = 1) { const y = new Date().getFullYear(); return String(pattern || '') .replaceAll('{YYYY}', String(y)) .replaceAll('{YY}', String(y).slice(-2)) .replace(/\{(#+)\}/g, (_, hashes) => String(counter).padStart(hashes.length, '0')); } Object.assign(window, { DEFAULT_COMPANY, PDF_TEMPLATES, ACCENT_OPTIONS, buildDocHTML, printDocHTML, fileToScaledDataURL, TemplatePreview, previewNumber, euroFmt: euro, });