From 2f743d82dbb4d737fcb5f54dda5269271bae3de2 Mon Sep 17 00:00:00 2001 From: Case211 <87642841+Case211@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:28:33 +0500 Subject: [PATCH] =?UTF-8?q?feat(users):=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA?= =?UTF-8?q?=D0=B0=20=C2=AB=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=C2=BB=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Паритет с бот-кнопкой «✉️ Отправить сообщение»: в блоке действий карточки юзера появляется кнопка (RBAC users:send_message), открывающая модалку с текстом (HTML, лимит 4096) — бот отправляет юзеру прямое сообщение через POST /admin/users/{id}/send-message. Для email-only юзеров без telegram_id кнопка задизейблена с подсказкой; коды ошибок бэкенда (no_telegram_id / forbidden / bad_request) маппятся в честные локализованные сообщения. Локали ru/en/fa/zh. --- src/api/adminUsers.ts | 9 ++ src/components/admin/userDetail/InfoTab.tsx | 104 +++++++++++++++++++- src/locales/en.json | 14 +++ src/locales/fa.json | 14 +++ src/locales/ru.json | 14 +++ src/locales/zh.json | 14 +++ 6 files changed, 164 insertions(+), 5 deletions(-) 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')} +

+