// 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)}
` : ''}
`;
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 = `
| Beschreibung | Menge | Einzel | Summe |
${rows}
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).
` : ''}
`;
} else {
middle = `${esc(doc.bodyText != null ? doc.bodyText : doc.text || '')}
`;
if (doc.signature) {
middle += `
`;
}
}
const title = doc.kind === 'invoice' ? 'Rechnung' : (doc.title || 'Dokument');
const footerContact = `
`;
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 (
);
}
// 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,
});