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


import { ModelSelect } from '@/features/Settings/selects';
import { LocationFilters } from '@/features/filters/LocationFilters/LocationFilters';
import { AppConsts } from '@/shared';
import {
  Button,
  Column,
  DateTimeField,
  IconButton,
  IpInput,
  Row,
  Select,
  TextField,
  Tooltip,
  Typography,
} from '@/shared/components';
import { SearchSelect } from '@/shared/components/SearchSelect';
import { InputMask } from '@/shared/lib/InputMask';
import {
  ChevronsDownIcon,
  ChevronsUpIcon,
  HelpCircleIcon,
} from '@sber-friend/flamingo-icons';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuditSideFilter } from '../controller';

export const UsersAuditSideFilter = () => {
  const { t } = useTranslation('common');

  const {
    searchParams,
    isEquipmentFiltersOpen,
    refetch,

    auditOptions,
    smartRoomFilter,
    additionalFilters,
    baseFilters,

    handleEquipmentFiltersToggle,
  } = useAuditSideFilter();

  const {
    resetFilters,
    handleChangeTimeFrom,
    handleChange,
    handleChangeTimeMax,
    handleChangeUser,
  } = baseFilters;
  const {
    modelIds,
    macValue,
    ipValue,
    cityId,
    floorId,
    placeId,
    regionId,
    roomId,
    isEquipmentFiltersAvailable,
    equipmentModelType,

    handleChangeModels,
    handleMacChange,
    handleIpChange,
    setRegionId,
    setCityId,
    setPlaceId,
    setFloorId,
    setRoomId,
  } = additionalFilters;

  const sourceOptions = useMemo(() => {
    return [
      { label: t('all'), value: '' },
      { label: t('monitoring'), value: 'smartroom-monitoring' },
      { label: t('authentication'), value: 'smartroom-auth' },
      {
        label: t('notifications:notifications'),
        value: 'smartroom-notifications',
      },
    ];
  }, [searchParams.source]);

  const { smartRoomId, smartRoomIdError, handleSmartRoomIdChange } =
    smartRoomFilter;

  const { isFetchingUsers, userOptions, typeOptions, setSearchUser } =
    auditOptions;
  return (
    <Column minWidth={330} maxWidth={330} gridGap={16}>
      <DateTimeField
        label={t('timeFrom')}
        onChange={handleChangeTimeFrom}
        margin='none'
        value={searchParams.tmMin && searchParams.tmMin * AppConsts.THOUSAND}
      />

      <DateTimeField
        label={t('timeTo')}
        margin='none'
        onChange={handleChangeTimeMax}
        value={searchParams.tmMax && searchParams.tmMax * AppConsts.THOUSAND}
      />

      <Select
        label={t('type')}
        onChange={handleChange('type')}
        options={typeOptions || ''}
        value={searchParams.type}
      />

      <SearchSelect
        label={t('user')}
        placeholder={t('user:placeholders.searchByFullName')}
        loading={isFetchingUsers}
        value={searchParams?.userId || ''}
        setValue={setSearchUser}
        options={userOptions}
        onChangeCallback={handleChangeUser}
      />

      <Select
        key='qwe'
        label={t('source')}
        onChange={handleChange('source')}
        options={sourceOptions || ''}
        value={searchParams.source}
      />

      <TextField
        label='Smartroom ID'
        value={smartRoomId}
        onChange={handleSmartRoomIdChange}
        error={smartRoomIdError}
        helperText={smartRoomIdError ? t('messages.invalidUUIDMessage') : ''}
      />

      {isEquipmentFiltersAvailable && (
        <>
          <Row justifyContent='space-between'>
            <Typography variant='h5'>{t('audit:equipmentParams')}</Typography>

            <IconButton onClick={handleEquipmentFiltersToggle}>
              {isEquipmentFiltersOpen ? (
                <ChevronsUpIcon />
              ) : (
                <ChevronsDownIcon />
              )}
            </IconButton>
          </Row>

          {isEquipmentFiltersOpen && equipmentModelType && (
            <Column gridGap={16}>
              <Row alignItems={'center'}>
                <Typography variant='h5'>{t('location')}</Typography>
                <Tooltip title={t('audit:locationHint')}>
                  <HelpCircleIcon fontSize='small' />
                </Tooltip>
              </Row>
              <LocationFilters
                isRooms
                isRoomRequired
                regionId={regionId}
                setRegionId={setRegionId}
                cityId={cityId}
                setCityId={setCityId}
                placeId={placeId}
                setPlaceId={setPlaceId}
                floorId={floorId}
                setFloorId={setFloorId}
                roomId={roomId}
                setRoomId={setRoomId}
                withDropdown={false}
                onLocationIdsChange={() => {}}
              />

              <Typography variant='h5'>{t('other')}</Typography>
              <ModelSelect
                multiple
                isCreateButton={false}
                type={equipmentModelType}
                value={modelIds}
                onChange={handleChangeModels}
              />

              <TextField
                fullWidth
                label='MAC'
                mask={InputMask.mac}
                value={macValue}
                onChange={handleMacChange}
              />

              <IpInput
                label='ip'
                value={ipValue}
                onChange={handleIpChange}
                error={false}
                helperText={undefined}
              />
            </Column>
          )}
        </>
      )}

      <Column gridGap={8}>
        <Button onClick={refetch} color='primary'>
          {t('buttons.update')}
        </Button>
        <Button onClick={resetFilters} color='error'>
          {t('buttons.reset')}
        </Button>
      </Column>
    </Column>
  );
};



