Загрузка данных
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}
/>
);
};