import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Badge } from '@/components/ui/Badge'; import { EmptyState } from '@/components/ui/EmptyState'; import { Modal } from '@/components/ui/Modal'; import { Input } from '@/components/ui/Input'; import { useToast } from '@/components/ui/Toast'; import { Loader2 } from 'lucide-react'; import * as LucideIcons from 'lucide-react'; import { HistoryViewer } from '@/components/HistoryViewer'; import { usePluginStore } from '@/store/pluginStore'; import { useAIUIControlStore } from '@/store/aiUIControlStore'; import { PluginPage } from '@/components/plugins/PluginLoader'; import { useAuthStore } from '@/store/authStore'; import { CustomFieldRenderer } from '@/components/contacts/CustomFieldRenderer'; import { TagBadge } from '@/components/tags/TagBadge'; import { EntityHistoryPanel } from '@/components/common/EntityHistoryPanel'; import { useCustomFields } from '@/api/customFields'; import { type UnifiedContact, type ContactPerson, useDeleteUnifiedContact, useCreateContactPerson, useUpdateContactPerson, useDeleteContactPerson, } from '@/api/hooks'; export interface ContactDetailProps { contact: UnifiedContact | null; loading?: boolean; onEdit: () => void; onDeleted: () => void; } function Field({ label, value }: { label: string; value?: string | null }) { return (
{label}
{value || '—'}
); } function Section({ title, children, action }: { title: string; children: React.ReactNode; action?: React.ReactNode }) { return (

{title}

{action}
{children}
); } function AddressBlock({ prefix, contact, t }: { prefix: string; contact: UnifiedContact; t: (k: string) => string }) { const street = (contact as any)[`${prefix}_street`] as string | undefined; const number = (contact as any)[`${prefix}_number`] as string | undefined; const postalcode = (contact as any)[`${prefix}_postalcode`] as string | undefined; const city = (contact as any)[`${prefix}_city`] as string | undefined; const state = (contact as any)[`${prefix}_state`] as string | undefined; const country = (contact as any)[`${prefix}_country`] as string | undefined; const district = (contact as any)[`${prefix}_district`] as string | undefined; const extra = (contact as any)[`${prefix}_extra_address_line`] as string | undefined; const hasData = street || city || postalcode; if (!hasData) return null; return (
{extra &&
{extra}
}
{street} {number}
{district &&
{district}
}
{postalcode} {city}
{state &&
{state}
} {country &&
{country}
}
); } function ContactPersonModal({ open, onClose, onSave, person, }: { open: boolean; onClose: () => void; onSave: (data: Partial) => void; person?: ContactPerson | null; }) { const { t } = useTranslation(); const [firstname, setFirstname] = useState(person?.firstname || ''); const [lastname, setLastname] = useState(person?.lastname || ''); const [func, setFunc] = useState(person?.function || ''); const [email, setEmail] = useState(person?.email || ''); const [phone, setPhone] = useState(person?.phone || ''); const [mobilephone, setMobilephone] = useState(person?.mobilephone || ''); React.useEffect(() => { if (open) { setFirstname(person?.firstname || ''); setLastname(person?.lastname || ''); setFunc(person?.function || ''); setEmail(person?.email || ''); setPhone(person?.phone || ''); setMobilephone(person?.mobilephone || ''); } }, [open, person]); const handleSave = () => { onSave({ firstname: firstname || null, lastname: lastname || null, function: func || null, email: email || null, phone: phone || null, mobilephone: mobilephone || null, displayname: [firstname, lastname].filter(Boolean).join(' ') || firstname || lastname || '', }); }; return (
setFirstname(e.target.value)} /> setLastname(e.target.value)} />
setFunc(e.target.value)} /> setEmail(e.target.value)} />
setPhone(e.target.value)} /> setMobilephone(e.target.value)} />
); } function getIcon(name: string): React.ReactNode { const Icon = (LucideIcons as any)[name]; return Icon ? : ; } export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDetailProps) { const { t } = useTranslation(); const toast = useToast(); const deleteMutation = useDeleteUnifiedContact(); const createPersonMutation = useCreateContactPerson(); const updatePersonMutation = useUpdateContactPerson(); const deletePersonMutation = useDeleteContactPerson(); const [personModalOpen, setPersonModalOpen] = useState(false); const [editingPerson, setEditingPerson] = useState(null); const [activeTab, setActiveTab] = useState('details'); const manifests = usePluginStore(s => s.manifests); const pluginTabs = useMemo( () => manifests .flatMap((m) => m.detail_tabs) .filter((t) => t.entity_type === 'contact') .sort((a, b) => a.order - b.order), [manifests] ); const aiActiveTab = useAIUIControlStore(s => s.activeTab); const aiActiveModal = useAIUIControlStore(s => s.activeModal); // Sync tab from AI UI Control (Phase 4.8) useEffect(() => { if (aiActiveTab) { setActiveTab(aiActiveTab); } }, [aiActiveTab]); // Sync modal from AI UI Control (Phase 4.7) useEffect(() => { if (aiActiveModal === 'edit' || aiActiveModal === 'create') { setEditingPerson(null); setPersonModalOpen(true); } else if (aiActiveModal === 'close') { setPersonModalOpen(false); } }, [aiActiveModal]); const user = useAuthStore(s => s.user); // Custom fields from plugin definitions const customFieldDefs = useMemo( () => manifests .flatMap((m) => m.custom_fields || []) .filter((cf) => cf.entity === 'contact'), [manifests] ); const { data: customFieldsData } = useCustomFields(contact?.id); if (loading) { return (
); } if (!contact) { return (
); } const displayName = contact.displayname || contact.name || [contact.firstname, contact.surname].filter(Boolean).join(' ') || contact.id; const persons = contact.contact_persons || []; const tags = contact.tags ? contact.tags.split(',').map((t) => t.trim()).filter(Boolean) : []; const handleDelete = async () => { if (!window.confirm(t('contacts.deleteConfirm'))) return; try { await deleteMutation.mutateAsync({ id: contact.id }); toast.success(t('contacts.deleted')); onDeleted(); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleSavePerson = async (data: Partial) => { try { if (editingPerson) { await updatePersonMutation.mutateAsync({ contactId: contact.id, personId: editingPerson.id, data }); toast.success(t('contacts.personUpdated')); } else { await createPersonMutation.mutateAsync({ contactId: contact.id, data }); toast.success(t('contacts.personCreated')); } setPersonModalOpen(false); setEditingPerson(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDeletePerson = async (person: ContactPerson) => { if (!window.confirm(t('contacts.deletePersonConfirm'))) return; try { await deletePersonMutation.mutateAsync({ contactId: contact.id, personId: person.id }); toast.success(t('contacts.personDeleted')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const visiblePluginTabs = pluginTabs.filter(tab => { if (!tab.permission) return true; return user?.permissions?.includes(tab.permission) ?? false; }); const allTabs = [ { id: 'details', label: t('contacts.details', 'Details'), icon: null }, ...visiblePluginTabs.map(tab => ({ id: tab.component, label: t(tab.label_key, tab.label), icon: tab.icon, component: tab.component, })), ]; return (
{/* Header */}
{displayName.charAt(0).toUpperCase()}

{displayName}

{contact.type === 'company' ? t('contacts.companies') : t('contacts.persons')}
{/* Tab Bar */}
{allTabs.map(tab => ( ))}
{activeTab === 'details' ? (
{/* Contact Persons */}
{ setEditingPerson(null); setPersonModalOpen(true); }}> {t('contacts.addPerson')} } > {persons.length === 0 ? (

{t('contacts.noPersons')}

) : (
    {persons.map((person) => (
  • {[person.firstname, person.lastname].filter(Boolean).join(' ') || person.displayname}
    {person.function &&
    {person.function}
    } {person.email &&
    {person.email}
    } {person.phone &&
    {person.phone}
    }
  • ))}
)}
{/* Addresses */}

{t('contacts.mailingAddress')}

{!contact.mailing_street && !contact.mailing_city &&

}

{t('contacts.visitAddress')}

{!contact.visit_street && !contact.visit_city &&

}

{t('contacts.invoiceAddress')}

{!contact.invoice_street && !contact.invoice_city &&

}
{/* Communication */}
{/* Financial */}
{/* Discounts */}
{/* Geo */}
{/* Notes & Tags */}
{t('tags.title')}
{tags.length > 0 ? (
{tags.map((tag) => ( ))}
) : ( )}
{/* Custom Fields */} {contact.id && customFieldDefs.length > 0 && (
)} {/* History */} {contact.id && (
)} {/* Entity History Timeline */} {contact.id && (
)}
) : (
{allTabs.filter((t): t is { id: string; label: string; icon: string; component: string } => t.id !== 'details').map(tab => ( activeTab === tab.id && ( ) ))}
)} { setPersonModalOpen(false); setEditingPerson(null); }} onSave={handleSavePerson} person={editingPerson} />
); }