import { useAutocompleteDebounce } from '@/shared/hooks';
import { Helpers } from '@/shared/lib';
import { Autocomplete } from '@components/UI/autocomplete/Autocomplete';
import {
  AdditionalAutocompleteProps,
  DeviceModelOnChange,
} from '@components/types';
import { PlusIcon } from '@sber-friend/flamingo-icons';
import { DeviceModel } from '@shared/api/controller/client';
import { Box, IconButton } from '@shared/components';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { EquipmentModelType } from '../../lib';
import { useGetModels } from '../hooks/useGetModels';
import { useModelSelectActions } from '../hooks/useHelpers';
import { useVirtualization } from '../hooks/useVirtualization';

type ModelSelectValue = string | number | boolean | null;

type CommonProps = Omit<
  AdditionalAutocompleteProps,
  'onChange' | 'value' | 'multiple'
> & {
  onChange: DeviceModelOnChange;
  type: EquipmentModelType;
  isCreateButton?: boolean;
};

type SingleProps = CommonProps & {
  multiple?: false;
  value?: ModelSelectValue;
  initialModel?: DeviceModel | null;
  setModel?: (model: DeviceModel | null) => void;
};

type MultipleProps = Omit<CommonProps, 'onChange'> & {
  multiple: true;
  value?: ModelSelectValue[];
  onChange: (value: ModelSelectValue[]) => void;
  initialModel?: DeviceModel | DeviceModel[] | null;
  setModel?: (model: DeviceModel[]) => void;
};

type Props = SingleProps | MultipleProps;

export const ModelSelect = ({
  value,
  onChange,
  error,
  helperText,
  required,
  type,
  initialModel,
  multiple,
  isCreateButton = true,
  setModel,
  onKeyDown,
  handleClearButtonClick,
  ...rest
}: Props) => {
  const { t } = useTranslation('common');

  const [page, setPage] = useState(0);
  const initialInputValue = Array.isArray(initialModel)
    ? undefined
    : initialModel?.name;
  const {
    inputValue,
    setInputValue,
    debouncedValue,
    isDebounceLoading,
    onInputChange,
  } = useAutocompleteDebounce(initialInputValue);

  const { models, total, status, isDataFetching } = useGetModels({
    debouncedValue,
    type,
    page,
  });

  const { allItems, VirtualizedListbox, options } = useVirtualization({
    initialModel,
    models,
    total,
    status,
    page,
    setPage,
  });

  const {
    handleChange,
    handleOpenCreateModalDrawer,
    handleInputChange,
    handleBackSpace,
  } = useModelSelectActions({
    allItems,
    type,
    setModel,
    onChange,
    multiple,
    onInputChange,
    handleClearButtonClick,
    setPage,
    setInputValue,
  });
  const info = Helpers.isEmpty(allItems) ? t('messages.addModel') : helperText;
  console.log(inputValue);
  return (
    <Box display='flex' gridGap='8px'>
      <Autocomplete
        loading={isDataFetching || isDebounceLoading}
        margin='none'
        label={t('controller:model')}
        options={options}
        value={value}
        multiple={multiple}
        onChange={handleChange}
        inputValue={inputValue}
        error={error}
        helperText={info}
        required={required}
        disabled={!options.length}
        ListboxComponent={VirtualizedListbox}
        onKeyDown={handleBackSpace}
        handleClearButtonClick={() => {
          handleClearButtonClick?.();
          setInputValue('');
        }}
        onInputChange={handleInputChange}
        //@ts-ignore
        data-testId={rest['data-testid']}
        {...rest}
      />

      {isCreateButton && (
        <Box marginTop='20px'>
          <IconButton
            variant='square'
            contained
            onClick={handleOpenCreateModalDrawer}
          >
            <PlusIcon />
          </IconButton>
        </Box>
      )}
    </Box>
  );
};


