// Verträge — contract templates: Mietvertrag, Kündigung, Mieterhöhung, etc. // Templates auto-fill from property + unit + tenant Stammdaten. const CONTRACT_TEMPLATES = [ { id: 'mietvertrag-wohn', title: 'Mietvertrag (Wohnraum)', category: 'Vertrag', description: 'Standard-Mietvertrag für Wohnraum nach §§ 535 ff. BGB. Unbefristet, Staffelmiete optional.', pages: 6, fields: ['Mieter', 'Objekt', 'Kaltmiete', 'Nebenkosten', 'Kaution', 'Mietbeginn'], icon: 'doc', legal: '§§ 535–580a BGB', }, { id: 'mietvertrag-gewerbe', title: 'Mietvertrag (Gewerbe)', category: 'Vertrag', description: 'Gewerblicher Mietvertrag mit Indexmiete-Klausel und Konkurrenzschutz.', pages: 8, fields: ['Mieter', 'Objekt', 'Nutzungszweck', 'Vertragsdauer', 'Kaltmiete', 'Indexbindung'], icon: 'doc', legal: '§§ 535 ff. BGB · HGB', }, { id: 'staffelmietvertrag', title: 'Staffelmietvertrag', category: 'Vertrag', description: 'Mietvertrag mit vereinbarten jährlichen Mieterhöhungen (§ 557a BGB).', pages: 7, fields: ['Mieter', 'Objekt', 'Startmiete', 'Staffelung', 'Kaution'], icon: 'doc', legal: '§ 557a BGB', }, { id: 'kuendigung-vermieter', title: 'Kündigung durch Vermieter', category: 'Kündigung', description: 'Ordentliche Kündigung mit Begründung (Eigenbedarf, Pflichtverletzung, wirtschaftl. Verwertung).', pages: 2, fields: ['Mieter', 'Objekt', 'Kündigungsgrund', 'Kündigungstermin'], icon: 'cancel', legal: '§§ 573, 568 BGB', }, { id: 'kuendigung-fristlos', title: 'Fristlose Kündigung', category: 'Kündigung', description: 'Außerordentliche Kündigung wegen Mietrückstand oder erheblicher Pflichtverletzung.', pages: 2, fields: ['Mieter', 'Objekt', 'Kündigungsgrund', 'Mietrückstand'], icon: 'cancel', legal: '§ 543 BGB', }, { id: 'kuendigungsbestaetigung', title: 'Kündigungsbestätigung', category: 'Kündigung', description: 'Bestätigung einer Kündigung durch den Mieter mit Übergabetermin.', pages: 1, fields: ['Mieter', 'Objekt', 'Kündigungseingang', 'Auszugstermin'], icon: 'doc', legal: '§ 568 BGB', }, { id: 'mieterhoehung', title: 'Mieterhöhung (Mietspiegel)', category: 'Anpassung', description: 'Mieterhöhungsverlangen bis zur ortsüblichen Vergleichsmiete.', pages: 2, fields: ['Mieter', 'Objekt', 'Aktuelle Miete', 'Neue Miete', 'Mietspiegel-Bezug'], icon: 'arrow-up', legal: '§§ 558 ff. BGB', }, { id: 'modernisierung', title: 'Modernisierungsankündigung', category: 'Anpassung', description: 'Ankündigung baulicher Maßnahmen mit Fristen und voraussichtlicher Umlage.', pages: 3, fields: ['Mieter', 'Objekt', 'Maßnahme', 'Beginn', 'Dauer', 'Umlage'], icon: 'wrench', legal: '§§ 555c, 559 BGB', }, { id: 'uebergabeprotokoll', title: 'Übergabeprotokoll', category: 'Protokoll', description: 'Wohnungsübergabe bei Ein- oder Auszug mit Zählerständen und Mängelliste.', pages: 3, fields: ['Mieter', 'Objekt', 'Datum', 'Zählerstände', 'Schlüssel'], icon: 'clipboard', legal: 'Beweissicherung', }, { id: 'nebenkostenabrechnung', title: 'Nebenkostenabrechnung', category: 'Abrechnung', description: 'Jahresabrechnung der Betriebskosten nach BetrKV mit Verteilerschlüssel.', pages: 4, fields: ['Mieter', 'Objekt', 'Abrechnungszeitraum', 'Vorauszahlungen', 'Verteilerschlüssel'], icon: 'receipt', legal: '§ 556 BGB · BetrKV', }, { id: 'mahnung', title: 'Mahnung Mietrückstand', category: 'Mahnung', description: 'Erste / zweite Mahnung mit Zahlungsfrist vor Kündigungsandrohung.', pages: 1, fields: ['Mieter', 'Objekt', 'Offener Betrag', 'Mahnstufe', 'Frist'], icon: 'alert', legal: '§ 286 BGB', }, { id: 'mietquittung', title: 'Mietquittung', category: 'Bescheinigung', description: 'Quittung über erhaltene Mietzahlung auf Verlangen des Mieters.', pages: 1, fields: ['Mieter', 'Objekt', 'Zeitraum', 'Betrag'], icon: 'receipt', legal: '§ 368 BGB', }, ]; const CATEGORY_COLORS = { 'Vertrag': 'var(--accent)', 'Kündigung': 'var(--neg)', 'Anpassung': 'var(--warn)', 'Protokoll': 'var(--ink)', 'Abrechnung': 'var(--ink-3)', 'Mahnung': 'var(--neg)', 'Bescheinigung': 'var(--ink-3)', }; // Sample generated documents (already-created contracts) const GENERATED_DOCS = [ { id: 'g1', templateId: 'mietvertrag-wohn', date: '2024-02-01', recipient: 'Mia Bauer', propertyId: 'p4', unitId: 'u11', status: 'unterzeichnet' }, { id: 'g2', templateId: 'kuendigungsbestaetigung', date: '2026-04-12', recipient: 'Jens Maier', propertyId: 'p2', unitId: 'u8', status: 'versendet' }, { id: 'g3', templateId: 'mieterhoehung', date: '2026-03-08', recipient: 'Anna Hofmann', propertyId: 'p1', unitId: 'u1', status: 'entwurf' }, { id: 'g4', templateId: 'nebenkostenabrechnung', date: '2026-02-28', recipient: 'Familie Schulz', propertyId: 'p1', unitId: 'u3', status: 'versendet' }, { id: 'g5', templateId: 'modernisierung', date: '2026-01-15', recipient: 'Tobias Lang', propertyId: 'p2', unitId: 'u6', status: 'unterzeichnet' }, { id: 'g6', templateId: 'mahnung', date: '2026-04-25', recipient: 'Lukas Wolf', propertyId: 'p4', unitId: 'u12', status: 'entwurf' }, ]; // ───── Main Page ───── function ContractsPage({ data, route, setRoute }) { if (route.contractWizard) { return ; } const [activeTab, setActiveTab] = React.useState('templates'); const [filter, setFilter] = React.useState('Alle'); const cats = ['Alle', ...new Set(CONTRACT_TEMPLATES.map(t => t.category))]; const visible = filter === 'Alle' ? CONTRACT_TEMPLATES : CONTRACT_TEMPLATES.filter(t => t.category === filter); return (

