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')}
+
+
+
+ )}
);
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 2acecf4..a3c6070 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -3353,6 +3353,20 @@
"confirm": {
"block": "Block this user?"
},
+ "sendMessage": {
+ "button": "✉️ Send message",
+ "title": "Send message to user",
+ "placeholder": "Text the bot will send to the user (HTML supported)",
+ "send": "Send",
+ "sending": "Sending...",
+ "success": "Message sent to user",
+ "noTelegram": "This user is registered by email only and cannot receive Telegram messages",
+ "errors": {
+ "no_telegram_id": "This user is registered by email only and cannot receive Telegram messages",
+ "forbidden": "The user has blocked the bot or cannot receive messages",
+ "bad_request": "Telegram rejected the message. Check the text (HTML markup) and try again"
+ }
+ },
"userActions": {
"delete": "Delete user",
"resetTrial": "Reset trial",
diff --git a/src/locales/fa.json b/src/locales/fa.json
index 0e73d2a..7039dc4 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -3153,6 +3153,20 @@
},
"notFound": "کاربر یافت نشد",
"purchaseCount": "تعداد خرید: {{count}}",
+ "sendMessage": {
+ "button": "✉️ ارسال پیام",
+ "title": "ارسال پیام به کاربر",
+ "placeholder": "متنی که ربات برای کاربر ارسال میکند (HTML پشتیبانی میشود)",
+ "send": "ارسال",
+ "sending": "در حال ارسال...",
+ "success": "پیام برای کاربر ارسال شد",
+ "noTelegram": "این کاربر فقط با ایمیل ثبتنام کرده و نمیتواند پیام تلگرام دریافت کند",
+ "errors": {
+ "no_telegram_id": "این کاربر فقط با ایمیل ثبتنام کرده و نمیتواند پیام تلگرام دریافت کند",
+ "forbidden": "کاربر ربات را مسدود کرده یا نمیتواند پیام دریافت کند",
+ "bad_request": "تلگرام پیام را رد کرد. متن (نشانهگذاری HTML) را بررسی کرده و دوباره تلاش کنید"
+ }
+ },
"userActions": {
"delete": "حذف کاربر",
"disable": "غیرفعالسازی",
diff --git a/src/locales/ru.json b/src/locales/ru.json
index d9044ea..55d0ba6 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -3750,6 +3750,20 @@
"confirm": {
"block": "Заблокировать пользователя?"
},
+ "sendMessage": {
+ "button": "✉️ Отправить сообщение",
+ "title": "Отправка сообщения пользователю",
+ "placeholder": "Текст, который бот отправит пользователю (поддерживается HTML)",
+ "send": "Отправить",
+ "sending": "Отправка...",
+ "success": "Сообщение отправлено пользователю",
+ "noTelegram": "Пользователь зарегистрирован только по email и не может получать сообщения в Telegram",
+ "errors": {
+ "no_telegram_id": "Пользователь зарегистрирован только по email и не может получать сообщения в Telegram",
+ "forbidden": "Пользователь заблокировал бота или не может получать сообщения",
+ "bad_request": "Telegram отклонил сообщение. Проверьте текст (HTML-разметку) и попробуйте ещё раз"
+ }
+ },
"userActions": {
"delete": "Удалить пользователя",
"resetTrial": "Сбросить триал",
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 7a4281f..1f08a6f 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -3152,6 +3152,20 @@
},
"notFound": "未找到用户",
"purchaseCount": "购买次数:{{count}}",
+ "sendMessage": {
+ "button": "✉️ 发送消息",
+ "title": "向用户发送消息",
+ "placeholder": "机器人将发送给用户的文本(支持 HTML)",
+ "send": "发送",
+ "sending": "发送中...",
+ "success": "消息已发送给用户",
+ "noTelegram": "该用户仅通过邮箱注册,无法接收 Telegram 消息",
+ "errors": {
+ "no_telegram_id": "该用户仅通过邮箱注册,无法接收 Telegram 消息",
+ "forbidden": "用户已屏蔽机器人或无法接收消息",
+ "bad_request": "Telegram 拒绝了该消息。请检查文本(HTML 标记)后重试"
+ }
+ },
"userActions": {
"delete": "删除用户",
"disable": "禁用",