import React, { useState } from "react"; import { generateId, formatCHF, formatDate } from "../utils.js"; import { Header, Modal, FormField, StatusBadge, useConfirm } from "../components/UI.jsx"; const CONTACT_TYPES = [ "Elektroplaner", "HLKSE-Planer", "Statiker", "Tragwerksplaner", "Kostenplaner", "Landschaftsarchitekt", "Bauphysiker", "Vermessungsingenieur", "Brandschutzspezialist", "Geologe", "Generalunternehmer", "Fachplaner", "Sonstiges", ]; const emptyFirm = { name: "", type: "", street: "", zip: "", city: "", country: "CH", email: "", phone: "", website: "", note: "", contacts: [], honorarOffers: [], isAuftraggeber: false, isPartner: false, }; const RoleBadge = ({ label, color, bg }) => ( {label} ); const SectionHead = ({ label }) => (
{label}
); // ── Detail View ──────────────────────────────────────────────────────────────── function FirmaDetail({ firma, data, onBack, onEdit, onAddPerson, onEditPerson, onDeletePerson, onDeleteFirm }) { const invoices = (data.invoices || []).filter(i => i.clientId === firma.id); const quotes = (data.quotes || []).filter(q => q.clientId === firma.id); const asClient = (data.projects || []).filter(p => p.clientId === firma.id); const asPartner = (data.projects || []).filter(p => (p.projectContacts || []).some(pc => pc.contactId === firma.id)); const allProjects = [...new Map([...asClient, ...asPartner].map(p => [p.id, p])).values()] .sort((a, b) => (b.startDate || "").localeCompare(a.startDate || "")); const protocols = (data.protocols || []).filter(proto => asPartner.some(p => p.id === proto.projectId) ).sort((a, b) => (b.date || "").localeCompare(a.date || "")); const addr = [firma.street, [firma.zip, firma.city].filter(Boolean).join(" ")].filter(Boolean).join(", "); const InfoRow = ({ label, value }) => value ? (
{label} {value}
) : null; return (
{/* Header */}
{firma.isAuftraggeber && } {firma.isPartner && } {firma.type && {firma.type}}

{firma.name}

{addr &&
{addr}
}
{/* KPIs */}
{[ { label: "PROJEKTE", value: allProjects.length }, ...(firma.isAuftraggeber ? [ { label: "RECHNUNGEN", value: invoices.length }, { label: "OFFERTEN", value: quotes.length }, { label: "OFFEN", value: formatCHF(invoices.filter(i => i.status === "gesendet" || i.status === "überfällig").reduce((s, i) => s + (i.total || 0), 0)), color: "#b5621e" }, ] : [ { label: "BETEILIGUNGEN", value: asPartner.length }, { label: "PROTOKOLLE", value: protocols.length }, { label: "ANSPRECHP.", value: (firma.contacts || []).length }, ]), ].map((k, i) => (
{k.label}
{k.value}
))}
{/* Projekte */} {allProjects.length > 0 && (
PROJEKTE
{allProjects.map(p => ( ))}
{p.number || "—"} {p.name} {asClient.find(x => x.id === p.id) && Auftraggeber} {asPartner.find(x => x.id === p.id) && Beteiligt}
)} {/* Rechnungen — nur bei Auftraggeber */} {firma.isAuftraggeber && (
RECHNUNGEN
{invoices.length === 0 ? (
Keine Rechnungen.
) : ( {[...invoices].sort((a, b) => (b.date || "").localeCompare(a.date || "")).map(inv => ( ))}
{inv.number || "—"} {formatDate(inv.date)} {formatCHF(inv.total || 0)}
)}
)} {/* Offerten — nur bei Auftraggeber */} {firma.isAuftraggeber && (
OFFERTEN
{quotes.length === 0 ? (
Keine Offerten.
) : ( {[...quotes].sort((a, b) => (b.date || "").localeCompare(a.date || "")).map(q => ( ))}
{q.number || "—"} {formatDate(q.date)} {q.mode === "sia" ? "SIA 102" : q.mode === "manual" ? "Manuell" : "Frei"} {formatCHF(q.sub || 0)}
)}
)} {/* Protokolle — nur bei Partner */} {firma.isPartner && protocols.length > 0 && (
PROTOKOLLE
{protocols.slice(0, 10).map(proto => { const proj = (data.projects || []).find(p => p.id === proto.projectId); return ( ); })}
{formatDate(proto.date)} {proto.type || "Protokoll"}{proto.number ? ` #${proto.number}` : ""} {proj?.name || ""}
)}
{/* Right column: Stammdaten + Ansprechpartner */}
STAMMDATEN
{firma.note &&
{firma.note}
}
ANSPRECHPARTNER
{(firma.contacts || []).length === 0 ? (
Keine Ansprechpartner erfasst.
) : (firma.contacts || []).map(c => (
{c.name}
{c.position &&
{c.position}
} {c.email &&
{c.email}
} {c.phone &&
{c.phone}
}
))}
); } // ── Main View ────────────────────────────────────────────────────────────────── export default function Personen({ data, update, saveAll, setView }) { const persons = data.persons || []; const projects = data.projects || []; const { askConfirm, ConfirmModalEl } = useConfirm(); const [selectedId, setSelectedId] = useState(null); const [search, setSearch] = useState(""); const [ansicht, setAnsicht] = useState("firmen"); const [groupBy, setGroupBy] = useState("alpha"); const [firmModal, setFirmModal] = useState(null); const [firmForm, setFirmForm] = useState({}); const [personModal, setPersonModal] = useState(null); const [personForm, setPersonForm] = useState({}); // ── Counts ───────────────────────────────────────────────────────────────── const allPersonsFlat = persons.flatMap(firma => (firma.contacts || []).map(c => ({ ...c, _firmaId: firma.id, _firmaName: firma.name, _firmaType: firma.type })) ); // ── Filtering ────────────────────────────────────────────────────────────── const q = search.toLowerCase(); const filteredFirmen = persons.filter(p => { if (ansicht === "auftraggeber" && !p.isAuftraggeber) return false; if (ansicht === "partner" && !p.isPartner) return false; if (q) { const addr = [p.street, p.zip, p.city].filter(Boolean).join(" "); const contactNames = (p.contacts || []).map(c => c.name).join(" "); if (![p.name, p.type, p.email, addr, contactNames].some(v => v?.toLowerCase().includes(q))) return false; } return true; }).sort((a, b) => a.name.localeCompare(b.name, "de")); const filteredPersons = allPersonsFlat.filter(c => { if (!q) return true; return [c.name, c.position, c.email, c.phone, c._firmaName].some(v => v?.toLowerCase().includes(q)); }).sort((a, b) => a.name.localeCompare(b.name, "de")); const showPersonsView = ansicht === "personen"; // ── Grouping ─────────────────────────────────────────────────────────────── const groupedFirmen = (() => { if (groupBy === "none") return [{ key: "_all", label: null, items: filteredFirmen }]; if (groupBy === "alpha") { const g = {}; filteredFirmen.forEach(p => { const k = p.name[0]?.toUpperCase() || "#"; (g[k] = g[k] || []).push(p); }); return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); } if (groupBy === "type") { const g = {}; filteredFirmen.forEach(p => { const k = p.isAuftraggeber && p.isPartner ? "Auftraggeber & Partner" : p.isAuftraggeber ? "Auftraggeber" : (p.type || "Ohne Typ"); (g[k] = g[k] || []).push(p); }); const order = ["Auftraggeber & Partner", "Auftraggeber"]; return Object.entries(g).sort((a, b) => { const oa = order.indexOf(a[0]), ob = order.indexOf(b[0]); if (oa !== -1 && ob !== -1) return oa - ob; if (oa !== -1) return -1; if (ob !== -1) return 1; return a[0].localeCompare(b[0]); }).map(([k, items]) => ({ key: k, label: k, items })); } })(); const groupedPersons = (() => { if (groupBy === "none") return [{ key: "_all", label: null, items: filteredPersons }]; if (groupBy === "alpha") { const g = {}; filteredPersons.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 === "firma") { const g = {}; filteredPersons.forEach(c => { (g[c._firmaName] = g[c._firmaName] || []).push(c); }); return Object.entries(g).sort((a, b) => a[0].localeCompare(b[0])).map(([k, items]) => ({ key: k, label: k, items })); } })(); const firmenGroupOpts = [{ id: "alpha", label: "Alphabetisch" }, { id: "type", label: "Nach Typ" }, { id: "none", label: "Keine" }]; const personGroupOpts = [{ id: "alpha", label: "Alphabetisch" }, { id: "firma", label: "Nach Firma" }, { id: "none", label: "Keine" }]; const activeGroupOpts = showPersonsView ? personGroupOpts : firmenGroupOpts; const hasFilter = search || ansicht !== "firmen"; // ── Firma CRUD ───────────────────────────────────────────────────────────── const openNewFirm = (preset = {}) => { setFirmForm({ ...emptyFirm, ...preset, _hauptName: "", _hauptPosition: "", _hauptPhone: "", _hauptEmail: "" }); setFirmModal({ isNew: true }); }; const openEditFirm = (id) => { const p = persons.find(x => x.id === id); if (!p) return; setFirmForm({ ...emptyFirm, ...p }); setFirmModal({ id }); }; const saveFirm = () => { if (!firmForm.name?.trim() || (!firmForm.isAuftraggeber && !firmForm.isPartner)) return; const { _hauptName, _hauptPosition, _hauptPhone, _hauptEmail, ...firmData } = firmForm; if (firmModal.id) { update("persons", persons.map(p => p.id === firmModal.id ? { ...p, ...firmData } : p)); } else { const hauptkontakt = _hauptName?.trim() ? [{ id: generateId(), name: _hauptName.trim(), position: _hauptPosition || "", phone: _hauptPhone || "", email: _hauptEmail || "" }] : []; update("persons", [...persons, { ...firmData, contacts: [...hauptkontakt, ...(firmData.contacts || [])], id: generateId() }]); } setFirmModal(null); }; const deleteFirm = async (id) => { if (!await askConfirm("Firma löschen? Referenzen in Projekten und Rechnungen bleiben erhalten.")) return; update("persons", persons.filter(p => p.id !== id)); if (selectedId === id) setSelectedId(null); }; const deleteContactFromFirm = async (firmaId, personId) => { if (!await askConfirm("Ansprechpartner entfernen?")) return; update("persons", persons.map(p => p.id === firmaId ? { ...p, contacts: (p.contacts || []).filter(c => c.id !== personId) } : p)); }; // ── Ansprechpartner CRUD ─────────────────────────────────────────────────── const openNewPerson = (firmaId = "") => { setPersonForm({ firmaId, name: "", position: "", phone: "", email: "" }); setPersonModal({ isNew: true }); }; const openEditPerson = (firmaId, personId) => { const firma = persons.find(p => p.id === firmaId); const person = (firma?.contacts || []).find(c => c.id === personId); if (!person) return; setPersonForm({ firmaId, name: person.name || "", position: person.position || "", phone: person.phone || "", email: person.email || "" }); setPersonModal({ firmaId, personId }); }; const savePerson = () => { if (!personForm.name?.trim() || !personForm.firmaId) return; if (personModal.personId) { update("persons", persons.map(p => p.id === personModal.firmaId ? { ...p, contacts: (p.contacts || []).map(c => c.id === personModal.personId ? { ...c, name: personForm.name, position: personForm.position, phone: personForm.phone, email: personForm.email } : c) } : p)); } else { const newContact = { id: generateId(), name: personForm.name.trim(), position: personForm.position || "", phone: personForm.phone || "", email: personForm.email || "" }; update("persons", persons.map(p => p.id === personForm.firmaId ? { ...p, contacts: [...(p.contacts || []), newContact] } : p)); } setPersonModal(null); }; // ── Detail view ──────────────────────────────────────────────────────────── const selectedFirma = selectedId ? persons.find(p => p.id === selectedId) : null; if (selectedFirma) { return (
{ConfirmModalEl} setSelectedId(null)} onEdit={() => openEditFirm(selectedFirma.id)} onAddPerson={() => openNewPerson(selectedFirma.id)} onEditPerson={openEditPerson} onDeletePerson={deleteContactFromFirm} onDeleteFirm={deleteFirm} /> {firmModal && renderFirmModal()} {personModal && renderPersonModal()}
); } // ── List view helpers ────────────────────────────────────────────────────── function renderFirmModal() { return ( setFirmModal(null)} onSave={saveFirm} > setFirmForm({ ...firmForm, name: e.target.value })} style={!firmForm.name?.trim() ? { borderColor: "#b5621e" } : {}} />
{!firmForm.isAuftraggeber && !firmForm.isPartner && (
Mindestens eine Rolle auswählen.
)}
{(firmForm.isPartner || firmForm.type) && ( )}
setFirmForm({ ...firmForm, street: e.target.value })} /> setFirmForm({ ...firmForm, zip: e.target.value })} style={{ maxWidth: 90 }} /> setFirmForm({ ...firmForm, city: e.target.value })} />
setFirmForm({ ...firmForm, email: e.target.value })} /> setFirmForm({ ...firmForm, phone: e.target.value })} /> setFirmForm({ ...firmForm, website: e.target.value })} />