import { useState, useEffect } from "react"; import { Merge, CircleX, AlertTriangle, ArrowDown } from "lucide-react"; import { useDataProvider, useRecordContext, useGetList, useGetManyReference, required, Form, useNotify, useRedirect, useTranslate, } from "ra-core"; import type { Identifier } from "ra-core"; import { useMutation } from "@tanstack/react-query"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from "@/components/ds/ui/dialog"; import { Button } from "@/components/ds/ui/button"; import { ReferenceInput } from "@/components/ds/admin/reference-input"; import { AutocompleteInput } from "@/components/ds/admin/autocomplete-input"; import { Alert, AlertDescription, AlertTitle } from "@/components/ds/ui/alert"; import type { Contact } from "../types"; import { contactOptionText } from "../misc/ContactOption"; export const ContactMergeButton = () => { const [mergeDialogOpen, setMergeDialogOpen] = useState(false); const translate = useTranslate(); return ( <> setMergeDialogOpen(true)} > {translate("crm.contact.action.merge_with_another")} setMergeDialogOpen(false)} /> > ); }; interface ContactMergeDialogProps { open: boolean; onClose: () => void; } const ContactMergeDialog = ({ open, onClose }: ContactMergeDialogProps) => { const loserContact = useRecordContext(); const notify = useNotify(); const redirect = useRedirect(); const dataProvider = useDataProvider(); const translate = useTranslate(); const [winnerId, setWinnerId] = useState(null); const [suggestedWinnerId, setSuggestedWinnerId] = useState( null, ); const [isMerging, setIsMerging] = useState(false); const { mutateAsync } = useMutation({ mutationKey: ["contacts", "merge", { loserId: loserContact?.id, winnerId }], mutationFn: async () => { return dataProvider.mergeContacts(loserContact?.id, winnerId); }, }); // Find potential contacts with matching first and last name const { data: matchingContacts } = useGetList( "contacts", { filter: { first_name: loserContact?.first_name, last_name: loserContact?.last_name, "id@neq": `${loserContact?.id}`, // Exclude current contact }, pagination: { page: 1, perPage: 10 }, }, { enabled: open && !!loserContact }, ); // Get counts of items to be merged const canFetchCounts = open && !!loserContact && !!winnerId; const { total: tasksCount } = useGetManyReference( "tasks", { target: "contact_id", id: loserContact?.id, pagination: { page: 1, perPage: 1 }, }, { enabled: canFetchCounts }, ); const { total: notesCount } = useGetManyReference( "contactNotes", { target: "contact_id", id: loserContact?.id, pagination: { page: 1, perPage: 1 }, }, { enabled: canFetchCounts }, ); const { total: dealsCount } = useGetList( "deals", { filter: { "contact_ids@cs": `{${loserContact?.id}}` }, pagination: { page: 1, perPage: 1 }, }, { enabled: canFetchCounts }, ); useEffect(() => { if (matchingContacts && matchingContacts.length > 0) { const suggestedWinnerId = matchingContacts[0].id; setSuggestedWinnerId(suggestedWinnerId); setWinnerId(suggestedWinnerId); } }, [matchingContacts]); const handleMerge = async () => { if (!winnerId || !loserContact) { notify(translate("crm.contact.merge.select_contact"), { type: "warning", }); return; } try { setIsMerging(true); await mutateAsync(); setIsMerging(false); notify(translate("crm.contact.merge.success"), { type: "success" }); redirect(`/contacts/${winnerId}/show`); onClose(); } catch (error) { setIsMerging(false); notify(translate("crm.contact.merge.error"), { type: "error" }); console.error("Merge failed:", error); } }; if (!loserContact) return null; return ( {translate("crm.contact.merge.title")} {translate("crm.contact.merge.description")} {translate("crm.contact.merge.current_contact")} {contactOptionText} {translate("crm.contact.merge.target_contact")} {winnerId && ( <> {translate("crm.contact.merge.what_will_be_merged")} {notesCount != null && notesCount > 0 && ( •{" "} {translate("crm.contact.merge.notes_to_merge", { smart_count: notesCount, })} )} {tasksCount != null && tasksCount > 0 && ( •{" "} {translate("crm.contact.merge.tasks_to_merge", { smart_count: tasksCount, })} )} {dealsCount != null && dealsCount > 0 && ( •{" "} {translate("crm.contact.merge.deals_to_merge", { smart_count: dealsCount, })} )} {loserContact.email_jsonb?.length > 0 && ( •{" "} {translate("crm.contact.merge.emails_to_merge", { smart_count: loserContact.email_jsonb.length, })} )} {loserContact.phone_jsonb?.length > 0 && ( •{" "} {translate("crm.contact.merge.phones_to_merge", { smart_count: loserContact.phone_jsonb.length, })} )} {!notesCount && !tasksCount && !dealsCount && !loserContact.email_jsonb?.length && !loserContact.phone_jsonb?.length && ( {translate("crm.contact.merge.no_data")} )} {translate("crm.contact.merge.warning_title")} {translate("crm.contact.merge.warning_message")} > )} {translate("crm.activity.cancel")} {isMerging ? translate("crm.contact.merge.merging") : translate("crm.contact.merge.merge_contacts")} ); };
{translate("crm.contact.merge.current_contact")}
{translate("crm.contact.merge.target_contact")}
{translate("crm.contact.merge.what_will_be_merged")}