/** * MergeDialog — Merge confirmation dialog with per-field value selection. * Shows both contacts side-by-side, lets user choose source/target/manual value * for each mergeable field, then calls useMergeContacts(). */ import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; import { AlertTriangle } from 'lucide-react'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { useToast } from '@/components/ui/Toast'; import { useMergeContacts, type DuplicatePair, type DuplicateContactBrief } from '@/api/dedup'; export interface MergeDialogProps { open: boolean; pair: DuplicatePair | null; onClose: () => void; } type FieldChoice = 'source' | 'target' | 'manual'; interface FieldConfig { key: keyof DuplicateContactBrief; labelKey: string; labelFallback: string; } const MERGE_FIELDS: FieldConfig[] = [ { key: 'firstname', labelKey: 'dedup.field.firstname', labelFallback: 'Vorname' }, { key: 'surname', labelKey: 'dedup.field.surname', labelFallback: 'Nachname' }, { key: 'email_1', labelKey: 'dedup.field.email', labelFallback: 'E-Mail' }, { key: 'phone_1', labelKey: 'dedup.field.phone', labelFallback: 'Telefon' }, { key: 'mailing_city', labelKey: 'dedup.field.city', labelFallback: 'Stadt' }, { key: 'mailing_postalcode', labelKey: 'dedup.field.postalcode', labelFallback: 'PLZ' }, ]; function getFieldValue(contact: DuplicateContactBrief | undefined, key: keyof DuplicateContactBrief): string { if (!contact) return ''; const val = contact[key]; return val == null ? '' : String(val); } export function MergeDialog({ open, pair, onClose }: MergeDialogProps) { const { t } = useTranslation(); const toast = useToast(); const mergeMutation = useMergeContacts(); const [choices, setChoices] = useState>({}); const [manualValues, setManualValues] = useState>({}); const [note, setNote] = useState(''); // Reset state when dialog opens with a new pair useEffect(() => { if (open && pair) { const initialChoices: Record = {}; const initialManual: Record = {}; for (const field of MERGE_FIELDS) { initialChoices[field.key] = 'target'; initialManual[field.key] = ''; } setChoices(initialChoices); setManualValues(initialManual); setNote(''); } }, [open, pair]); const handleChoiceChange = (fieldKey: string, choice: FieldChoice) => { setChoices((prev) => ({ ...prev, [fieldKey]: choice })); }; const handleManualChange = (fieldKey: string, value: string) => { setManualValues((prev) => ({ ...prev, [fieldKey]: value })); }; // Build field_overrides from choices const fieldOverrides = useMemo(() => { if (!pair) return {}; const overrides: Record = {}; for (const field of MERGE_FIELDS) { const choice = choices[field.key] ?? 'target'; if (choice === 'manual') { overrides[field.key] = manualValues[field.key] ?? ''; } else if (choice === 'source') { overrides[field.key] = getFieldValue(pair.source_contact, field.key); } // 'target' means no override — surviving record keeps its value } return overrides; }, [choices, manualValues, pair]); const handleMerge = () => { if (!pair) return; mergeMutation.mutate( { source_contact_id: pair.source_contact.id, target_contact_id: pair.target_contact.id, field_overrides: fieldOverrides, note: note.trim() || undefined, }, { onSuccess: () => { toast.success(t('dedup.mergeSuccess', 'Kontakte wurden erfolgreich zusammengeführt.')); onClose(); }, onError: (error: unknown) => { const message = error instanceof Error ? error.message : t('dedup.mergeError', 'Fehler beim Zusammenführen der Kontakte.'); toast.error(message); }, }, ); }; if (!pair) return null; const sourceName = pair.source_contact.firstname || pair.source_contact.surname ? `${pair.source_contact.firstname ?? ''} ${pair.source_contact.surname ?? ''}`.trim() : pair.source_contact.displayname || pair.source_contact.id; const targetName = pair.target_contact.firstname || pair.target_contact.surname ? `${pair.target_contact.firstname ?? ''} ${pair.target_contact.surname ?? ''}`.trim() : pair.target_contact.displayname || pair.target_contact.id; return (
{/* Warning */}
{/* Contact names summary */}
{sourceName} {targetName}
{/* Field-by-field selection table */}
{MERGE_FIELDS.map((field) => { const sourceVal = getFieldValue(pair.source_contact, field.key); const targetVal = getFieldValue(pair.target_contact, field.key); const choice = choices[field.key] ?? 'target'; return ( ); })}
{t('dedup.field', 'Feld')} {t('dedup.sourceContact', 'Quell-Kontakt')} {t('dedup.targetContact', 'Ziel-Kontakt')} {t('dedup.manual', 'Manuell')}
{t(field.labelKey, field.labelFallback)}
handleChoiceChange(field.key, 'manual')} className="h-4 w-4 text-primary-600 focus:ring-primary-500" /> { handleManualChange(field.key, e.target.value); if (choice !== 'manual') handleChoiceChange(field.key, 'manual'); }} disabled={choice !== 'manual'} className={clsx( 'flex-1 rounded-md border px-2 py-1 text-sm min-h-touch', 'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500', choice === 'manual' ? 'border-primary-400 text-secondary-900' : 'border-secondary-200 text-secondary-400 bg-secondary-50', )} placeholder={t('dedup.manualPlaceholder', 'Eigener Wert…')} />
{/* Note textarea */}