Verträge & Vorlagen

Mietverträge, Kündigungen und Schreiben aus Stammdaten erstellen
{activeTab === 'templates' && ( <>
{cats.map(c => ( ))}
{visible.map(t => ( setRoute({ page: 'contracts', contractWizard: t.id })} /> ))}
)} {activeTab === 'history' && (
{GENERATED_DOCS.map(d => { const tmpl = CONTRACT_TEMPLATES.find(t => t.id === d.templateId); const prop = data.properties.find(p => p.id === d.propertyId); const unit = prop?.units.find(u => u.id === d.unitId); return ( ); })}
Datum Vorlage Empfänger Objekt Status
{fmtDateShort(d.date)}
{tmpl?.title || '—'}
{tmpl?.category}
{d.recipient}
{prop?.name}
{unit?.label}
)}
); } function TemplateCard({ template, onClick }) { const color = CATEGORY_COLORS[template.category] || 'var(--ink-3)'; return ( ); } function DocStatusPill({ status }) { const map = { 'entwurf': { tone: 'warn', label: 'Entwurf' }, 'versendet': { tone: 'pos', label: 'Versendet' }, 'unterzeichnet': { tone: 'pos', label: 'Unterzeichnet' }, }; const s = map[status] || { tone: '', label: status }; return ( {s.label} ); } // ───── Wizard ───── function ContractWizard({ data, route, setRoute }) { const template = CONTRACT_TEMPLATES.find(t => t.id === route.contractWizard); if (!template) return
Vorlage nicht gefunden.
; // Pre-select unit/tenant if route carries them const initialPropertyId = route.propertyId || data.properties[0].id; const [propertyId, setPropertyId] = React.useState(initialPropertyId); const property = data.properties.find(p => p.id === propertyId); const initialUnitId = route.unitId || property.units[0].id; const [unitId, setUnitId] = React.useState(initialUnitId); const unit = property.units.find(u => u.id === unitId) || property.units[0]; const tenant = unit.tenant ? data.tenants[unit.tenant] : null; // Wizard state — fields specific to template type const [form, setForm] = React.useState(() => initialFormFor(template, property, unit, tenant)); const [step, setStep] = React.useState(1); // 1 = configure, 2 = preview const previewRef = React.useRef(null); const [saving, setSaving] = React.useState(false); const [savedDoc, setSavedDoc] = React.useState(null); const [saveError, setSaveError] = React.useState(null); // Re-init form when unit changes React.useEffect(() => { setForm(initialFormFor(template, property, unit, tenant)); }, [unitId, propertyId]); const update = (k, v) => setForm(f => ({ ...f, [k]: v })); const generateAndSave = async () => { if (!previewRef.current || !window.html2pdf) { alert('PDF-Bibliothek nicht geladen. Lade die Seite neu.'); return; } setSaving(true); setSaveError(null); try { const filename = `${template.title.replace(/[^\w]+/g, '_')}__${(tenant?.name || 'Mieter').replace(/[^\w]+/g, '_')}__${form.issuedDate || new Date().toISOString().slice(0, 10)}.pdf`; const pdfBlob = await window.html2pdf() .from(previewRef.current) .set({ margin: [10, 10, 10, 10], filename, image: { type: 'jpeg', quality: 0.95 }, html2canvas: { scale: 2, useCORS: true, backgroundColor: '#ffffff' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }, }) .outputPdf('blob'); const file = new File([pdfBlob], filename, { type: 'application/pdf' }); const meta = { category: template.category || 'Vertrag', propertyId: property.id, unitId: unit.id, scope: 'einheit', }; const doc = await window.uploadDocument(file, meta); setSavedDoc(doc); } catch (err) { setSaveError(err.message || String(err)); } finally { setSaving(false); } }; return (

