import { useState, useEffect, useRef, useCallback } from 'react'; import { Modal } from '../../components/Modal'; import { useCreateDeal, useUpdateDeal, usePipelines, useDealTypes, useSetCustomFieldValues } from '../hooks'; import { contactsApi, companiesApi } from '../api'; import { CustomFieldsForm } from '../CustomFieldsForm'; import type { Deal, DealStatus, LostReason, Contact, Company, CustomFieldValue } from '../types'; import { LOST_REASON_LABELS } from '../types'; interface DealFormModalProps { isOpen: boolean; onClose: () => void; deal?: Deal | null; onSuccess: () => void; } const STATUS_OPTIONS: { value: DealStatus; label: string }[] = [ { value: 'OPEN', label: 'Offen' }, { value: 'WON', label: 'Gewonnen' }, { value: 'LOST', label: 'Verloren' }, ]; const labelStyle: React.CSSProperties = { fontSize: '0.875rem', fontWeight: 500, color: 'var(--color-text)', marginBottom: '0.25rem', display: 'block', }; const inputStyle: React.CSSProperties = { width: '100%', padding: '0.625rem 0.75rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', fontSize: '0.9375rem', outline: 'none', boxSizing: 'border-box', background: 'var(--color-bg-card)', color: 'var(--color-text)', }; const rowStyle: React.CSSProperties = { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem', }; export function DealFormModal({ isOpen, onClose, deal, onSuccess, }: DealFormModalProps) { const isEditMode = !!deal; const createMutation = useCreateDeal(); const updateMutation = useUpdateDeal(); const setCustomFieldValues = useSetCustomFieldValues(); const mutation = isEditMode ? updateMutation : createMutation; const customFieldValuesRef = useRef>({}); const customFields: CustomFieldValue[] = deal?.customFields ?? []; const handleCustomFieldsChange = useCallback( (values: Record) => { customFieldValuesRef.current = values; }, [], ); const { data: pipelinesData } = usePipelines(); const pipelines = pipelinesData?.data ?? []; const { data: dealTypesData } = useDealTypes(); const dealTypes = dealTypesData?.data ?? []; const [error, setError] = useState(''); const [title, setTitle] = useState(''); const [pipelineId, setPipelineId] = useState(''); const [stageId, setStageId] = useState(''); const [status, setStatus] = useState('OPEN'); const [value, setValue] = useState(''); const [currency, setCurrency] = useState('EUR'); const [expectedCloseDate, setExpectedCloseDate] = useState(''); const [notes, setNotes] = useState(''); const [lostReason, setLostReason] = useState(''); const [lostReasonText, setLostReasonText] = useState(''); const [dealTypeId, setDealTypeId] = useState(''); // Kontakt-Suche const [contactSearch, setContactSearch] = useState(''); const [contactResults, setContactResults] = useState([]); const [selectedContact, setSelectedContact] = useState<{ id: string; name: string; } | null>(null); const [showContactDropdown, setShowContactDropdown] = useState(false); const contactRef = useRef(null); const searchTimeoutRef = useRef>(); // Unternehmen-Suche const [companySearch, setCompanySearch] = useState(''); const [companyResults, setCompanyResults] = useState([]); const [selectedCompany, setSelectedCompany] = useState<{ id: string; name: string; } | null>(null); const [showCompanyDropdown, setShowCompanyDropdown] = useState(false); const companyRef = useRef(null); const companySearchTimeout = useRef>(); // Stages der gewaehlten Pipeline const selectedPipeline = pipelines.find((p) => p.id === pipelineId); const stages = selectedPipeline?.stages ? [...selectedPipeline.stages].sort((a, b) => a.sortOrder - b.sortOrder) : []; // Click-Outside für Kontakt- und Unternehmen-Dropdown useEffect(() => { function handleClick(e: MouseEvent) { if ( contactRef.current && !contactRef.current.contains(e.target as Node) ) { setShowContactDropdown(false); } if ( companyRef.current && !companyRef.current.contains(e.target as Node) ) { setShowCompanyDropdown(false); } } document.addEventListener('mousedown', handleClick); return () => document.removeEventListener('mousedown', handleClick); }, []); // Kontakt suchen (debounced) useEffect(() => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); if (!contactSearch || contactSearch.length < 2) { setContactResults([]); return; } searchTimeoutRef.current = setTimeout(async () => { try { const res = await contactsApi.list({ search: contactSearch, pageSize: 8, }); setContactResults(res.data); setShowContactDropdown(true); } catch { setContactResults([]); } }, 300); return () => { if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current); }; }, [contactSearch]); // Unternehmen suchen (debounced) useEffect(() => { if (companySearchTimeout.current) clearTimeout(companySearchTimeout.current); if (!companySearch || companySearch.length < 2) { setCompanyResults([]); return; } companySearchTimeout.current = setTimeout(async () => { try { const res = await companiesApi.list({ search: companySearch, pageSize: 8, }); setCompanyResults(res.data); setShowCompanyDropdown(true); } catch { setCompanyResults([]); } }, 300); return () => { if (companySearchTimeout.current) clearTimeout(companySearchTimeout.current); }; }, [companySearch]); useEffect(() => { if (isOpen) { setError(''); if (deal) { setTitle(deal.title); setPipelineId(deal.pipelineId); setStageId(deal.stageId); setStatus(deal.status); setValue(deal.value ? String(parseFloat(deal.value)) : ''); setCurrency(deal.currency ?? 'EUR'); setExpectedCloseDate( deal.expectedCloseDate ? deal.expectedCloseDate.slice(0, 10) : '', ); setNotes(deal.notes ?? ''); setLostReason((deal.lostReason as LostReason) ?? ''); setLostReasonText(deal.lostReasonText ?? ''); setDealTypeId(deal.dealTypeId ?? ''); if (deal.contact) { const { id, firstName, lastName, companyName } = deal.contact; const name = companyName || [firstName, lastName].filter(Boolean).join(' ') || 'Kontakt'; setSelectedContact({ id, name }); setContactSearch(name); } else { setSelectedContact(null); setContactSearch(''); } if (deal.company) { setSelectedCompany({ id: deal.company.id, name: deal.company.name }); setCompanySearch(deal.company.name); } else { setSelectedCompany(null); setCompanySearch(''); } } else { setTitle(''); setPipelineId(pipelines.find((p) => p.isDefault)?.id ?? pipelines[0]?.id ?? ''); setStageId(''); setStatus('OPEN'); setValue(''); setCurrency('EUR'); setExpectedCloseDate(''); setNotes(''); setLostReason(''); setLostReasonText(''); setDealTypeId(''); setSelectedContact(null); setContactSearch(''); setSelectedCompany(null); setCompanySearch(''); } setContactResults([]); setShowContactDropdown(false); setCompanyResults([]); setShowCompanyDropdown(false); } }, [isOpen, deal, pipelines]); // Wenn Pipeline wechselt, erste Stage auswaehlen useEffect(() => { if (!isEditMode && stages.length > 0 && !stages.find((s) => s.id === stageId)) { setStageId(stages[0].id); } }, [pipelineId, stages, stageId, isEditMode]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); setError(''); if (!title.trim()) { setError('Titel ist ein Pflichtfeld'); return; } if (!pipelineId) { setError('Pipeline auswählen'); return; } if (!stageId) { setError('Stage auswählen'); return; } if (status === 'LOST' && !lostReason) { setError('Bitte einen Grund fuer den Verlust angeben'); return; } const payload = { title: title.trim(), pipelineId, stageId, status, ...(selectedContact ? { contactId: selectedContact.id } : {}), ...(selectedCompany ? { companyId: selectedCompany.id } : {}), ...(value ? { value: parseFloat(value) } : {}), currency, ...(expectedCloseDate ? { expectedCloseDate: new Date(expectedCloseDate).toISOString() } : {}), ...(notes ? { notes } : {}), ...(status === 'LOST' && lostReason ? { lostReason } : {}), ...(status === 'LOST' && lostReasonText ? { lostReasonText } : {}), ...(dealTypeId ? { dealTypeId } : {}), }; const saveCustomFields = (entityId: string) => { const vals = customFieldValuesRef.current; const entries = Object.entries(vals); if (entries.length === 0) return; setCustomFieldValues.mutate({ entityId, data: { values: entries.map(([fieldDefId, value]) => ({ fieldDefId, value })), }, }); }; if (isEditMode && deal) { updateMutation.mutate( { id: deal.id, data: payload }, { onSuccess: () => { saveCustomFields(deal.id); onSuccess(); }, onError: (err: unknown) => { const msg = (err as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Fehler beim Speichern'; setError(msg); }, }, ); } else { createMutation.mutate(payload, { onSuccess: (res) => { if (res?.data?.id) saveCustomFields(res.data.id); onSuccess(); }, onError: (err: unknown) => { const msg = (err as { response?: { data?: { error?: { message?: string } } } }) ?.response?.data?.error?.message ?? 'Fehler beim Anlegen'; setError(msg); }, }); } }; return (
{error && (
{error}
)} {/* Titel */}
setTitle(e.target.value)} placeholder="Vorgangs-Titel" required />
{/* Pipeline leer – Hinweis */} {pipelines.length === 0 && (
⚠️ Keine Pipelines vorhanden. Bitte zuerst unter{' '} CRM Einstellungen → Pipelines {' '} eine Pipeline anlegen.
)} {/* Pipeline + Stage */}
{/* Vorgangsart */}
{dealTypes.length === 0 && (

Keine Vorgangsarten konfiguriert.{' '} In CRM Einstellungen anlegen

)}
{/* Kontakt-Suche */}
{ setContactSearch(e.target.value); if (selectedContact) setSelectedContact(null); }} onFocus={() => { if (contactResults.length > 0) setShowContactDropdown(true); }} placeholder="Kontakt suchen..." /> {selectedContact && ( )} {showContactDropdown && contactResults.length > 0 && (
{contactResults.map((c) => { const name = c.type === 'ORGANIZATION' ? c.companyName : [c.firstName, c.lastName].filter(Boolean).join(' '); return (
{ setSelectedContact({ id: c.id, name: name ?? '' }); setContactSearch(name ?? ''); setShowContactDropdown(false); }} style={{ padding: '0.5rem 0.75rem', cursor: 'pointer', fontSize: '0.875rem', borderBottom: '1px solid var(--color-border)', }} onMouseEnter={(e) => ((e.target as HTMLDivElement).style.background = 'var(--color-bg)') } onMouseLeave={(e) => ((e.target as HTMLDivElement).style.background = 'transparent') } > {name} {c.email && ( {c.email} )}
); })}
)}
{/* Unternehmen-Suche */}
{ setCompanySearch(e.target.value); if (selectedCompany) setSelectedCompany(null); }} onFocus={() => { if (companyResults.length > 0) setShowCompanyDropdown(true); }} placeholder="Unternehmen suchen..." /> {selectedCompany && ( )} {showCompanyDropdown && companyResults.length > 0 && (
{companyResults.map((comp) => (
{ setSelectedCompany({ id: comp.id, name: comp.name }); setCompanySearch(comp.name); setShowCompanyDropdown(false); }} style={{ padding: '0.5rem 0.75rem', cursor: 'pointer', fontSize: '0.875rem', borderBottom: '1px solid var(--color-border)', }} onMouseEnter={(e) => ((e.target as HTMLDivElement).style.background = 'var(--color-bg)') } onMouseLeave={(e) => ((e.target as HTMLDivElement).style.background = 'transparent') } > {comp.name} {comp.industry && ( {comp.industry} )}
))}
)}
{/* Wert + Währung */}
setValue(e.target.value)} placeholder="0.00" />
setCurrency(e.target.value)} maxLength={3} placeholder="EUR" />
{/* Status + Erw. Abschluss */}
setExpectedCloseDate(e.target.value)} />
{/* Lost-Reason (nur bei Status LOST) */} {status === 'LOST' && ( <>