diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index bc9a053..1bc20f2 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -535,6 +535,15 @@ export const adminUsersApi = { return response.data; }, + // Send direct Telegram message to user via bot (parity with bot's admin action) + sendMessage: async ( + userId: number, + text: string, + ): Promise<{ success: boolean; message: string }> => { + const response = await apiClient.post(`/cabinet/admin/users/${userId}/send-message`, { text }); + return response.data; + }, + // Update restrictions updateRestrictions: async ( userId: number, diff --git a/src/components/admin/userDetail/InfoTab.tsx b/src/components/admin/userDetail/InfoTab.tsx index 7438ae3..22ecf6e 100644 --- a/src/components/admin/userDetail/InfoTab.tsx +++ b/src/components/admin/userDetail/InfoTab.tsx @@ -1,12 +1,15 @@ +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router'; +import { useNotify } from '../../../platform/hooks/useNotify'; import { useCurrency } from '../../../hooks/useCurrency'; import { createNumberInputHandler } from '../../../utils/inputHelpers'; -import type { - UserDetailResponse, - UserListItem, - UserPanelInfo, - UserSubscriptionInfo, +import { + adminUsersApi, + type UserDetailResponse, + type UserListItem, + type UserPanelInfo, + type UserSubscriptionInfo, } from '../../../api/adminUsers'; import type { PromoGroup } from '../../../api/promocodes'; import { ServerIcon } from '@/components/icons'; @@ -92,6 +95,12 @@ export function InfoTab(props: InfoTabProps) { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); + const notify = useNotify(); + + // «Отправить сообщение» — паритет с бот-кнопкой в карточке юзера + const [sendMsgOpen, setSendMsgOpen] = useState(false); + const [sendMsgText, setSendMsgText] = useState(''); + const [sendMsgLoading, setSendMsgLoading] = useState(false); const { user, hasPermission, @@ -124,6 +133,32 @@ export function InfoTab(props: InfoTabProps) { onFullDeleteUser, } = props; + const handleSendMessage = async () => { + const text = sendMsgText.trim(); + if (!text || sendMsgLoading) return; + setSendMsgLoading(true); + try { + await adminUsersApi.sendMessage(user.id, text); + notify.success(t('admin.users.sendMessage.success'), t('common.success')); + setSendMsgOpen(false); + setSendMsgText(''); + } catch (err) { + const detail = ( + err as { response?: { data?: { detail?: { code?: string; message?: string } | string } } } + )?.response?.data?.detail; + const code = typeof detail === 'object' ? detail?.code : undefined; + const known = ['no_telegram_id', 'forbidden', 'bad_request']; + const message = + code && known.includes(code) + ? t(`admin.users.sendMessage.errors.${code}`) + : (typeof detail === 'object' ? detail?.message : detail) || + t('admin.users.userActions.error'); + notify.error(message, t('common.error')); + } finally { + setSendMsgLoading(false); + } + }; + return (
{/* Status */} @@ -470,6 +505,16 @@ export function InfoTab(props: InfoTabProps) { {t('admin.users.detail.actions.title')}
+ {hasPermission('users:send_message') && ( + + )}
+ + {/* Send message modal */} + {sendMsgOpen && ( +
+
!sendMsgLoading && setSendMsgOpen(false)} + aria-hidden="true" + /> +
+

+ {t('admin.users.sendMessage.title')} +

+