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


import { WithLocationFilter } from '@/app/router/queries';
import { type AuditRouteSearch } from '@/app/router/routes/audit';
import { useCallback } from 'react';

type Params = {
  searchParams: AuditRouteSearch;
  updateParamAndResetPage: (paramName: string, value: unknown) => void;
};

export const useAuditLocationFilters = ({
  searchParams,
  updateParamAndResetPage,
}: Params) => {
  const setLocationStringArray = useCallback(
    (paramName: string, value: string[]) => {
      updateParamAndResetPage(paramName, value);
    },
    [updateParamAndResetPage],
  );

  const setRegionId = useCallback(
    (value: string[]) =>
      setLocationStringArray(
        WithLocationFilter.WITH_LOCATION_FILTER_QUERY.regionId,
        value,
      ),
    [setLocationStringArray],
  );

  const setCityId = useCallback(
    (value: string[]) =>
      setLocationStringArray(
        WithLocationFilter.WITH_LOCATION_FILTER_QUERY.cityId,
        value,
      ),
    [setLocationStringArray],
  );

  const setPlaceId = useCallback(
    (value: string[]) =>
      setLocationStringArray(
        WithLocationFilter.WITH_LOCATION_FILTER_QUERY.placeId,
        value,
      ),
    [setLocationStringArray],
  );

  const setFloorId = useCallback(
    (value: string[]) =>
      setLocationStringArray(
        WithLocationFilter.WITH_LOCATION_FILTER_QUERY.floorId,
        value,
      ),
    [setLocationStringArray],
  );

  const setRoomId = useCallback(
    (value: string[]) =>
      setLocationStringArray(
        WithLocationFilter.WITH_LOCATION_FILTER_QUERY.roomId,
        value,
      ),
    [setLocationStringArray],
  );

  const changeLocationIds = useCallback(
    (value: string[]) => {
      updateParamAndResetPage('locationIds', value);
    },
    [updateParamAndResetPage],
  );

  return {
    locationIds: searchParams.locationIds,
    cityId: searchParams.cityId,
    floorId: searchParams.floorId,
    placeId: searchParams.placeId,
    regionId: searchParams.regionId,
    roomId: searchParams.roomId,
    setRegionId,
    setCityId,
    setPlaceId,
    setFloorId,
    setRoomId,
    changeLocationIds,
  };
};

import { auditRoute } from '@/app/router/routes/audit';
import { AppConsts } from '@/shared';
import { useGetApiV1MessageQuery } from '@/shared/api/audit/client';
import { RouterConsts } from '@/shared/consts/RouterConsts';
import { ChangeEvent, useCallback, useState } from 'react';
import { useAuditLocationFilters } from './useAuditLocationFilters';
import { useAuditOptions } from './useAuditOptions';
import { useAuditSearchUpdater } from './useAuditSearchUpdater';
import { useSmartRoomIdFilter } from './useAuditSmartRoomIdFilter';

type ModelSelectValue = string | number | boolean | null;