import { DispatchStateAction } from '@/components/types';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';

export type AutocompleteDebounceReturn = {
  inputValue: string;
  setInputValue: DispatchStateAction<string>;
  debouncedValue: string;
  isDebounceLoading: boolean;
  onInputChange: (value: string) => void;
  resetDebounce: (value?: string) => void;
};

const AUTOCOMPLETE_DEBOUNCE_STATE = {
  debouncedValue: '',
  isDebounceLoading: false,
  inputValue: '',
  hasUserInteracted: false,
  debounceDuration: 1000,
} as const;

/**
 * Хук для управления состоянием автокомплита с дебаунсом.
 * Позволяет отложить обновление основного значения до завершения ввода пользователя.
 *
 * @example
 * ```tsx
 * const MyComponent = () => {
 *   const {
 *     inputValue,
 *     debouncedValue,
 *     isDebounceLoading,
 *     onInputChange
 *   } = useAutocompleteDebounce('initial value');
 *
 *   useEffect(() => {
 *     // Вызывается только через 1 секунду после остановки ввода
 *     if (debouncedValue) {
 *       fetchSearchResults(debouncedValue);
 *     }
 *   }, [debouncedValue]);
 *
 *   return (
 *     <div>
 *       <input
 *         type="text"
 *         value={inputValue}
 *         onChange={onInputChange}
 *         placeholder="Поиск..."
 *       />
 *       {isDebounceLoading && <Spinner />}
 *     </div>
 *   );
 * };
 * ```
 *
 * @param initialInputValue - Начальное значение для поля ввода (опционально)
 * @returns Объект с состоянием и обработчиками
 */
export const useAutocompleteDebounce = (
  initialInputValue?: string,
): AutocompleteDebounceReturn => {
  const [debouncedValue, setDebouncedValue] = useState<string>(
    AUTOCOMPLETE_DEBOUNCE_STATE.debouncedValue,
  );

  const [isDebounceLoading, setIsDebounceLoading] = useState<boolean>(
    AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading,
  );

  const [inputValue, setInputValue] = useState<string>(
    AUTOCOMPLETE_DEBOUNCE_STATE.inputValue,
  );

  const hasUserInteracted = useRef<boolean>(
    AUTOCOMPLETE_DEBOUNCE_STATE.hasUserInteracted,
  );

  const debounce = useDebouncedCallback((value: string) => {
    setDebouncedValue(value);
    setIsDebounceLoading(AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading);
  }, AUTOCOMPLETE_DEBOUNCE_STATE.debounceDuration);

  const onInputChange = useCallback(
    (value: string) => {
      console.log(value);
      setInputValue(value);
      hasUserInteracted.current = true;
      debounce(value);
      setIsDebounceLoading(true);
    },
    [debounce],
  );

  const resetDebounce = useCallback(
    (value = '') => {
      debounce.cancel();
      setInputValue(value);
      setDebouncedValue(value);
      setIsDebounceLoading(AUTOCOMPLETE_DEBOUNCE_STATE.isDebounceLoading);
    },
    [debounce],
  );

  useEffect(() => {
    if (initialInputValue !== undefined && !hasUserInteracted.current) {
      setInputValue(initialInputValue);
    }
  }, [initialInputValue]);

  return {
    inputValue,
    setInputValue,
    debouncedValue,
    isDebounceLoading,
    onInputChange,
    resetDebounce,
  };
};


