fix(ws): маршруты сущностей и локализованные уведомления
Клик по WS-уведомлению ведёт на детальную страницу, тексты через i18n. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+23
-31
@@ -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');
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user