"use client"; import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import { CombineIcon, LoaderCircleIcon, TriangleAlertIcon } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Command, CommandEmpty, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { CompanyAvatar } from "@/components/pipeline/company-avatar"; import { companyDomain } from "@/lib/company"; import { mergeCompaniesAction, previewCompanyMergeAction } from "@/server/actions"; export type MergeCandidate = { id: string; name: string; website: string; applications: number; contacts: number; }; type Plan = Awaited>; /** * Folding a duplicate employer into this one. * * A one-way door, so it is built as one: you pick the duplicate, the server * says exactly what will move, and only then does the confirm button light up. * The plan comes from the data layer rather than being re-derived here — * whether the notes get appended is a rule about the data, and there is * supposed to be one implementation of it. * * CompanyAvatar rather than CompanyChip on the rows: the chip is a link, and a * link inside a dialog row you are trying to select is a trap. */ export function MergeCompaniesDialog({ company, candidates, logos, suggestedId, }: { company: { id: string; name: string }; candidates: MergeCandidate[]; logos: boolean; /** Pre-selected when the list flagged these two as probable duplicates. */ suggestedId?: string; }) { const router = useRouter(); const [open, setOpen] = useState(false); const [picked, setPicked] = useState(null); const [plan, setPlan] = useState(null); const [loading, startLoading] = useTransition(); const [merging, startMerging] = useTransition(); const choose = (candidate: MergeCandidate) => { setPicked(candidate); setPlan(null); startLoading(async () => { try { setPlan(await previewCompanyMergeAction(company.id, candidate.id)); } catch (error) { toast.error(error instanceof Error ? error.message : "Could not work that out."); setPicked(null); } }); }; const confirm = () => { if (!picked || !plan) return; startMerging(async () => { try { await mergeCompaniesAction(company.id, picked.id); toast.success(`${picked.name} folded into ${company.name}`); setOpen(false); setPicked(null); setPlan(null); router.refresh(); } catch (error) { toast.error(error instanceof Error ? error.message : "Could not merge those."); } }); }; const others = candidates.filter((candidate) => candidate.id !== company.id); return ( { setOpen(next); if (!next) { setPicked(null); setPlan(null); } else if (suggestedId) { const suggestion = others.find((candidate) => candidate.id === suggestedId); if (suggestion) choose(suggestion); } }} > Merge another company into {company.name} Everything on the company you pick moves here, and that company is deleted.{" "} {company.name} keeps its name. {others.length === 0 ? (

There is nothing else on file to merge in.

) : ( No company matches. {others.map((candidate) => ( choose(candidate)} className={cnRow(picked?.id === candidate.id)} > {candidate.name} {candidate.applications || 0} app{candidate.applications === 1 ? "" : "s"} ·{" "} {candidate.contacts || 0} {candidate.contacts === 1 ? "person" : "people"} ))} )} {loading && (

Working out what would move…

)} {plan && picked && (

{plan.applications === 0 && plan.contacts === 0 ? `Nothing to move from ${plan.merge.name}.` : `Moving from ${plan.merge.name} to ${plan.keep.name}:`}

    {plan.applications > 0 && (
  • {plan.applications} application{plan.applications === 1 ? "" : "s"} {plan.movingRoles.length > 0 && ( — {plan.movingRoles.join(", ")} )}
  • )} {plan.contacts > 0 && (
  • {plan.contacts} {plan.contacts === 1 ? "person" : "people"}
  • )} {plan.fills.length > 0 && (
  • Filling {plan.keep.name}'s empty{" "} {plan.fills.map((fill) => fill.field).join(", ")}
  • )} {plan.notesAppended &&
  • Their notes appended to yours, nothing overwritten
  • }

{plan.merge.name} is deleted and its page stops working. This cannot be undone, and nothing is de-duplicated — identical roles stay as two.

)}
); } function cnRow(active: boolean) { return `flex items-center gap-2 px-2 py-1.5 text-[13px] ${active ? "bg-accent" : ""}`; }