From eae12e269418dd1b8f6ff9c6ab9ab268f3631a35 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 04:39:13 +0300 Subject: [PATCH 1/4] Update adminUsers.ts --- src/api/adminUsers.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index 2c35665..2ff043b 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -476,4 +476,22 @@ export const adminUsersApi = { const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data); return response.data; }, + + // Reset trial + resetTrial: async (userId: number): Promise<{ success: boolean; message: string }> => { + const response = await apiClient.post(`/cabinet/admin/users/${userId}/reset-trial`); + return response.data; + }, + + // Reset subscription + resetSubscription: async (userId: number): Promise<{ success: boolean; message: string }> => { + const response = await apiClient.post(`/cabinet/admin/users/${userId}/reset-subscription`); + return response.data; + }, + + // Disable user + disableUser: async (userId: number): Promise<{ success: boolean; message: string }> => { + const response = await apiClient.post(`/cabinet/admin/users/${userId}/disable`); + return response.data; + }, }; From 42bb563ab679a51399a6d870261cf93f8bb76267 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 04:39:42 +0300 Subject: [PATCH 2/4] Update AdminUsers.tsx --- src/pages/AdminUsers.tsx | 328 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 326 insertions(+), 2 deletions(-) diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index 9ae68d8..8b8f9b6 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -1,8 +1,9 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { useCurrency } from '../hooks/useCurrency'; +import { useToast } from '../components/Toast'; import { adminUsersApi, type UserListItem, @@ -81,6 +82,56 @@ const TelegramIcon = () => ( ); +const DotsVerticalIcon = () => ( + + + +); + +const TrashIcon = () => ( + + + +); + +const ArrowPathIcon = () => ( + + + +); + +const NoSymbolIcon = () => ( + + + +); + +const ExclamationTriangleIcon = () => ( + + + +); + // ============ Components ============ interface StatCardProps { @@ -125,15 +176,167 @@ function StatusBadge({ status }: { status: string }) { ); } +// ============ Confirmation Modal ============ + +interface ConfirmationModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + title: string; + message: string; + confirmText: string; + confirmButtonClass?: string; + isLoading?: boolean; +} + +function ConfirmationModal({ + isOpen, + onClose, + onConfirm, + title, + message, + confirmText, + confirmButtonClass = 'bg-rose-500 hover:bg-rose-600', + isLoading = false, +}: ConfirmationModalProps) { + const { t } = useTranslation(); + + if (!isOpen) return null; + + return ( +
+
+
+
+
+ +
+

{title}

+
+

{message}