export const useAuditSideFilter = () => {
  const searchParams = auditRoute.useSearch();

  const { refetch } = useGetApiV1MessageQuery(searchParams);
  const { updateSearchParams, updateParamAndResetPage } =
    useAuditSearchUpdater();

  const [isLocationFiltersOpen, setIsLocationFiltersOpen] = useState(false);

  const auditOptions = useAuditOptions(searchParams);

  const smartRoomFilter = useSmartRoomIdFilter({
    initialValue: searchParams.smartroomId,
    updateParamAndResetPage,
  });

  const locationFilters = useAuditLocationFilters({
    searchParams,
    updateParamAndResetPage,
  });

  const handleChange =
    (paramName: string) =>
    (event: ChangeEvent<{ name?: string | undefined; value: unknown }>) => {
      updateParamAndResetPage(paramName, event.target.value);
    };

  const handleChangeUser = useCallback(
    (value: string) => {
      updateParamAndResetPage('userId', value);
    },
    [updateParamAndResetPage],
  );

  const handleChangeTimeFrom = useCallback(
    (time: Date) => {
      updateParamAndResetPage(
        'tmMin',
        new Date(time).getTime() / AppConsts.THOUSAND,
      );
    },
    [updateParamAndResetPage],
  );

  const handleChangeTimeMax = useCallback(
    (time: Date) => {
      updateParamAndResetPage(
        'tmMax',
        new Date(time).getTime() / AppConsts.THOUSAND,
      );
    },
    [updateParamAndResetPage],
  );

  const handleMacChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      updateParamAndResetPage('mac', event.target.value || undefined);
    },
    [updateParamAndResetPage],
  );

  const handleIpChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      updateParamAndResetPage('ip', event.target.value || undefined);
    },
    [updateParamAndResetPage],
  );

  const handleChangeModels = useCallback(
    (value: ModelSelectValue[]) => {
      updateParamAndResetPage('modelIds', value.map(String));
    },
    [updateParamAndResetPage],
  );

  const handleLocationFiltersToggle = useCallback(() => {
    setIsLocationFiltersOpen((prev) => !prev);
  }, []);

  const resetFilters = useCallback(() => {
    updateSearchParams({
      queryObject: { ...RouterConsts.AUDIT_DEFAULT_QUERY },
    });
    smartRoomFilter.resetSmartRoomId();
  }, [smartRoomFilter, updateSearchParams]);

  const refetchQuery = useCallback(() => {
    refetch();
  }, [refetch]);

  return {
    searchParams,
    isLocationFiltersOpen,

    ...auditOptions,
    ...smartRoomFilter,
    ...locationFilters,

    modelIds: searchParams.modelIds,

    refetchQuery,
    resetFilters,
    handleChange,
    handleChangeUser,
    handleChangeTimeFrom,
    handleChangeTimeMax,
    handleMacChange,
    handleIpChange,
    handleLocationFiltersToggle,
    handleChangeModels,
  };
};


import { LocationFilters } from '@/features/filters/LocationFilters/LocationFilters';

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

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

  const {
    searchParams,
    isFetchingUsers,
    userOptions,
    typeOptions,
    smartRoomId,
    smartRoomIdError,
    modelIds,
    cityId,
    floorId,
    placeId,
    regionId,
    roomId,
    locationIds,
    isLocationFiltersOpen,
    setSearchUser,
    resetFilters,
    refetchQuery,
    handleChangeTimeFrom,
    handleChange,
    handleChangeTimeMax,
    handleChangeUser,
    handleSmartRoomIdChange,
    handleLocationFiltersToggle,
    handleChangeModels,
    setRegionId,
    setCityId,
    setPlaceId,
    setFloorId,
    setRoomId,
    changeLocationIds,
  } = useAuditSideFilter();

  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}
      />

      <ModelSelect
        multiple
        isCreateButton={false}
        type={EquipmentModel.device}
        value={modelIds}
        onChange={handleChangeModels}
      />

      <Select
        label={t('source')}
        onChange={handleChange('source')}
        value={searchParams.source}
        options={[
          { label: t('all'), value: undefined },
          { label: t('monitoring'), value: 'smartroom-monitoring' },
          { label: t('authentication'), value: 'smartroom-auth' },
          {
            label: t('notifications:notifications'),
            value: 'smartroom-notifications',
          },
        ]}
      />

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

      <Row justifyContent='space-between'>
        <Typography variant='h4'>Поиск локации</Typography>
        <IconButton onClick={handleLocationFiltersToggle}>
          {isLocationFiltersOpen ? <ChevronsUpIcon /> : <ChevronsDownIcon />}
        </IconButton>
      </Row>

      {isLocationFiltersOpen && (
        <Column gridGap={16}>
          <LocationFilters
            isRooms
            outputIdType='id'
            regionId={regionId}
            setRegionId={setRegionId}
            cityId={cityId}
            setCityId={setCityId}
            placeId={placeId}
            setPlaceId={setPlaceId}
            floorId={floorId}
            setFloorId={setFloorId}
            roomId={roomId}
            setRoomId={setRoomId}
            ids={locationIds || []}
            withDropdown={false}
            onLocationIdsChange={changeLocationIds}
          />
        </Column>
      )}

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