import {
  AdditionalAutocompleteProps,
  AutocompleteOption,
} from '@/components/types';
import { AutocompleteRenderInputParams } from '@material-ui/lab';
import { Autocomplete as FlamingoAutocomplete } from '@sber-friend/flamingo-core';
import { TextField, TextFieldProps } from '@shared/components';
import { KeyboardEventHandler, memo, useCallback, useMemo } from 'react';
import { useAutocompleteController } from './controllers/useAutocompleteController/useAutocompleteController';
type AutocompleteValue = string | number | boolean | null;

export type AutocompleteProps = {
  options: AutocompleteOption[];
  error?: boolean | undefined;
  value?: AutocompleteValue | AutocompleteValue[];
  onChange: (value: AutocompleteValue | AutocompleteValue[]) => void;
  label?: string;
  disabled?: boolean;
  onKeyDown?: KeyboardEventHandler<HTMLDivElement>;
} & Omit<AdditionalAutocompleteProps, 'onChange' | 'value'> & {
    margin?: TextFieldProps['margin'];
    'data-fui-tid'?: TextFieldProps['data-fui-tid'];
    'data-testid'?: string;
  };

export const Autocomplete = memo(
  ({
    options,
    onChange,
    error,
    helperText,
    required,
    value,
    label,
    disabled,
    placeholder,
    styleVariant,
    onInputChange,
    inputValue,
    loading,
    margin,
    popupIcon,
    onReset,
    onClose,
    open,
    onOpen,
    renderOption,
    size,
    onKeyDown,
    ListboxComponent,
    handleClearButtonClick,
    multiple,
    ...rest
  }: AutocompleteProps) => {
    const handleRenderInput = useCallback(
      (params: AutocompleteRenderInputParams) => (
        <TextField
          margin={margin ?? 'none'}
          error={error}
          required={required}
          helperText={helperText}
          label={label}
          placeholder={placeholder}
          {...params}
          inputProps={{
            ...params.inputProps,
            'data-testid': rest['data-testid']
              ? `${rest['data-testid']}-input`
              : undefined,
          }}
        />
      ),
      [margin, error, required, helperText, label, placeholder, rest],
    );

    const { getOptionLabel, getOptionSelected, getAutocompleteValue } =
      useAutocompleteController();

    const autocompleteValue = useMemo(() => {
      if (multiple) {
        const values = Array.isArray(value) ? value : [];

        return options.filter((option) => values.includes(option.value));
      }

      return getAutocompleteValue(options, value as string);
    }, [getAutocompleteValue, multiple, options, value]);

    const handleReset = useCallback(() => {
      onChange(multiple ? [] : '');
    }, [multiple, onChange]);

    const handleChange = useCallback<
      (
        event: unknown,
        value: AutocompleteOption | AutocompleteOption[] | null,
      ) => void
    >(
      (_, newValue) => {
        if (Array.isArray(newValue)) {
          onChange(newValue.map((option) => option.value));
          return;
        }

        onChange(newValue ? newValue.value : '');
      },
      [onChange],
    );

    return (
      <FlamingoAutocomplete
        open={open}
        styleVariant={styleVariant}
        disabled={disabled}
        value={autocompleteValue}
        multiple={multiple}
        getOptionSelected={getOptionSelected}
        fullWidth
        getOptionLabel={getOptionLabel}
        onChange={handleChange}
        onInputChange={onInputChange}
        options={options}
        renderInput={handleRenderInput}
        onReset={onReset ?? handleReset}
        inputValue={inputValue}
        loading={loading}
        customPopupIcon={!!popupIcon}
        popupIcon={popupIcon}
        onClose={onClose}
        onOpen={onOpen}
        renderOption={renderOption}
        size={size}
        onKeyDown={onKeyDown}
        ListboxComponent={ListboxComponent}
        handleClearButtonClick={handleClearButtonClick}
        // @ts-ignore
        data-testid={rest['data-testid']}
        data-fui-tid={rest['data-fui-tid']}
      />
    );
  },
);