import React, { useMemo, useState } from 'react'; import { Modal } from '../ui/Modal'; import { AvatarPicker } from '../ui/AvatarPicker'; import { Avatar, inputClass, labelClass } from './leadUi'; import { connectionsApi, leadsApi } from '../../lib/api'; import { useToast } from '../Toast'; import { CONNECTION_TYPES, LEAD_STATUSES, leadFullName, leadInitials, type AppUser, type ConnectionType, type Lead, type LeadConnection, type LeadStatus, } from '../../types'; interface LeadModalProps { lead: Lead | null; leads: Lead[]; users: AppUser[]; connections: LeadConnection[]; onClose: () => void; onSaved: () => void; /** * Nur beim Neuanlegen: liefert den frisch gespeicherten Kontakt zurueck. * Damit kann der Aufrufer ihn direkt weiterverwenden - etwa als * Ansprechpartner einer Opportunity. */ onCreated?: (lead: Lead) => void; } interface FormState { salutation: string; first_name: string; last_name: string; company: string; position: string; email: string; phone: string; mobile: string; linkedin: string; website: string; street: string; zip: string; city: string; country: string; status: LeadStatus; source: string; notes: string; avatar: string; owner_id: string; tags: string[]; /** Einverstaendnis in Werbemails - Voraussetzung fuer die Mailingliste. */ mail_consent: boolean; } function toFormState(lead: Lead | null, fallbackOwnerId: number | null): FormState { return { salutation: lead?.salutation ?? '', first_name: lead?.first_name ?? '', last_name: lead?.last_name ?? '', company: lead?.company ?? '', position: lead?.position ?? '', email: lead?.email ?? '', phone: lead?.phone ?? '', mobile: lead?.mobile ?? '', linkedin: lead?.linkedin ?? '', website: lead?.website ?? '', street: lead?.street ?? '', zip: lead?.zip ?? '', city: lead?.city ?? '', country: lead?.country ?? '', status: lead?.status ?? 'Neu', source: lead?.source ?? '', notes: lead?.notes ?? '', avatar: lead?.avatar ?? '', owner_id: String(lead?.owner_id ?? fallbackOwnerId ?? ''), tags: lead?.tags ?? [], mail_consent: lead?.mail_consent ?? false, }; } export const LeadModal: React.FC = ({ lead, leads, users, connections, onClose, onSaved, onCreated, }) => { const { addToast } = useToast(); const [form, setForm] = useState(() => toFormState(lead, users[0]?.id ?? null)); const [tagInput, setTagInput] = useState(''); const [isSaving, setIsSaving] = useState(false); // Verbindungen dieses Kontakts const [newConnectionId, setNewConnectionId] = useState(''); const [newConnectionType, setNewConnectionType] = useState('Netzwerk'); const [isLinking, setIsLinking] = useState(false); const leadById = useMemo(() => new Map(leads.map((l) => [l.id, l])), [leads]); const ownConnections = useMemo(() => { if (!lead) return []; return connections .filter((c) => c.from_lead_id === lead.id || c.to_lead_id === lead.id) .map((c) => { const otherId = c.from_lead_id === lead.id ? c.to_lead_id : c.from_lead_id; return { connection: c, other: leadById.get(otherId) ?? null }; }) .filter((entry) => entry.other !== null); }, [lead, connections, leadById]); const linkableLeads = useMemo(() => { if (!lead) return []; const linked = new Set(ownConnections.map((entry) => entry.other!.id)); return leads .filter((candidate) => candidate.id !== lead.id && !linked.has(candidate.id)) .sort((a, b) => leadFullName(a).localeCompare(leadFullName(b), 'de')); }, [lead, leads, ownConnections]); const set = (key: K, value: FormState[K]) => setForm((prev) => ({ ...prev, [key]: value })); const addTags = () => { const parsed = tagInput .split(',') .map((t) => t.trim()) .filter((t) => t && !form.tags.includes(t)); if (parsed.length > 0) set('tags', [...form.tags, ...parsed]); setTagInput(''); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); if (!form.first_name.trim() || !form.last_name.trim()) { addToast('Vorname und Nachname sind erforderlich.', 'error'); return; } if (form.linkedin && !form.linkedin.includes('linkedin.com/')) { addToast('Bitte eine gültige LinkedIn-URL angeben.', 'error'); return; } setIsSaving(true); try { const payload = { ...form, owner_id: form.owner_id === '' ? null : Number(form.owner_id), avatar: form.avatar || null, }; if (lead) { await leadsApi.update(lead.id, payload); addToast('Kontakt aktualisiert.', 'success'); } else { const created = await leadsApi.create(payload); addToast('Kontakt angelegt.', 'success'); onCreated?.(created); } onSaved(); onClose(); } catch (err) { addToast(err instanceof Error ? err.message : 'Speichern fehlgeschlagen.', 'error'); } finally { setIsSaving(false); } }; const handleAddConnection = async () => { if (!lead || !newConnectionId) return; setIsLinking(true); try { await connectionsApi.create({ from_lead_id: lead.id, to_lead_id: Number(newConnectionId), type: newConnectionType, }); addToast('Verbindung angelegt.', 'success'); setNewConnectionId(''); onSaved(); } catch (err) { addToast(err instanceof Error ? err.message : 'Verbindung fehlgeschlagen.', 'error'); } finally { setIsLinking(false); } }; const handleRemoveConnection = async (id: number) => { try { await connectionsApi.remove(id); addToast('Verbindung entfernt.', 'success'); onSaved(); } catch (err) { addToast(err instanceof Error ? err.message : 'Entfernen fehlgeschlagen.', 'error'); } }; return ( } >
{/* Foto + Kerndaten */}
set('avatar', value)} initials={leadInitials({ first_name: form.first_name, last_name: form.last_name })} />
set('first_name', e.target.value)} className={inputClass} />
set('last_name', e.target.value)} className={inputClass} />
set('company', e.target.value)} className={inputClass} />
set('position', e.target.value)} className={inputClass} />
{/* Kontaktdaten */}

Kontaktdaten

set('email', e.target.value)} className={inputClass} />
set('phone', e.target.value)} className={inputClass} />
set('mobile', e.target.value)} className={inputClass} />
set('linkedin', e.target.value)} className={inputClass} />
set('website', e.target.value)} className={inputClass} />
{/* * Einverstaendnis in Werbemails. Steht bewusst direkt unter der * E-Mail-Adresse und nicht bei den Tags: es gehoert zur Adresse und * entscheidet, ob sie in eine Mailingliste darf. */}
{/* Adresse */}

Adresse

set('street', e.target.value)} className={inputClass} />
set('zip', e.target.value)} className={inputClass} />
set('city', e.target.value)} className={inputClass} />
set('country', e.target.value)} className={inputClass} />
{/* Einordnung */}

Einordnung

set('source', e.target.value)} className={inputClass} />
{form.tags.length > 0 && (
{form.tags.map((tag) => ( {tag} ))}
)}
setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); addTags(); } }} placeholder="Mit Komma trennen, z. B. S/4HANA, M365" className={inputClass} />