+
+ + +
+
+
+ ); +} + +// ============ User Actions Menu ============ + +type UserAction = 'delete' | 'resetTrial' | 'resetSubscription' | 'disable'; + +interface UserActionsMenuProps { + user: UserListItem; + onAction: (action: UserAction, user: UserListItem) => void; +} + +function UserActionsMenu({ user, onAction }: UserActionsMenuProps) { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleAction = (action: UserAction) => { + setIsOpen(false); + onAction(action, user); + }; + + const actions = [ + { + key: 'resetTrial' as const, + label: t('admin.users.userActions.resetTrial'), + icon: , + className: 'text-blue-400 hover:bg-blue-500/10', + }, + { + key: 'resetSubscription' as const, + label: t('admin.users.userActions.resetSubscription'), + icon: , + className: 'text-amber-400 hover:bg-amber-500/10', + }, + { + key: 'disable' as const, + label: t('admin.users.userActions.disable'), + icon: , + className: 'text-dark-400 hover:bg-dark-700', + }, + { + key: 'delete' as const, + label: t('admin.users.userActions.delete'), + icon: , + className: 'text-rose-400 hover:bg-rose-500/10', + }, + ]; + + return ( +
+ + + {isOpen && ( +
+ {actions.map((action) => ( + + ))} +
+ )} +
+ ); +} + // ============ User List Component ============ interface UserRowProps { user: UserListItem; onSelect: (user: UserListItem) => void; + onAction: (action: UserAction, user: UserListItem) => void; formatAmount: (rubAmount: number) => string; } -function UserRow({ user, onSelect, formatAmount }: UserRowProps) { +function UserRow({ user, onSelect, onAction, formatAmount }: UserRowProps) { const { t } = useTranslation(); return (
+ {/* Actions Menu */} + +
); @@ -970,9 +1176,16 @@ function UserDetailModal({ userId, onClose, onUpdate }: UserDetailModalProps) { // ============ Main Page ============ +interface ConfirmModalState { + isOpen: boolean; + action: UserAction | null; + user: UserListItem | null; +} + export default function AdminUsers() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); + const { showToast } = useToast(); const [users, setUsers] = useState([]); const [stats, setStats] = useState(null); @@ -984,6 +1197,12 @@ export default function AdminUsers() { const [offset, setOffset] = useState(0); const [total, setTotal] = useState(0); const [selectedUserId, setSelectedUserId] = useState(null); + const [confirmModal, setConfirmModal] = useState({ + isOpen: false, + action: null, + user: null, + }); + const [actionLoading, setActionLoading] = useState(false); const limit = 20; @@ -1030,6 +1249,101 @@ export default function AdminUsers() { loadUsers(); }; + const handleUserAction = (action: UserAction, user: UserListItem) => { + setConfirmModal({ isOpen: true, action, user }); + }; + + const closeConfirmModal = () => { + setConfirmModal({ isOpen: false, action: null, user: null }); + }; + + const executeAction = async () => { + const { action, user } = confirmModal; + if (!action || !user) return; + + setActionLoading(true); + try { + let result: { success: boolean; message: string }; + + switch (action) { + case 'delete': + result = await adminUsersApi.deleteUser(user.id); + break; + case 'resetTrial': + result = await adminUsersApi.resetTrial(user.id); + break; + case 'resetSubscription': + result = await adminUsersApi.resetSubscription(user.id); + break; + case 'disable': + result = await adminUsersApi.disableUser(user.id); + break; + default: + throw new Error('Unknown action'); + } + + if (result.success) { + showToast({ + type: 'success', + title: t('common.success'), + message: t(`admin.users.userActions.success.${action}`), + }); + loadUsers(); + loadStats(); + } else { + showToast({ + type: 'error', + title: t('common.error'), + message: result.message || t('admin.users.userActions.error'), + }); + } + } catch (error) { + console.error('Action failed:', error); + showToast({ + type: 'error', + title: t('common.error'), + message: t('admin.users.userActions.error'), + }); + } finally { + setActionLoading(false); + closeConfirmModal(); + } + }; + + const getConfirmModalContent = () => { + const { action } = confirmModal; + if (!action) return { title: '', message: '', confirmText: '', confirmButtonClass: '' }; + + const configs: Record = { + delete: { + title: t('admin.users.userActions.confirmDelete.title'), + message: t('admin.users.userActions.confirmDelete.message'), + confirmText: t('admin.users.userActions.delete'), + confirmButtonClass: 'bg-rose-500 hover:bg-rose-600', + }, + resetTrial: { + title: t('admin.users.userActions.confirmResetTrial.title'), + message: t('admin.users.userActions.confirmResetTrial.message'), + confirmText: t('admin.users.userActions.resetTrial'), + confirmButtonClass: 'bg-blue-500 hover:bg-blue-600', + }, + resetSubscription: { + title: t('admin.users.userActions.confirmResetSubscription.title'), + message: t('admin.users.userActions.confirmResetSubscription.message'), + confirmText: t('admin.users.userActions.resetSubscription'), + confirmButtonClass: 'bg-amber-500 hover:bg-amber-600', + }, + disable: { + title: t('admin.users.userActions.confirmDisable.title'), + message: t('admin.users.userActions.confirmDisable.message'), + confirmText: t('admin.users.userActions.disable'), + confirmButtonClass: 'bg-dark-600 hover:bg-dark-500', + }, + }; + + return configs[action]; + }; + const totalPages = Math.ceil(total / limit); const currentPage = Math.floor(offset / limit) + 1; @@ -1165,6 +1479,7 @@ export default function AdminUsers() { key={user.id} user={user} onSelect={(u) => setSelectedUserId(u.id)} + onAction={handleUserAction} formatAmount={(amount) => formatWithCurrency(amount)} /> )) @@ -1214,6 +1529,15 @@ export default function AdminUsers() { }} /> )} + + {/* User action confirmation modal */} + ); } From efb8113647ed0596a75519db18c4778b2527d833 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 04:40:00 +0300 Subject: [PATCH 3/4] Add files via upload --- src/locales/en.json | 29 +++++++++++++++++++++++++++++ src/locales/ru.json | 29 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/locales/en.json b/src/locales/en.json index d98e720..e85fa57 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1609,6 +1609,35 @@ "confirm": { "block": "Block this user?" }, + "userActions": { + "delete": "Delete user", + "resetTrial": "Reset trial", + "resetSubscription": "Reset subscription", + "disable": "Disable user", + "error": "Failed to perform action", + "success": { + "delete": "User successfully deleted", + "resetTrial": "Trial successfully reset", + "resetSubscription": "Subscription successfully reset", + "disable": "User successfully disabled" + }, + "confirmDelete": { + "title": "Delete user?", + "message": "Are you sure? This action is irreversible. All user data will be deleted." + }, + "confirmResetTrial": { + "title": "Reset trial?", + "message": "The user will be able to use the trial period again." + }, + "confirmResetSubscription": { + "title": "Reset subscription?", + "message": "The user's current subscription will be cancelled." + }, + "confirmDisable": { + "title": "Disable user?", + "message": "The user will not be able to use the service until reactivated." + } + }, "detail": { "tabs": { "info": "Information", diff --git a/src/locales/ru.json b/src/locales/ru.json index d0acebb..2ca16af 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2120,6 +2120,35 @@ "confirm": { "block": "Заблокировать пользователя?" }, + "userActions": { + "delete": "Удалить пользователя", + "resetTrial": "Сбросить триал", + "resetSubscription": "Сбросить подписку", + "disable": "Отключить пользователя", + "error": "Не удалось выполнить действие", + "success": { + "delete": "Пользователь успешно удалён", + "resetTrial": "Триал успешно сброшен", + "resetSubscription": "Подписка успешно сброшена", + "disable": "Пользователь успешно отключён" + }, + "confirmDelete": { + "title": "Удалить пользователя?", + "message": "Вы уверены? Это действие необратимо. Все данные пользователя будут удалены." + }, + "confirmResetTrial": { + "title": "Сбросить триал?", + "message": "Пользователь сможет снова воспользоваться пробным периодом." + }, + "confirmResetSubscription": { + "title": "Сбросить подписку?", + "message": "Текущая подписка пользователя будет отменена." + }, + "confirmDisable": { + "title": "Отключить пользователя?", + "message": "Пользователь не сможет пользоваться сервисом до повторной активации." + } + }, "detail": { "tabs": { "info": "Информация", From bab722385d7aa316eef8a50da2fe0f24dede61be Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 3 Feb 2026 04:41:34 +0300 Subject: [PATCH 4/4] Update AdminUsers.tsx --- src/pages/AdminUsers.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index 8b8f9b6..54cefa6 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -205,10 +205,7 @@ function ConfirmationModal({ return (
-
+
@@ -1314,7 +1311,10 @@ export default function AdminUsers() { const { action } = confirmModal; if (!action) return { title: '', message: '', confirmText: '', confirmButtonClass: '' }; - const configs: Record = { + const configs: Record< + UserAction, + { title: string; message: string; confirmText: string; confirmButtonClass: string } + > = { delete: { title: t('admin.users.userActions.confirmDelete.title'), message: t('admin.users.userActions.confirmDelete.message'),