Ctrl K
ComunidadGuías…

src/components/crm/contact-companies.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { useState, useTransition } from "react";
import { PlusIcon, XIcon } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
  Command,
  CommandEmpty,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { CompanyAvatar } from "@/components/pipeline/company-avatar";
import { CompanyChip } from "@/components/crm/company-chip";
import { companyDomain } from "@/lib/company";
import { setContactCompaniesAction } from "@/server/actions";

export type CompanyRef = { id: string; name: string; website: string };

/**
 * Everywhere a person represents, as chips you can add to and take away.
 *
 * A contact used to point at one company, which meant choosing which of a
 * founder's three hats to keep. The set is written whole on every change —
 * same rule as an application's sources — and the server hands back what it
 * stored, so the chips settle on the truth rather than on what was clicked.
 */
export function ContactCompanies({
  contactId,
  companies,
  options,
  logos,
  size = "sm",
  onChanged,
}: {
  contactId: string;
  companies: CompanyRef[];
  /** Every company on file, for the picker. */
  options: CompanyRef[];
  logos: boolean;
  size?: "sm" | "md";
  onChanged?: (companies: CompanyRef[]) => void;
}) {
  const [value, setValue] = useState(companies);
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState("");
  const [pending, startTransition] = useTransition();

  const write = (ids: string[], create?: string) => {
    startTransition(async () => {
      try {
        const next = await setContactCompaniesAction(contactId, ids, create);
        setValue(next);
        onChanged?.(next);
      } catch (error) {
        toast.error(error instanceof Error ? error.message : "Could not change that.");
      }
    });
  };

  const add = (company: CompanyRef) => {
    setOpen(false);
    setQuery("");
    if (value.some((item) => item.id === company.id)) return;
    write([...value.map((item) => item.id), company.id]);
  };

  const create = (name: string) => {
    setOpen(false);
    setQuery("");
    write(
      value.map((item) => item.id),
      name,
    );
  };

  const drop = (id: string) =>
    write(value.filter((item) => item.id !== id).map((item) => item.id));

  const linked = new Set(value.map((item) => item.id));
  const typed = query.trim();
  const exists = options.some((item) => item.name.toLowerCase() === typed.toLowerCase());

  return (
    <div className="flex flex-wrap items-center gap-1.5">
      {value.map((company) => (
        // The remove control sits beside the chip's link rather than inside
        // it: a button nested in an anchor is invalid, and the chip's whole
        // job is to be a link to the company.
        <span key={company.id} className="relative inline-flex max-w-full items-center">
          <CompanyChip company={company} logos={logos} size={size} className="pr-6" />
          <button
            type="button"
            onClick={() => drop(company.id)}
            disabled={pending}
            aria-label={`Unlink ${company.name}`}
            title={`Unlink ${company.name}`}
            className="text-faint hover:text-destructive absolute right-1 flex size-4 items-center justify-center rounded-full transition-colors"
          >
            <XIcon className="size-3" />
          </button>
        </span>
      ))}

      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button
            variant="ghost"
            size="xs"
            disabled={pending}
            className="text-muted-foreground gap-1 px-1.5"
          >
            <PlusIcon className="size-3" />
            {value.length === 0 ? "Company" : "Add"}
          </Button>
        </PopoverTrigger>
        <PopoverContent align="start" className="w-72 p-0">
          <Command loop>
            <CommandInput
              value={query}
              onValueChange={setQuery}
              placeholder="Which company?"
              className="h-9"
            />
            <CommandList>
              {/* Only when there is nothing to create either: a force-mounted
                  item never registers in cmdk's store, so it does not count
                  towards the filtered total and this would otherwise show
                  above the very "Create X" row that contradicts it. */}
              {!typed && <CommandEmpty>No company matches.</CommandEmpty>}
              {/* The ones already linked stay in the list, marked. Filtering
                  them out leaves typing their name showing nothing at all —
                  no match, and no "Create" row either, since the name exists. */}
              {options.map((company) => (
                <CommandItem
                  key={company.id}
                  value={`${company.name} ${company.id}`}
                  onSelect={() => add(company)}
                  className="flex items-center gap-2 px-2 py-1.5 text-[13px]"
                >
                  <CompanyAvatar
                    name={company.name}
                    domain={
                      logos ? companyDomain({ name: company.name, website: company.website }) : null
                    }
                    size={18}
                  />
                  <span className="min-w-0 flex-1 truncate">{company.name}</span>
                  {linked.has(company.id) && <span className="text-faint text-[11px]">linked</span>}
                </CommandItem>
              ))}
              {typed && !exists && (
                <CommandItem
                  forceMount
                  value="±new±"
                  onSelect={() => create(typed)}
                  className="flex items-center gap-2 px-2 py-1.5 text-[13px]"
                >
                  <PlusIcon className="size-3.5" />
                  Create “{typed}”
                </CommandItem>
              )}
            </CommandList>
          </Command>
        </PopoverContent>
      </Popover>
    </div>
  );
}