Загрузка данных


import {
  Location,
  useGetLocationChildrenQuery,
} from '@/shared/api/location/client';
import { Helpers } from '@/shared/lib';
import {
  useResetLocationFilterSelect,
  useSetLocation,
} from '@entities/LocationFiltersList/lib/hooks';
import { Select, SelectProps } from '@shared/components';
import { ChangeEvent, useMemo, useState } from 'react';

export type SideFilterSelectProps = {
  value: string[] | string;
  onChange: NonNullable<SelectProps['onChange']>;
  parentIds?: string[] | string;
  isRoot?: boolean;
  isFloor?: boolean;
  onSelectedLocationIdsChange?: (locationIds: string[]) => void;

  /**
   * Возвращает полные объекты выбранных локаций.
   * Необязательный prop, существующие вызовы не затрагивает.
   */
  onSelectedLocationsChange?: (locations: Location[]) => void;

  /**
   * Исключает переданные treeId из списка options.
   * Необязательный prop, существующие вызовы не затрагивает.
   */
  excludedTreeIds?: Array<string | number>;

  required?: boolean;
} & Omit<SelectProps, 'onChange' | 'value' | 'options'>;

type LocationEvent = ChangeEvent<{
  name?: string;
  value: unknown;
}>;

const getLocationLabel = (
  option: Location,
  parentIds?: string[] | string,
): string => {
  const hasMultipleParents =
    Array.isArray(parentIds) && parentIds.length > 1;

  if (!hasMultipleParents || !option.pathName) {
    return option.name ?? '';
  }

  return `${option.name}, ${option.pathName}`;
};

const normalizeValue = (value: unknown): string[] => {
  if (Array.isArray(value)) {
    return value.map(String);
  }

  return value ? [String(value)] : [];
};

const getSelectedLocations = (
  locations: Location[],
  selectedTreeIds: string[],
): Location[] => {
  const selectedTreeIdSet = new Set(selectedTreeIds);

  return locations.filter((option) =>
    selectedTreeIdSet.has(String(option.treeId)),
  );
};

const getSelectedLocationIds = (
  locations: Location[],
): string[] =>
  locations
    .map((option) => option.id)
    .filter(
      (id): id is NonNullable<typeof id> =>
        id !== null && id !== undefined,
    )
    .map(String);

export const SideFilterSelect = ({
  value,
  onChange,
  disabled,
  label,
  parentIds,
  isRoot = false,
  required,
  isFloor = false,
  onSelectedLocationIdsChange,
  onSelectedLocationsChange,
  excludedTreeIds = [],
  ...rest
}: SideFilterSelectProps) => {
  const [location, setLocation] = useState<Location[]>([]);

  const requestParams = isRoot
    ? {}
    : {
        parentId:
          typeof parentIds === 'object'
            ? Helpers.convertStringsToNumbers(parentIds)
            : [Number(parentIds)],
      };

  const requestOptions = isRoot
    ? {}
    : {
        skip:
          typeof parentIds === 'object'
            ? !parentIds?.length
            : !parentIds,
      };

  const { data, status } = useGetLocationChildrenQuery(
    requestParams,
    {
      ...requestOptions,
      refetchOnMountOrArgChange: true,
    },
  );

  useSetLocation({
    entity: data,
    status,
    setter: setLocation,
    isFloor,
  });

  useResetLocationFilterSelect({
    setter: setLocation,
    ids: parentIds ?? '',
  });

  const handleChange: NonNullable<
    SelectProps['onChange']
  > = (...args) => {
    const event = args[0] as LocationEvent;

    const selectedTreeIds = normalizeValue(
      event.target.value,
    );

    const selectedLocations = getSelectedLocations(
      location,
      selectedTreeIds,
    );

    onChange(...args);

    onSelectedLocationIdsChange?.(
      getSelectedLocationIds(selectedLocations),
    );

    onSelectedLocationsChange?.(
      selectedLocations,
    );
  };

  const excludedTreeIdSet = useMemo(
    () => new Set(excludedTreeIds.map(String)),
    [excludedTreeIds],
  );

  const innerOption = location
    .filter(
      (option) =>
        !excludedTreeIdSet.has(
          String(option.treeId),
        ),
    )
    .map((option) => ({
      value: String(option.treeId),
      label: getLocationLabel(
        option,
        parentIds,
      ),
    }));

  return (
    <Select
      label={label}
      options={innerOption}
      value={value}
      onChange={handleChange}
      disabled={
        disabled || (!isRoot && !location.length)
      }
      required={required}
      {...rest}
    />
  );
};