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 (
);
}
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 (
{t('common.loading')}
);
}
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')}
{t('common.edit')}
{t('common.delete')}
{/* Tab Bar */}
{allTabs.map(tab => (
setActiveTab(tab.id)}
>
{tab.icon && getIcon(tab.icon)}
{tab.label}
))}
{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}
}
{ setEditingPerson(person); setPersonModalOpen(true); }}
className="text-xs text-primary-600 hover:text-primary-700 px-2 py-1 min-h-touch"
>
{t('common.edit')}
handleDeletePerson(person)}
className="text-xs text-danger-600 hover:text-danger-700 px-2 py-1 min-h-touch"
>
{t('common.delete')}
))}
)}
{/* 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}
/>
);
}