// 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"
/>
{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 || '—'}
);
})}
);
}
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 ;
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 &&
}
{c.mobile &&
}
{c.email &&
}
{c.website &&
}
{c.address &&
}
{c.iban &&
}
{c.ustId &&
}
{c.notes && (
)}
Buchungshistorie
{linkedTxns.length} Einträge
{linkedTxns.length === 0 ? (
Noch keine Buchungen verknüpft.
) : (
| Datum | Beschreibung | Objekt | Betrag |
{linkedTxns.map(tx => {
const prop = properties.find(p => p.id === tx.propertyId);
return (
| {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[0]?.label}
{data[data.length - 1]?.label}
);
}
const ContactIcon = {
Phone: () => ,
Mail: () => ,
Chat: () => ,
Web: () => ,
};
Object.assign(window, { ContactsPage, ContactDetail, ContactIcon });