import { useState, useRef, useCallback } from 'react'; import { Modal, Select, Typography, Radio, Tag, Space, Button, Divider, message, } from 'antd'; import { SearchOutlined, SwapOutlined } from '@ant-design/icons'; import { api } from '@/lib/api'; import type { Contact, UnifiedPerson, PeopleListResponse, MergeContactPayload } from '@/types/api'; import { SUPPORT_LEVEL_LABELS, CONTACT_SOURCE_LABELS, CONTACT_SOURCE_COLORS } from '@/types/api'; interface MergeContactModalProps { open: boolean; targetContact: Contact; onClose: () => void; onMerged: () => void; } type MergeField = 'displayName' | 'email' | 'phone' | 'supportLevel' | 'tags' | 'notes'; export default function MergeContactModal({ open, targetContact, onClose, onMerged }: MergeContactModalProps) { const [searchResults, setSearchResults] = useState([]); const [searching, setSearching] = useState(false); const [sourcePerson, setSourcePerson] = useState(null); const [fieldChoices, setFieldChoices] = useState>({ displayName: 'target', email: 'target', phone: 'target', supportLevel: 'target', tags: 'target', notes: 'target', }); const [merging, setMerging] = useState(false); const searchTimerRef = useRef>(undefined); const handleSearch = useCallback((value: string) => { clearTimeout(searchTimerRef.current); if (!value.trim()) { setSearchResults([]); return; } searchTimerRef.current = setTimeout(async () => { setSearching(true); try { const { data } = await api.get('/people', { params: { search: value, limit: 8 }, }); // Filter out the target contact setSearchResults(data.people.filter((p) => p.contactId !== targetContact.id)); } catch { setSearchResults([]); } finally { setSearching(false); } }, 300); }, [targetContact.id]); const handleFieldChoice = (field: MergeField, value: 'target' | 'source') => { setFieldChoices((prev) => ({ ...prev, [field]: value })); }; const handleMerge = async () => { if (!sourcePerson) { message.warning('Please select a person to merge'); return; } const colonIdx = sourcePerson.id.indexOf(':'); const sourceType = colonIdx !== -1 ? sourcePerson.id.substring(0, colonIdx) : 'contact'; const sourceId = colonIdx !== -1 ? sourcePerson.id.substring(colonIdx + 1) : sourcePerson.id; setMerging(true); try { const payload: MergeContactPayload = { sourceType: sourceType as 'user' | 'addr' | 'contact', sourceId, keepFields: fieldChoices as Record, }; await api.post(`/people/contacts/${targetContact.id}/merge`, payload); message.success('Contacts merged successfully'); onMerged(); } catch { message.error('Failed to merge contacts'); } finally { setMerging(false); } }; const handleClose = () => { setSourcePerson(null); setSearchResults([]); setFieldChoices({ displayName: 'target', email: 'target', phone: 'target', supportLevel: 'target', tags: 'target', notes: 'target', }); onClose(); }; const getSourceValue = (field: MergeField): string => { if (!sourcePerson) return '--'; switch (field) { case 'displayName': return sourcePerson.displayName || '--'; case 'email': return sourcePerson.email || '--'; case 'phone': return sourcePerson.phone || '--'; case 'supportLevel': return sourcePerson.supportLevel ? SUPPORT_LEVEL_LABELS[sourcePerson.supportLevel] : '--'; case 'tags': return sourcePerson.tags?.join(', ') || '--'; case 'notes': return '(from source)'; default: return '--'; } }; const getTargetValue = (field: MergeField): string => { switch (field) { case 'displayName': return targetContact.displayName || '--'; case 'email': return targetContact.email || '--'; case 'phone': return targetContact.phone || '--'; case 'supportLevel': return targetContact.supportLevel ? SUPPORT_LEVEL_LABELS[targetContact.supportLevel] : '--'; case 'tags': return targetContact.tags?.join(', ') || '--'; case 'notes': return targetContact.notes || '--'; default: return '--'; } }; const mergeFields: { field: MergeField; label: string }[] = [ { field: 'displayName', label: 'Display Name' }, { field: 'email', label: 'Email' }, { field: 'phone', label: 'Phone' }, { field: 'supportLevel', label: 'Support Level' }, { field: 'tags', label: 'Tags (union of both)' }, { field: 'notes', label: 'Notes (concatenated)' }, ]; return ( Merge Contacts } open={open} onCancel={handleClose} width={700} footer={[ , , ]} > {/* Search for source person */}
Search for the person to merge INTO this contact: