Загрузка данных
import { AppConsts } from '@/shared';
import { PAGINATION_LIMIT_50 } from '@/shared/consts/constants';
import { useComponentHeight } from '@/shared/hooks/useComponentHeight';
import { rootRoute } from '@app/router/routes/root/root';
import { styled } from '@mui/material';
import {
ArchiveIcon,
PencilIcon,
SidebarIcon,
SortIcon,
} from '@sber-friend/flamingo-icons';
import {
useArchiveLocationTicketMutation,
useGetLocationByTreeIdQuery,
useGetLocationTicketsQuery,
} from '@shared/api/location/client';
import {
Box,
Button,
ColumnsProps,
Dropdown,
IconButton,
Loader,
Row,
Search,
Table,
Tooltip,
Typography,
} from '@shared/components';
import { useUpdateSearchParams } from '@shared/hooks';
import { useSetDrawerType } from '@shared/hooks/drawer/useSetDrawerType';
import {
EventActionTypes,
useSendClickStreamEvent,
} from '@shared/hooks/useSendClickStreamEvent';
import { DrawerTypes } from '@shared/model/drawer/drawer.types';
import dayjs from 'dayjs';
import { useEffect, type MouseEvent } from 'react';
import { useTranslation } from 'react-i18next';
const LocationTicketsTableStyled = styled(Table)`
&::-webkit-scrollbar {
height: 12px !important;
}
`;
type Props = {
withCreateTicketButton?: boolean;
};
export const LocationTickets = ({ withCreateTicketButton = true }: Props) => {
const { t } = useTranslation('common');
const { sendEvent } = useSendClickStreamEvent(
EventActionTypes.archiveLocationTicket,
);
const { detailedLocationId, ticketSearch, ticketSortType, ticketSortColumn } =
rootRoute.useSearch<any>();
const updateSearchParams = useUpdateSearchParams();
const openDrawer = useSetDrawerType();
const { data: locationData } = useGetLocationByTreeIdQuery(
{
treeId: Number(detailedLocationId),
},
{ skip: !detailedLocationId },
);
const locationId = locationData?.id ?? '';
const {
data,
isLoading: isTicketsLoading,
isSuccess: isTicketsSuccess,
} = useGetLocationTicketsQuery(
{
locationId: locationId,
search: ticketSearch,
sortColumn: ticketSortColumn ?? 'updated_at',
sortType: ticketSortType ?? 'desc',
},
{ skip: !locationId },
);
const [archiveTicket, { isLoading, isSuccess }] =
useArchiveLocationTicketMutation();
useEffect(() => {
if (isSuccess) {
sendEvent();
}
}, [isSuccess]);
const { ref, height: tableHeight } = useComponentHeight({
deps: [isTicketsSuccess],
});
if (isTicketsLoading) {
return <Loader isLoading={isTicketsLoading} size='large' />;
}
const items = [
{
id: 'desc',
title: t('sortBy.desc'),
value: 'desc',
},
{
id: 'asc',
title: t('sortBy.asc'),
value: 'asc',
},
];
const handleCloseMenu = (
event: MouseEvent<HTMLElement>,
sortColumn: 'number' | 'archived' | 'deadline',
) => {
updateSearchParams({
queryObject: {
ticketSortColumn: sortColumn,
// @ts-ignore
ticketSortType: event.target.dataset!.value,
},
});
};
const columns: ColumnsProps[] = [
{
headerName: t('number'),
field: 'number',
width: 200,
minWidth: 200,
icon: (
<Dropdown
variant='text'
size='smaller'
disableSelection
MenuItems={items}
id='demo-dropdown-31'
placement='bottom-end'
onCloseMenuItem={(e) => handleCloseMenu(e, 'number')}
icon={
<IconButton size='smaller' aria-label='demo-sort' variant='square'>
<SortIcon />
</IconButton>
}
/>
),
},
{
headerName: t('archived'),
field: 'archived',
width: 125,
minWidth: 125,
icon: (
<Dropdown
variant='text'
size='smaller'
disableSelection
MenuItems={items}
placement='bottom-end'
onCloseMenuItem={(e) => handleCloseMenu(e, 'archived')}
icon={
<IconButton size='smaller' aria-label='demo-sort' variant='square'>
<SortIcon />
</IconButton>
}
/>
),
},
{
headerName: t('createdAt'),
field: 'registredAt',
width: 160,
minWidth: 160,
},
{
headerName: t('ticket:deadline'),
field: 'deadline',
width: 160,
minWidth: 160,
icon: (
<Dropdown
variant='text'
size='smaller'
disableSelection
MenuItems={items}
id='demo-dropdown-31'
placement='bottom-end'
onCloseMenuItem={(e) => handleCloseMenu(e, 'deadline')}
icon={
<IconButton size='smaller' aria-label='demo-sort' variant='square'>
<SortIcon />
</IconButton>
}
/>
),
},
{ headerName: t('actions'), field: 'actions', width: 160, minWidth: 160 },
];
const rows = data?.tickets?.map((ticket) => {
console.log(ticket.registeredAt);
return {
...ticket,
id: ticket.id ?? '',
registredAt: dayjs(ticket.registeredAt).format(
AppConsts.Date.format.standard,
),
deadline: dayjs(ticket.deadline).format(AppConsts.Date.format.standard),
archived: ticket.archived ? t('yes') : t('no'),
actions: (
<Row>
<Tooltip title={t('ticket:ticketInfo')}>
<IconButton
color='primary'
loading={isLoading}
onClick={() => {
if (ticket.id) {
openDrawer(DrawerTypes.ticketInfo);
updateSearchParams({
queryObject: {
[AppConsts.SEARCH_PARAMS_NAME.TICKET_ID]: ticket?.id,
},
});
}
}}
>
<SidebarIcon />
</IconButton>
</Tooltip>
<Tooltip title={t('archive')}>
<IconButton
disabled={ticket.archived}
color='primary'
loading={isLoading}
onClick={() => {
if (ticket.id) {
archiveTicket({ ticketId: ticket.id });
}
}}
>
<ArchiveIcon />
</IconButton>
</Tooltip>
<Tooltip title={t('ticket:editTicket')}>
<IconButton
disabled={ticket.archived}
color='primary'
onClick={() => {
updateSearchParams({
paramName: AppConsts.SEARCH_PARAMS_NAME.TICKET_ID,
value: ticket.id,
});
openDrawer(DrawerTypes.ticketUpdate);
}}
>
<PencilIcon />
</IconButton>
</Tooltip>
</Row>
),
};
});
const handleSearchTicket = (value: string) =>
updateSearchParams({
paramName: AppConsts.SEARCH_PARAMS_NAME.TICKET_SEARCH,
value,
});
const hasActiveTicket = data?.tickets?.some((ticket) => !ticket.archived);
const handleOpenCreateTicketDrawer = () => {
openDrawer(DrawerTypes.ticketCreate);
};
return (
<>
<Search
placeholder={t('placeholders.search')}
setValue={handleSearchTicket}
value={ticketSearch}
debounce={1000}
margin='none'
/>
{withCreateTicketButton && (
<Button
disabled={hasActiveTicket}
onClick={handleOpenCreateTicketDrawer}
>
{hasActiveTicket
? t('ticket:messages.ticketExists')
: t('ticket:createTicket')}
</Button>
)}
{isTicketsSuccess && !!data.tickets?.length && (
<Box ref={ref} style={{ flex: 'auto', overflow: 'auto' }}>
<LocationTicketsTableStyled
rows={rows}
columns={columns}
stickyHeader
maxHeight={tableHeight}
paginationDefaultValue={PAGINATION_LIMIT_50}
/>
</Box>
)}
{isTicketsSuccess && !data.tickets?.length && (
<Typography variant='h6'>{t('ticket:messages.noTickets')}</Typography>
)}
</>
);
};
export class AppConsts {
public static Date = {
format: { Rfc3339: 'YYYY-MM-DDTHH:mm:ss[Z]', standard: 'DD-MM-YYYY HH:mm' },
};
public static readonly NOTIFICATIONS_ENABLED_LS_KEY = 'notifications_enabled';
public static readonly MONITORING_DASHBOARDS_OPEN_LS_KEY =
'monitoring_dashboards_open';
public static readonly MONITORING_EQUIPMENT_TABLE_COLUMNS_LS_KEY =
'monitoring_equipment_table_columns';
public static readonly EQUIPMENT_TABLE_COLUMNS_LS_KEY =
'equipment_table_columns';
private static readonly _DEFAULT_MONITORING_EQUIPMENT_TABLE_COLUMNS: string[] =
[
'type',
'name',
'status',
'segment',
'equipmentModelName',
'equipmentTypeName',
'manufacturerTypeName',
'gveState',
'tcpState',
'icmpState',
'hardwareId',
'interfaceType',
'ip',
'mac',
];
static get DEFAULT_MONITORING_EQUIPMENT_TABLE_COLUMNS(): string[] {
return this._DEFAULT_MONITORING_EQUIPMENT_TABLE_COLUMNS;
}
private static readonly _DEFAULT_EQUIPMENT_TABLE_COLUMNS: string[] = [
'type',
'name',
'pathName',
'ip',
'mac',
'segment',
'equipmentModelName',
'equipmentTypeName',
'manufacturerTypeName',
'interfaceType',
'pingStatus',
'powerState',
'status',
'gveState',
'tcpState',
'icmpState',
];
static get DEFAULT_EQUIPMENT_TABLE_COLUMNS(): string[] {
return this._DEFAULT_EQUIPMENT_TABLE_COLUMNS;
}
public static readonly LOCATION_TABLE_LIMIT_LS_KEY = 'location_table_limit';
public static readonly MONITORING_LOCATION_TABLE_LIMIT_LS_KEY =
'monitoring_location_table_limit';
public static readonly LOCATION_TABLE_COLUMNS_LS_KEY =
'location_table_columns';
private static readonly _DEFAULT_LOCATION_TABLE_COLUMNS: string[] = [
'name',
'pathName',
'roomState',
'equipmentsNumber',
'serviceStatus',
'status',
'ticket',
];
static get DEFAULT_LOCATION_TABLE_COLUMNS(): string[] {
return this._DEFAULT_LOCATION_TABLE_COLUMNS;
}
public static readonly LOCATION_PAGE_TABLE_COLUMNS_LS_KEY =
'location_page_table_columns';
private static readonly _DEFAULT_LOCATION_PAGE_TABLE_COLUMNS: string[] = [
'name',
'pathName',
'equipmentsNumber',
'serviceStatus',
'status',
];
static get DEFAULT_LOCATION_PAGE_TABLE_COLUMNS(): string[] {
return this._DEFAULT_LOCATION_PAGE_TABLE_COLUMNS;
}
public static readonly MONITORING_DEVICE_FIELDS_LS_KEY =
'monitoring_device_fields';
private static readonly _DEFAULT_MONITORING_DEVICE_FIELDS: string[] = [
'name',
'typeName',
'hardwareId',
'manufacturerName',
'modelName',
'ip',
'interface',
'status',
'powerState',
];
static get DEFAULT_MONITORING_DEVICE_FIELDS(): string[] {
return this._DEFAULT_MONITORING_DEVICE_FIELDS;
}
private static readonly _SEARCH_PARAMS_NAME = {
EDIT_COMMENT: 'editComment',
COMMENT_ID: 'commentId',
DETAILED_LOCATION_ID: 'detailedLocationId',
LOCATION_ID: 'treeId',
IS_ROOM_ID: 'isRoomId',
LIMIT: 'limit',
PAGE: 'page',
SORT_COLUMN: 'sortColumn',
SORT_TYPE: 'sortType',
SEARCH: 'search',
SEGMENT: 'segment',
REGION_ID: 'regionId',
CITY_ID: 'cityId',
PLACE_ID: 'placeId',
FLOOR_ID: 'floorId',
HAS_TICKETS: 'hasTickets',
TICKET_ID: 'ticketId',
TICKET_SEARCH: 'ticketSearch',
};
public static THOUSAND = 1000;
public static START_OF_DAY = new Date().setHours(0, 0, 0, 0) / 1000;
static get SEARCH_PARAMS_NAME() {
return this._SEARCH_PARAMS_NAME;
}
public static readonly _SIP_URI_REGEX =
/^(?<user>[^@;]+)@(?<domain>[^;]+)(?:;(?<params>.*))?$/;
static get SIP_URI_REGEX() {
return this._SIP_URI_REGEX;
}
public static auditMessageTypes: Record<string, string> = {
sign_in: 'Вход',
log_out: 'Выход',
user_register: 'Регистрация пользователя',
user_update: 'Обновление пользователя',
user_delete: 'Удаление пользователя',
manufacturer_create: 'Создание производителя',
manufacturer_update: 'Обновление производителя',
manufacturer_delete: 'Удаление производителя',
controller_create: 'Создание контроллера',
controller_update: 'Обновление контроллера',
controller_delete: 'Удаления контроллера',
controller_model_create: 'Создание модели контроллера',
controller_model_update: 'Обновление модели контроллера',
controller_model_delete: 'Удаление модели контроллера',
controller_type_create: 'Создание типа контроллера',
controller_type_update: 'Обновление типа контроллера',
controller_type_delete: 'Удаление типа контроллера',
device_create: 'Создание устройства',
device_update: 'Обновление устройства',
device_delete: 'Удаление устройства',
device_type_create: 'Создание типа устройства',
device_type_update: 'Обновление типа устройства',
device_type_delete: 'Удаление типа устройства',
device_model_create: 'Создание модели устройства',
device_model_update: 'Обновление модели устройства',
device_model_delete: 'Удаление модели устройства',
location_create: 'Создание локации',
location_update: 'Обновление локации',
location_delete: 'Удаление локации',
location_note_create: 'Создание заметки локации',
location_note_update: 'Обновление заметки локации',
location_note_delete: 'Удаление заметки локации',
location_ticket_create: 'Создание заявки для локации',
location_ticket_update: 'Обновление заявки для локации',
location_ticket_delete: 'Удаление заявки для локации',
};
}
{
"tickets": [
{
"id": "2fc48d7e-13a4-4477-9712-b344253bfe49",
"number": "12321",
"registeredAt": "2026-05-04T08:52:21Z",
"deadline": "2026-05-13T09:13:00Z",
"locationId": "a15b668a-403a-4b66-8eda-f67aec80d3f0",
"archived": true,
"description": "1121",
"contractor": {
"id": "442e09e3-b5d8-4664-af62-58946fc09b93",
"name": "Atm",
"description": ""
}
},
{
"id": "63b7b18a-467b-4cb0-9c9f-b69174c5c118",
"number": "difjui",
"registeredAt": "2026-05-07T11:13:09Z",
"deadline": "2026-05-21T11:20:00Z",
"locationId": "a15b668a-403a-4b66-8eda-f67aec80d3f0",
"archived": true,
"description": "dusfidsjui",
"contractor": null
},
{
"id": "cc157eb6-3c80-43c6-877d-30f2c5a27d77",
"number": "fjsvijsdnvi",
"registeredAt": "2026-05-07T11:22:11Z",
"deadline": "2026-05-14T11:22:00Z",
"locationId": "a15b668a-403a-4b66-8eda-f67aec80d3f0",
"archived": false,
"description": "ufidnidsn",
"contractor": null
}
],
"total": 3
}