Ctrl K
ComunidadGuías…

src/components/dives/cylinder-combobox.tsx

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

Archivo original ↗Preguntar a KiBot
"use client";

import { useCallback, useState } from "react";
import {
  ComboboxSearchResult,
  CreatableCombobox,
} from "@/components/ui/creatable-combobox";
import type { FormControlA11yProps } from "@/components/ui/form";
import { gearAPI, GearItem, GearItemInfo, gearItemLabel } from "@/lib/api/gear";
import { GearItemDialog } from "@/components/gear/gear-item-dialog";

// How many cylinders the dropdown asks for at a time - see `GearItemMultiSelect`.
const CYLINDERS_PER_SEARCH = 25;

// The members of a gear item a tank's link carries, off whatever shape it came in.
function asInfo(item: GearItemInfo | GearItem): GearItemInfo {
  return {
    uuid: item.uuid,
    name: item.name,
    brand: item.brand,
    type: item.type,
    rented: item.rented,
    is_archived: item.is_archived,
    volume: item.volume ?? null,
    working_pressure: item.working_pressure ?? null,
    material: item.material ?? null,
  };
}

export interface CylinderComboboxProps extends FormControlA11yProps {
  /** The linked vessel, or `null` - as the read embeds it, so no record is fetched. */
  value: GearItemInfo | null;
  /** The picked vessel, with its facts, or `null` when the link is cleared. */
  onChange: (vessel: GearItemInfo | null) => void;
  /**
   * Vessels another tank on the form already links. Left out of the menu, as the
   * reference pickers leave out what is already picked: one vessel is one gas
   * supply, and the API refuses a dive that links it twice.
   */
  excludeIds: string[];
  /** The dive's start time - see `TripCombobox.until`. */
  until?: string;
}

// Picks which of the diver's cylinders a tank is. The lookup is asked for cylinders
// only, and its rows carry the vessel's facts, so a pick hands the caller the
// capacity to copy without reading the record. A linked vessel since archived or
// retyped is still shown by the name the dive's read carries, since the lookup will
// not list it.
export function CylinderCombobox({
  value,
  onChange,
  excludeIds,
  until,
  ...slotProps
}: CylinderComboboxProps) {
  const [showNewDialog, setShowNewDialog] = useState(false);
  // Every row this picker has been shown, so a pick resolves to the whole item.
  const [known, setKnown] = useState<Record<string, GearItemInfo>>({});

  const searchCylinders = useCallback(
    async (query: string): Promise<ComboboxSearchResult> => {
      const response = await gearAPI.lookupGearItems(1, CYLINDERS_PER_SEARCH, {
        search: query,
        until,
        type: "cylinder",
      });
      setKnown((prev) => ({
        ...prev,
        ...Object.fromEntries(response.data.map((row) => [row.uuid, row])),
      }));
      return {
        items: response.data.map((row) => ({
          id: row.uuid,
          name: gearItemLabel(row),
          // The capacity tells two cylinders of one name apart.
          hint: row.volume != null ? `${row.volume} L` : undefined,
        })),
        hasMore: response.has_more,
      };
    },
    [until],
  );

  const handleChange = (id: string | undefined) => {
    if (!id) {
      onChange(null);
      return;
    }
    if (id === value?.uuid) return;
    const picked = known[id];
    if (picked) onChange(asInfo(picked));
  };

  const handleCreated = (created: GearItem) => {
    // The dialog opens as a cylinder; one the diver retyped there is not this
    // tank's vessel, and the API would refuse the link.
    if (created.type && created.type !== "cylinder") return;
    onChange(asInfo(created));
  };

  return (
    <>
      <CreatableCombobox
        {...slotProps}
        onSearch={searchCylinders}
        excludeIds={excludeIds}
        value={value?.uuid}
        selectedItem={
          value ? { id: value.uuid, name: gearItemLabel(value) } : undefined
        }
        onChange={handleChange}
        placeholder="Not linked to your gear"
        noItemsLabel="No cylinders in your gear yet."
        noMatchesLabel="No cylinders match."
        allExcludedLabel="Every cylinder in your gear is already on another tank."
        addNewLabel="New gear item..."
        onAddNew={() => setShowNewDialog(true)}
      />

      <GearItemDialog
        open={showNewDialog}
        onOpenChange={setShowNewDialog}
        onSaved={handleCreated}
        initialType="cylinder"
      />
    </>
  );
}