import React, { useState } from "react"; import { generateId } from "../utils.js"; import { Header, Modal, FormField, useConfirm } from "../components/UI.jsx"; export default function Clients({ data, update, modal, setModal, setView }) { const clients = data.clients || []; const { askConfirm, ConfirmModalEl } = useConfirm(); const [selectedId, setSelectedId] = useState(() => { const id = window.__navToClient || null; window.__navToClient = null; return id; }); const [search, setSearch] = useState(""); const [groupBy, setGroupBy] = useState("alpha"); const [contactModal, setContactModal] = useState(null); const [contactForm, setContactForm] = useState({ name: "", position: "", email: "", phone: "" }); const [showHauptPicker, setShowHauptPicker] = useState(false); const emptyForm = { name: "", street: "", zip: "", city: "", country: "CH", email: "", phone: "", website: "", contacts: [], _contactName: "", _contactPosition: "", }; const [form, setForm] = useState(emptyForm); const selectedClient = clients.find(c => c.id === selectedId) || null; // ── Client speichern ── const save = () => { if (!form.name.trim()) return; const { _contactName, _contactPosition, ...clientData } = form; let contacts = clientData.contacts || []; if (_contactName.trim() && !modal?.id) { contacts = [{ id: generateId(), name: _contactName.trim(), position: _contactPosition.trim(), email: "", phone: "" }]; } const client = { ...clientData, contacts, id: modal?.id || generateId() }; update("clients", modal?.id ? clients.map(c => c.id === modal.id ? client : c) : [...clients, client]); setModal(null); }; const openNew = () => { setForm(emptyForm); setModal({ type: "client" }); }; const openEdit = (c) => { setForm({ ...emptyForm, ...c, _contactName: "", _contactPosition: "" }); setModal({ type: "client", id: c.id }); }; const del = async (id) => { if (await askConfirm("Kunde löschen? Alle zugehörigen Projekte verlieren die Kundenzuordnung.")) { update("clients", clients.filter(c => c.id !== id)); if (selectedId === id) setSelectedId(null); } }; // ── Kontakt speichern ── const saveContact = () => { if (!contactForm.name.trim()) return; const client = clients.find(c => c.id === contactModal.clientId); if (!client) return; const contacts = client.contacts || []; const updated = contactModal.contactId ? contacts.map(ct => ct.id === contactModal.contactId ? { ...ct, ...contactForm } : ct) : [...contacts, { ...contactForm, id: generateId() }]; update("clients", clients.map(c => c.id === client.id ? { ...c, contacts: updated } : c)); setContactModal(null); }; const delContact = async (clientId, contactId) => { if (await askConfirm("Kontaktperson löschen?")) { const client = clients.find(c => c.id === clientId); update("clients", clients.map(c => c.id === clientId ? { ...c, contacts: (c.contacts || []).filter(ct => ct.id !== contactId) } : c)); } }; // ── Detail-Ansicht ── if (selectedId && selectedClient) { const projs = (data.projects || []).filter(p => p.clientId === selectedId).sort((a, b) => (b.startDate || "").localeCompare(a.startDate || "")); const invoices = (data.invoices || []).filter(i => i.clientId === selectedId).sort((a, b) => (b.date || "").localeCompare(a.date || "")); const quotes = (data.quotes || []).filter(q => q.clientId === selectedId).sort((a, b) => (b.date || "").localeCompare(a.date || "")); const contacts = selectedClient.contacts || []; const hauptkontakt = contacts[0] || null; const addressLine = [selectedClient.street, [selectedClient.zip, selectedClient.city].filter(Boolean).join(" ")].filter(Boolean).join(", "); const navTo = (view) => { window.__navClientId = selectedId; setView(view); }; const formatCHF = (v) => v != null ? `CHF ${Number(v).toLocaleString("de-CH", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : "—"; const fmtDate = (s) => s ? new Date(s).toLocaleDateString("de-CH") : "—"; return (
{ConfirmModalEl}

{selectedClient.name}

{addressLine &&
{addressLine}
}
{/* Firmeninfo */}
FIRMENINFO
{[ { label: "E-Mail", value: selectedClient.email, href: `mailto:${selectedClient.email}` }, { label: "Telefon", value: selectedClient.phone }, { label: "Website", value: selectedClient.website, href: selectedClient.website?.startsWith("http") ? selectedClient.website : selectedClient.website ? `https://${selectedClient.website}` : null }, { label: "Adresse", value: addressLine || null }, ].filter(r => r.value).map(({ label, value, href }) => (
{label} {href ? {value} : {value}}
))} {contacts.length > 0 && (
HAUPTKONTAKT
{contacts.length > 1 && ( )}
{showHauptPicker ? (
{contacts.map((ct, i) => ( ))}
) : hauptkontakt ? ( <>
{hauptkontakt.name}
{hauptkontakt.position &&
{hauptkontakt.position}
}
{hauptkontakt.email && {hauptkontakt.email}} {hauptkontakt.phone && {hauptkontakt.phone}}
) : null}
)}
{/* Ansprechpartner */}
0 ? "1px solid #ece8e2" : "none" }}>
ANSPRECHPARTNER ({contacts.length})
{contacts.length === 0 ? (
Noch keine Ansprechpartner erfasst.
) : ( contacts.map((ct, i) => (
{ct.name} {i === 0 && HAUPT}
{ct.position &&
{ct.position}
}
{ct.email && {ct.email}} {ct.phone && {ct.phone}}
)) )}
{/* Projekte */}
0 ? "1px solid #ece8e2" : "none" }}> PROJEKTE ({projs.length}) {projs.length > 0 && }
{projs.length === 0 ?
Noch keine Projekte.
: <> {projs.slice(0, 5).map(p => ( ))}
ProjektKategorieStatusBudget
{p.name}{p.number && {p.number}} {p.category || "—"} {p.status} {p.budget > 0 ? formatCHF(p.budget) : "—"}
{projs.length > 5 &&
+{projs.length - 5} weitere —
} }
{/* Rechnungen */}
0 ? "1px solid #ece8e2" : "none" }}> RECHNUNGEN ({invoices.length}) {invoices.length > 0 && }
{invoices.length === 0 ?
Noch keine Rechnungen.
: <> {invoices.slice(0, 5).map(inv => { const proj = inv.projectId ? (data.projects || []).find(p => p.id === inv.projectId) : null; return ( ); })}
Nr.DatumProjektStatusBetrag
{inv.number} {fmtDate(inv.date)} {proj?.name || "—"} {inv.status} {formatCHF(inv.total)}
{invoices.length > 5 &&
+{invoices.length - 5} weitere —
} }
{/* Offerten */}
0 ? "1px solid #ece8e2" : "none" }}> OFFERTEN ({quotes.length}) {quotes.length > 0 && }
{quotes.length === 0 ?
Noch keine Offerten.
: <> {quotes.slice(0, 5).map(q => ( ))}
Nr.DatumModusStatusHonorar
{q.number} {fmtDate(q.date)} {q.mode === "sia" ? "SIA 102" : q.mode === "manual" ? "Aufwand" : "Frei"} {q.status || "—"} {formatCHF(q.total)}
{quotes.length > 5 &&
+{quotes.length - 5} weitere —
} }
{/* Kontakt-Modal */} {contactModal && ( setContactModal(null)} onSave={saveContact}>
setContactForm({ ...contactForm, name: e.target.value })} autoFocus /> setContactForm({ ...contactForm, position: e.target.value })} placeholder="z.B. Geschäftsführer, Bauleiter…" />
setContactForm({ ...contactForm, email: e.target.value })} /> setContactForm({ ...contactForm, phone: e.target.value })} />
)} {/* Client-Edit-Modal */} {modal?.type === "client" && modal.id && ( setModal(null)} onSave={save} wide> {clientFormFields(form, setForm)} )}
); } // ── Listen-Ansicht ── const filteredClients = clients.filter(c => { if (!search) return true; const q = search.toLowerCase(); return [c.name, c.city, c.email, c.street, ...(c.contacts || []).map(ct => ct.name)].some(v => v?.toLowerCase().includes(q)); }); const clientGroups = (() => { if (groupBy === "none") return [{ key: "_all", label: null, items: filteredClients }]; if (groupBy === "alpha") { const g = {}; [...filteredClients].sort((a, b) => a.name.localeCompare(b.name, "de")) .forEach(c => { const k = c.name[0]?.toUpperCase() || "#"; (g[k] = g[k] || []).push(c); }); return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); } if (groupBy === "city") { const g = {}; [...filteredClients].sort((a, b) => a.name.localeCompare(b.name, "de")) .forEach(c => { const k = c.city || "Ohne Ort"; (g[k] = g[k] || []).push(c); }); return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); } })(); const ClientTable = ({ items }) => (
{items.length === 0 && } {items.map(c => { const projs = (data.projects || []).filter(p => p.clientId === c.id).length; const cts = c.contacts || []; const hauptkontakt = cts[0]; const city = [c.zip, c.city].filter(Boolean).join(" "); return ( setSelectedId(c.id)}> ); })}
Firmenname Adresse Hauptkontakt Kontakte Projekte
Keine Treffer
{c.name} {c.email &&
{c.email}
}
{c.street &&
{c.street}
} {city &&
{city}
}
{hauptkontakt ? ( <>
{hauptkontakt.name}
{hauptkontakt.position &&
{hauptkontakt.position}
} ) : }
{cts.length || "—"} {projs || "—"} e.stopPropagation()}>
); return (
{ConfirmModalEl}
+ Neuer Kunde} />
setSearch(e.target.value)} style={{ flex: "1 1 200px", maxWidth: 300, fontSize: 12 }} />
{clients.length === 0 ? (
Noch keine Kunden erfasst.
) : filteredClients.length === 0 ? (
Keine Treffer
) : clientGroups.map(group => (
{group.label && (
{group.label.toUpperCase()} {group.items.length}
)}
))} {modal?.type === "client" && ( setModal(null)} onSave={save} wide> {clientFormFields(form, setForm, !modal.id)} )}
); } function clientFormFields(form, setForm, isNew = false) { return ( <> setForm({ ...form, name: e.target.value })} autoFocus placeholder="z.B. Müller Immobilien AG" />
setForm({ ...form, street: e.target.value })} placeholder="Bahnhofstrasse 1" /> setForm({ ...form, zip: e.target.value })} style={{ maxWidth: 100 }} /> setForm({ ...form, city: e.target.value })} /> setForm({ ...form, country: e.target.value.toUpperCase() })} maxLength={2} style={{ maxWidth: 70 }} />
setForm({ ...form, email: e.target.value })} /> setForm({ ...form, phone: e.target.value })} /> setForm({ ...form, website: e.target.value })} placeholder="www.beispiel.ch" />
{isNew && ( <>
HAUPTKONTAKT (optional)
setForm({ ...form, _contactName: e.target.value })} placeholder="z.B. Hans Müller" /> setForm({ ...form, _contactPosition: e.target.value })} placeholder="z.B. Geschäftsführer" />
Weitere Ansprechpartner können in der Kundendetailseite hinzugefügt werden.
)} ); }