import { SideFilterSelect } from '@/shared/components/SideFilterSelect/SideFilterSelect';
import { Helpers } from '@/shared/lib';
import { CheckIcon, SortIcon } from '@sber-friend/flamingo-icons';
import { Badge, Box, Button, Dropdown } from '@shared/components';
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

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

type CommonLocationFiltersProps = {
  'data-fui-tid'?: string;
  withDropdown?: boolean;
  isRooms?: boolean;

  regionId: string[];
  cityId: string[];
  placeId: string[];
  floorId: string[];
  roomId: string[];

  setRegionId: (regionId: string[]) => void;
  setCityId: (cityId: string[]) => void;
  setPlaceId: (placeId: string[]) => void;
  setFloorId: (floorId: string[]) => void;
  setRoomId: (roomId: string[]) => void;
};

type TreeIdLocationFiltersProps = CommonLocationFiltersProps & {
  outputIdType?: 'treeId';
  ids?: number[];
  onLocationIdsChange: (locationIds: number[]) => void;
};

type IdLocationFiltersProps = CommonLocationFiltersProps & {
  outputIdType: 'id';
  ids?: string[];
  onLocationIdsChange: (locationIds: string[]) => void;
};

type LocationFiltersProps = TreeIdLocationFiltersProps | IdLocationFiltersProps;

const getDeepestTreeIds = ({
  regionId,
  cityId,
  placeId,
  floorId,
  roomId,
}: Pick<
  CommonLocationFiltersProps,
  'regionId' | 'cityId' | 'placeId' | 'floorId' | 'roomId'
>) => {
  if (roomId?.length > 0) {
    return roomId;
  }

  if (floorId?.length > 0) {
    return floorId;
  }

  if (placeId?.length > 0) {
    return placeId;
  }

  if (cityId?.length > 0) {
    return cityId;
  }

  return regionId;
};

export const LocationFilters = ({
  'data-fui-tid': tid,
  isRooms = false,
  ids,
  regionId,
  cityId,
  placeId,
  floorId,
  roomId,
  setRegionId,
  setCityId,
  setFloorId,
  setPlaceId,
  setRoomId,
  onLocationIdsChange,
  withDropdown = true,
  outputIdType = 'treeId',
}: LocationFiltersProps) => {
  const { t } = useTranslation('locations');
  const [anchorElement, setAnchorElement] = useState(null);

  useEffect(() => {
    if (ids?.length === 0) {
      setRegionId([]);
      setCityId([]);
      setPlaceId([]);
      setFloorId([]);
      setRoomId([]);
    }
  }, [ids, setCityId, setFloorId, setPlaceId, setRegionId, setRoomId]);

  const locationTreeIds = getDeepestTreeIds({
    regionId,
    cityId,
    placeId,
    floorId,
    roomId,
  });

  useEffect(() => {
    if (outputIdType === 'treeId') {
      (onLocationIdsChange as (locationIds: number[]) => void)(
        Helpers.convertStringsToNumbers(locationTreeIds),
      );
    }
  }, [locationTreeIds, onLocationIdsChange, outputIdType]);

  const handleSelectedLocationIdsChange = useCallback(
    (locationIds: string[]) => {
      if (outputIdType === 'id') {
        (onLocationIdsChange as (locationIds: string[]) => void)(locationIds);
      }
    },
    [onLocationIdsChange, outputIdType],
  );

  const handleChangeRegion = (event: LocationEvent) => {
    const { value } = event.target;

    setRegionId(value as string[]);
    setCityId([]);
    setPlaceId([]);
    setFloorId([]);
    setRoomId([]);
  };

  const handleChangeCity = (event: LocationEvent) => {
    const { value } = event.target;

    setCityId(value as string[]);
    setPlaceId([]);
    setFloorId([]);
    setRoomId([]);
  };

  const handleChangePlace = (event: LocationEvent) => {
    const { value } = event.target;

    setPlaceId(value as string[]);
    setFloorId([]);
    setRoomId([]);
  };

  const handleChangeFloor = (event: LocationEvent) => {
    const { value } = event.target;

    setFloorId(value as string[]);
    setRoomId([]);
  };

  const handleChangeRoom = (event: LocationEvent) => {
    const { value } = event.target;

    setRoomId(value as string[]);
  };

  const handleReset = () => {
    setRegionId([]);
    setCityId([]);
    setPlaceId([]);
    setFloorId([]);
    setRoomId([]);

    if (outputIdType === 'id') {
      (onLocationIdsChange as (locationIds: string[]) => void)([]);
      return;
    }

    (onLocationIdsChange as (locationIds: number[]) => void)([]);
  };

  const handleOpen = (event: any) => {
    setAnchorElement(event.currentTarget);
  };

  const handleClose = (event: any) => {
    event.preventDefault();
    setAnchorElement(null);
  };

  const filtersContent = (
    <>
      <SideFilterSelect
        value={regionId}
        label={t('region')}
        onChange={handleChangeRegion}
        onSelectedLocationIdsChange={handleSelectedLocationIdsChange}
        multiple
        isRoot
      />
      <SideFilterSelect
        value={cityId}
        parentIds={regionId}
        label={t('city')}
        onChange={handleChangeCity}
        onSelectedLocationIdsChange={handleSelectedLocationIdsChange}
        multiple
      />
      <SideFilterSelect
        value={placeId}
        parentIds={cityId}
        label={t('place')}
        onChange={handleChangePlace}
        onSelectedLocationIdsChange={handleSelectedLocationIdsChange}
        multiple
      />
      <SideFilterSelect
        value={floorId}
        parentIds={placeId}
        label={t('floor')}
        onChange={handleChangeFloor}
        onSelectedLocationIdsChange={handleSelectedLocationIdsChange}
        multiple
        isFloor
      />
      {isRooms && (
        <SideFilterSelect
          value={roomId}
          parentIds={floorId}
          label={t('room')}
          onChange={handleChangeRoom}
          onSelectedLocationIdsChange={handleSelectedLocationIdsChange}
          multiple
        />
      )}
      <Button size='small' onClick={handleReset}>
        {t('common:buttons.reset')}
      </Button>
    </>
  );

  if (!withDropdown) {
    return filtersContent;
  }

  return (
    <Badge
      invisible={
        !regionId?.length &&
        !cityId?.length &&
        !placeId?.length &&
        !floorId?.length &&
        !roomId?.length
      }
      badgeContent={<CheckIcon />}
      color='primary'
    >
      <Dropdown
        data-fui-tid={tid}
        size='small'
        icon={<SortIcon />}
        onClickButton={handleOpen}
        onClosePopover={handleClose}
        zIndexMenu={100}
        anchorElement={anchorElement}
      >
        <Box
          display='flex'
          gridGap={8}
          flexDirection='column'
          p={2}
          minWidth={330}
          maxWidth={330}
        >
          {filtersContent}
        </Box>
      </Dropdown>
    </Badge>
  );
};


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, useState } from 'react';

export type SideFilterSelectProps = {
  value: string[] | string;
  onChange: NonNullable<SelectProps['onChange']>;
  parentIds?: string[] | string;
  isRoot?: boolean;
  isFloor?: boolean;
  onSelectedLocationIdsChange?: (locationIds: string[]) => void;
} & 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 getSelectedLocationIds = (
  locations: Location[],
  selectedTreeIds: string[],
): string[] => {
  const selectedTreeIdSet = new Set(selectedTreeIds);

  return locations
    .filter((option) => selectedTreeIdSet.has(String(option.treeId)))
    .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,
  isFloor = false,
  onSelectedLocationIdsChange,
  ...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);

    onChange(...args);
    onSelectedLocationIdsChange?.(
      getSelectedLocationIds(location, selectedTreeIds),
    );
  };

  const innerOption = location?.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)}
      {...rest}
    />
  );
};