{template.title}

{template.description}
{step === 2 && ( )}
= 1 ? 'active' : ''}`}> 1 Daten prüfen
= 2 ? 'active' : ''}`}> 2 Vorschau & Versand

Objekt & Mieter

{tenant ? ( update('tenantName', e.target.value)} />
aus Stammdaten · {tenant.email}{tenant.phone ? ` · ${tenant.phone}` : ''}
) : ( <> { update('tenantName', e.target.value); }} placeholder="Vor- und Nachname" autoFocus />
Wohnung ist Leerstand — bitte Mieterdaten manuell eingeben.
{ update('tenantEmail', e.target.value); update('recipientEmail', e.target.value); }} placeholder="mieter@beispiel.de" /> update('tenantPhone', e.target.value)} placeholder="+49 …" /> )}

Versand

update('recipientEmail', e.target.value)} /> {form.sendPost && (
Hausgut druckt und versendet das Schreiben (1,80 € + Porto). Zustellnachweis im Dokument abgelegt.
)}
{step === 1 ? ( <> ) : ( <> )}
Vorschau {template.pages} Seiten
{savedDoc && (
✓ PDF gespeichert
Liegt in der Cloud unter {property.name} · {unit.label} · Kategorie {savedDoc.category}. {' '}PDF öffnen · ↓ Herunterladen
)} {saveError && (
✕ Speichern fehlgeschlagen: {saveError}
)}
Stammdaten automatisch befüllt · Felder gelb markiert prüfen
); } function FormRow({ label, children }) { return (
{children}
); } function initialFormFor(template, property, unit, tenant) { const today = new Date(2026, 4, 1); const fmtToday = today.toISOString().slice(0, 10); const base = { tenantName: tenant?.name || '', tenantEmail: tenant?.email || '', tenantPhone: tenant?.phone || '', recipientEmail: tenant?.email || '', issuedDate: fmtToday, sendPost: false, }; switch (template.id) { case 'mietvertrag-wohn': case 'mietvertrag-gewerbe': case 'staffelmietvertrag': return { ...base, startDate: fmtToday, coldRent: unit.rent || 0, operatingCosts: Math.round((unit.rent || 0) * 0.25), deposit: tenant?.deposit || (unit.rent || 0) * 3, contractType: 'unbefristet', purpose: template.id === 'mietvertrag-gewerbe' ? 'Gastronomie' : 'Wohnzwecke', indexClause: template.id === 'mietvertrag-gewerbe', staffelStart: unit.rent || 0, staffelStep: 30, }; case 'kuendigung-vermieter': return { ...base, reason: 'Eigenbedarf', reasonDetail: 'Die Wohnung wird für die Tochter des Eigentümers benötigt, die im Herbst ihr Studium am Standort aufnimmt.', terminationDate: '2026-10-31', }; case 'kuendigung-fristlos': return { ...base, reason: 'Mietrückstand', rentArrears: 2580, reasonDetail: 'Trotz Mahnung vom 25.04.2026 wurden die Mieten für März und April 2026 nicht beglichen.', }; case 'kuendigungsbestaetigung': return { ...base, receivedDate: '2026-04-30', moveOutDate: tenant?.until || '2026-07-31', }; case 'mieterhoehung': return { ...base, currentRent: unit.rent || 0, newRent: Math.round((unit.rent || 0) * 1.08), validFrom: '2026-09-01', reference: 'Mietspiegel der Stadt 2025', }; case 'modernisierung': return { ...base, measure: 'Energetische Sanierung der Fassade und Austausch der Fenster (3-fach-Verglasung)', beginDate: '2026-08-01', duration: '6 Wochen', costAllocation: Math.round((unit.rent || 0) * 0.12), }; case 'uebergabeprotokoll': return { ...base, handoverDate: fmtToday, meterStrom: '14582', meterWater: '892', meterHeat: '0', keyCount: 3, defects: 'Keine erkennbaren Mängel.', }; case 'nebenkostenabrechnung': return { ...base, period: '01.01.2025 – 31.12.2025', prepayments: Math.round((unit.rent || 0) * 0.25 * 12), actualCosts: Math.round((unit.rent || 0) * 0.27 * 12), allocation: 'Wohnfläche', }; case 'mahnung': return { ...base, amount: 980, dueDate: fmtToday, deadline: '2026-05-15', stage: 1, }; case 'mietquittung': return { ...base, period: 'Mai 2026', amount: unit.rent || 0, }; default: return base; } } // ───── Dynamic fields per template type ───── function DynamicFields({ template, form, update }) { const t = template.id; if (t.startsWith('mietvertrag') || t === 'staffelmietvertrag') { return (

Vertragsdaten

update('startDate', e.target.value)} /> update('coldRent', +e.target.value)} /> update('operatingCosts', +e.target.value)} /> update('deposit', +e.target.value)} />
Maximal 3 Kaltmieten ({fmtEUR(form.coldRent * 3)})
{t === 'mietvertrag-gewerbe' && ( <> update('purpose', e.target.value)} /> )} {t === 'staffelmietvertrag' && ( <> update('staffelStart', +e.target.value)} /> update('staffelStep', +e.target.value)} /> )}
); } if (t === 'kuendigung-vermieter') { return (

Kündigungsdetails