fix(ws): маршруты сущностей и локализованные уведомления

Клик по WS-уведомлению ведёт на детальную страницу, тексты через i18n.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 17:54:25 +03:00
parent 874d9099de
commit bc8ec9aeea
2 changed files with 37 additions and 31 deletions
+23 -31
View File
@@ -24,6 +24,8 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MENU_ITEMS, filterMenuByRole, getMenuSelectedKey, canReceiveWsNotification } from '../config/adminAccess'; import { MENU_ITEMS, filterMenuByRole, getMenuSelectedKey, canReceiveWsNotification } from '../config/adminAccess';
import { getEntityDetailPath } from '../utils/entityRoutes';
import { useTranslation } from 'react-i18next';
const { Header, Sider, Content } = Layout; const { Header, Sider, Content } = Layout;
const { Text } = Typography; const { Text } = Typography;
@@ -35,6 +37,7 @@ const AdminLayout: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user, logout } = useAuthStore(); const { user, logout } = useAuthStore();
const { t } = useTranslation();
const { token: { colorBgContainer } } = theme.useToken(); const { token: { colorBgContainer } } = theme.useToken();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
@@ -49,26 +52,21 @@ const AdminLayout: React.FC = () => {
return; return;
} }
const { report_id, target_type, target_id, reason } = msg.data || {}; const { report_id, target_type, target_id, reason } = msg.data || {};
const targetPath = target_id ? getEntityDetailPath(target_type, target_id) : undefined;
notification.info({ notification.info({
title: 'Новая жалоба', key: report_id ? `report-${report_id}` : `report-${Date.now()}`,
description: ( message: t('ws.newReport'),
<span> description: [
Жалоба{' '} report_id ? `Жалоба #${report_id}` : 'Жалоба',
{report_id ? ( target_type ? `на ${target_type}` : '',
<a href={`/reports/${report_id}`} onClick={(e) => { e.preventDefault(); navigate(`/reports/${report_id}`); }} style={{ fontWeight: 600 }}> target_id ?? '',
#{report_id} reason ? `(${reason})` : '',
</a> ].filter(Boolean).join(' '),
) : ('#?')}{' '}
на {target_type || 'неизвестный тип'}{' '}
{target_id ? (
<a href={`/${target_type}s/${target_id}`} onClick={(e) => { e.preventDefault(); navigate(`/${target_type}s/${target_id}`); }} style={{ fontWeight: 600 }}>
{target_id}
</a>
) : ('?')}{' '}
{reason ? `(${reason})` : ''}
</span>
),
placement: 'topRight', placement: 'topRight',
onClick: () => {
if (report_id) navigate(`/reports/${report_id}`);
else if (targetPath) navigate(targetPath);
},
}); });
} else if (msg.type === 'ticket_created') { } else if (msg.type === 'ticket_created') {
if (!canReceiveWsNotification('ticket_created', user?.role)) { if (!canReceiveWsNotification('ticket_created', user?.role)) {
@@ -76,25 +74,19 @@ const AdminLayout: React.FC = () => {
} }
const { ticket_id } = msg.data || {}; const { ticket_id } = msg.data || {};
notification.info({ notification.info({
title: 'Новый тикет', key: ticket_id ? `ticket-${ticket_id}` : `ticket-${Date.now()}`,
description: ( message: t('ws.newTicket'),
<span> description: ticket_id ? `Тикет #${ticket_id} создан` : 'Создан новый тикет',
Тикет{' '}
{ticket_id ? (
<a href={`/tickets/${ticket_id}`} onClick={(e) => { e.preventDefault(); navigate(`/tickets/${ticket_id}`); }} style={{ fontWeight: 600 }}>
#{ticket_id}
</a>
) : ('без ID')}{' '}
создан
</span>
),
placement: 'topRight', placement: 'topRight',
onClick: () => {
if (ticket_id) navigate(`/tickets/${ticket_id}`);
},
}); });
} }
}; };
window.addEventListener('admin-ws-message', handler as EventListener); window.addEventListener('admin-ws-message', handler as EventListener);
return () => window.removeEventListener('admin-ws-message', handler as EventListener); return () => window.removeEventListener('admin-ws-message', handler as EventListener);
}, [navigate, user?.role]); }, [navigate, user?.role, t]);
const onlineNodes = React.useMemo(() => { const onlineNodes = React.useMemo(() => {
const cutoff = dayjs().subtract(NODE_STATS_THRESHOLD, 'minute'); const cutoff = dayjs().subtract(NODE_STATS_THRESHOLD, 'minute');
+14
View File
@@ -0,0 +1,14 @@
/** Маршруты к сущностям по target_type (жалобы, WS-уведомления). */
const ENTITY_PATH: Record<string, string> = {
event: 'events',
calendar: 'calendars',
review: 'reviews',
user: 'users',
report: 'reports',
ticket: 'tickets',
};
export function getEntityDetailPath(type: string | undefined, id: string): string {
const segment = type ? ENTITY_PATH[type] ?? `${type}s` : 'unknown';
return `/${segment}/${id}`;
}