// Contacts (Kontakte) — suppliers, tradespeople, advisors function ContactsPage({ data, setRoute, route, setContactRoute }) { const { contacts, properties, transactions, store } = data; const [filter, setFilter] = React.useState('alle'); const [search, setSearch] = React.useState(''); const [showForm, setShowForm] = React.useState(false); const [editing, setEditing] = React.useState(null); const [deleting, setDeleting] = React.useState(null); if (route.contactId) { return ; } const categories = ['alle', 'Favoriten', 'Handwerk', 'Service', 'Beratung', 'Versicherung', 'Verwaltung']; let filtered = contacts || []; if (filter === 'Favoriten') filtered = filtered.filter(c => c.favorite); else if (filter !== 'alle') filtered = filtered.filter(c => c.category === filter); if (search) { const q = search.toLowerCase(); filtered = filtered.filter(c => (c.company || '').toLowerCase().includes(q) || (c.contactPerson || '').toLowerCase().includes(q) || (c.trade || '').toLowerCase().includes(q) ); } const openAdd = () => { setEditing(null); setShowForm(true); }; const openEdit = (c) => { setEditing(c); setShowForm(true); }; // Empty state — no contacts at all if ((contacts || []).length === 0) { return (

Kontakte

Handwerker · Lieferanten · Berater
setShowForm(false)} store={store} initial={editing} />
👤
Noch keine Kontakte
Lege Handwerker, Steuerberater, Versicherungen oder andere Lieferanten an — einmal eingetragen, kannst du Aufträge mit ihnen verknüpfen.
); } // Spend per contact YTD (transactions linked via contact_id) const spendByContact = {}; const yr = String(new Date().getFullYear()); transactions.filter(t => t.date.startsWith(yr) && t.type === 'ausgabe' && t.contactId).forEach(t => { spendByContact[t.contactId] = (spendByContact[t.contactId] || 0) + Math.abs(t.amount); }); return (

Kontakte

{contacts.length} {contacts.length === 1 ? 'Kontakt' : 'Kontakte'}
setShowForm(false)} store={store} initial={editing} /> setDeleting(null)} onConfirm={async () => { await store.deleteContact(deleting.id); }} title="Kontakt löschen" message={`„${deleting?.company}" endgültig löschen?`} confirmLabel="Endgültig löschen" />
setSearch(e.target.value)} />
{categories.map(c => ( ))}
{filtered.map(c => { const spend = spendByContact[c.id] || 0; return (
setRoute({ page: 'contacts', contactId: c.id })} style={{ cursor: 'pointer', textAlign: 'left' }}>
{c.company.split(' ').map(s => s[0]).filter(Boolean).slice(0, 2).join('')}
{c.favorite ? : null}
{c.trade &&
{c.trade}
}
{c.company}
{c.contactPerson &&
{c.contactPerson}
}
YTD Umsatz
{spend ? fmtEUR(spend) : '—'}
Kategorie
{c.category || '—'}
{c.phone && ( e.stopPropagation()} title="Anrufen"> )} {c.mobile && ( e.stopPropagation()} title="SMS"> )} {c.email && ( e.stopPropagation()} title="E-Mail"> )} {c.website && ( e.stopPropagation()} title="Webseite"> )}
); })}
); } function ContactDetail({ data, contactId, setRoute }) { const { contacts, properties, transactions, maintenance, store } = data; const c = contacts.find(x => x.id === contactId); const [showEdit, setShowEdit] = React.useState(false); const [showDelete, setShowDelete] = React.useState(false); if (!c) return
Kontakt nicht gefunden.
; const linkedTxns = transactions.filter(t => t.contactId === c.id); const linkedMaint = (maintenance || []).filter(m => m.contactId === c.id); const yr = String(new Date().getFullYear()); const spendYTD = linkedTxns.filter(t => t.date.startsWith(yr)).reduce((s, t) => s + Math.abs(t.amount), 0); const spendTotal = linkedTxns.reduce((s, t) => s + Math.abs(t.amount), 0); // Properties used in via linked maintenance + transactions const linkedPropIds = new Set([ ...linkedTxns.map(t => t.propertyId).filter(Boolean), ...linkedMaint.map(m => m.propertyId).filter(Boolean), ]); const linkedProps = properties.filter(p => linkedPropIds.has(p.id)); // Spend by month for sparkline (12 months back) const months = []; const now = new Date(); for (let i = 11; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; const v = linkedTxns.filter(t => t.date.startsWith(key)).reduce((s, t) => s + Math.abs(t.amount), 0); months.push({ key, label: d.toLocaleDateString('de-DE', { month: 'short' }), value: v }); } return (
setShowEdit(false)} store={store} initial={c} /> setShowDelete(false)} onConfirm={async () => { await store.deleteContact(c.id); setRoute({ page: 'contacts' }); }} title="Kontakt löschen" message={`„${c.company}" endgültig löschen? Verknüpfte Buchungen / Wartungsaufträge bleiben erhalten (Verknüpfung wird gelöst).`} confirmLabel="Endgültig löschen" />
{[c.category, c.trade].filter(Boolean).join(' · ') || 'Kontakt'}

{c.company}{c.favorite ? ' ★' : ''}

{c.contactPerson || '—'}
{c.phone && Anrufen} {c.email && E-Mail} {c.mobile && SMS} {c.website && Webseite}
{c.phone &&
Telefon
{c.phone}
} {c.mobile &&
Mobil
{c.mobile}
} {c.email &&
E-Mail
{c.email}
} {c.website &&
Webseite
{c.website}
} {c.address &&
Adresse
{c.address}
} {c.iban &&
IBAN
{c.iban}
} {c.ustId &&
USt-IdNr.
{c.ustId}
}
{c.notes && (

Notiz

„{c.notes}"
)}

Buchungshistorie

{linkedTxns.length} Einträge
{linkedTxns.length === 0 ? (
Noch keine Buchungen verknüpft.
) : (
{linkedTxns.map(tx => { const prop = properties.find(p => p.id === tx.propertyId); return ( ); })}
DatumBeschreibungObjektBetrag
{fmtDateShort(tx.date)} {tx.description} {prop ? prop.name : '—'} −{fmtEUR(Math.abs(tx.amount))}
)}
{linkedProps.length > 0 && (

Eingesetzt bei

{linkedProps.map(p => ( ))}
)} {linkedMaint.length > 0 && (

Wartungsaufträge

{linkedMaint.slice(0, 5).map(m => (
{m.title}
{m.status} · {m.dueDate ? fmtDateShort(m.dueDate) : 'kein Datum'}
))}
)}
); } function Sparkline({ data }) { const W = 240, H = 56; const max = Math.max(...data.map(d => d.value), 1); const bw = W / data.length; return (
Umsatz · 12 Monate
{data.map((d, i) => { const h = (d.value / max) * (H - 8); return ; })}
{data[0]?.label} {data[data.length - 1]?.label}
); } const ContactIcon = { Phone: () => , Mail: () => , Chat: () => , Web: () => , }; Object.assign(window, { ContactsPage, ContactDetail, ContactIcon });