diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index 2ff043b..c4a3d3d 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -415,7 +415,7 @@ export const adminUsersApi = { return response.data; }, - // Delete user + // Delete user (soft delete, does NOT remove from Remnawave) deleteUser: async ( userId: number, softDelete = true, @@ -427,6 +427,20 @@ export const adminUsersApi = { return response.data; }, + // Full delete user (removes from bot DB + Remnawave panel) + fullDeleteUser: async ( + userId: number, + ): Promise<{ + success: boolean; + message: string; + deleted_from_bot: boolean; + deleted_from_panel: boolean; + panel_error: string | null; + }> => { + const response = await apiClient.delete(`/cabinet/admin/users/${userId}/full`); + return response.data; + }, + // Get referrals getReferrals: async (userId: number, offset = 0, limit = 50): Promise => { const response = await apiClient.get(`/cabinet/admin/users/${userId}/referrals`, { diff --git a/src/locales/en.json b/src/locales/en.json index 96b5497..77acc04 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1808,7 +1808,7 @@ }, "confirmDelete": { "title": "Delete user?", - "message": "Are you sure? This action is irreversible. All user data will be deleted." + "message": "Are you sure? This action is irreversible. The user will be fully deleted from the bot and from the Remnawave panel." }, "confirmResetTrial": { "title": "Reset trial?", @@ -1848,6 +1848,10 @@ "subscription": "• Subscription purchase prohibited", "reason": "Reason" }, + "actions": { + "title": "Actions", + "areYouSure": "Are you sure?" + }, "subscription": { "current": "Current subscription", "tariff": "Tariff", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7585644..c6ef15f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2335,7 +2335,7 @@ }, "confirmDelete": { "title": "Удалить пользователя?", - "message": "Вы уверены? Это действие необратимо. Все данные пользователя будут удалены." + "message": "Вы уверены? Это действие необратимо. Пользователь будет полностью удалён из бота и из панели Remnawave." }, "confirmResetTrial": { "title": "Сбросить триал?", @@ -2375,6 +2375,10 @@ "subscription": "• Запрет покупки подписки", "reason": "Причина" }, + "actions": { + "title": "Действия", + "areYouSure": "Уверены?" + }, "subscription": { "current": "Текущая подписка", "tariff": "Тариф", diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 40c2c67..5a0c6c0 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -1,8 +1,9 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { useCurrency } from '../hooks/useCurrency'; +import { useNotify } from '../platform/hooks/useNotify'; import { adminUsersApi, type UserDetailResponse, @@ -80,6 +81,7 @@ export default function AdminUserDetail() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); + const notify = useNotify(); const { id } = useParams<{ id: string }>(); const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; @@ -92,6 +94,10 @@ export default function AdminUserDetail() { const [tariffs, setTariffs] = useState([]); const [actionLoading, setActionLoading] = useState(false); + // Inline confirm state + const [confirmingAction, setConfirmingAction] = useState(null); + const confirmTimerRef = useRef | null>(null); + // Balance form const [balanceAmount, setBalanceAmount] = useState(''); const [balanceDescription, setBalanceDescription] = useState(''); @@ -256,6 +262,90 @@ export default function AdminUserDetail() { } }; + const handleInlineConfirm = (actionKey: string, executeFn: () => Promise) => { + if (confirmingAction === actionKey) { + if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); + setConfirmingAction(null); + executeFn(); + } else { + if (confirmTimerRef.current) clearTimeout(confirmTimerRef.current); + setConfirmingAction(actionKey); + confirmTimerRef.current = setTimeout(() => setConfirmingAction(null), 3000); + } + }; + + const handleResetTrial = async () => { + if (!userId) return; + setActionLoading(true); + try { + const result = await adminUsersApi.resetTrial(userId); + if (result.success) { + notify.success(t('admin.users.userActions.success.resetTrial'), t('common.success')); + await loadUser(); + } else { + notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); + } + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleResetSubscription = async () => { + if (!userId) return; + setActionLoading(true); + try { + const result = await adminUsersApi.resetSubscription(userId); + if (result.success) { + notify.success(t('admin.users.userActions.success.resetSubscription'), t('common.success')); + await loadUser(); + } else { + notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); + } + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleDisableUser = async () => { + if (!userId) return; + setActionLoading(true); + try { + const result = await adminUsersApi.disableUser(userId); + if (result.success) { + notify.success(t('admin.users.userActions.success.disable'), t('common.success')); + await loadUser(); + } else { + notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); + } + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleFullDeleteUser = async () => { + if (!userId) return; + setActionLoading(true); + try { + const result = await adminUsersApi.fullDeleteUser(userId); + if (result.success) { + notify.success(t('admin.users.userActions.success.delete'), t('common.success')); + navigate('/admin/users'); + } else { + notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); + } + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + const formatDate = (date: string | null) => { if (!date) return '-'; return new Date(date).toLocaleDateString(locale, { @@ -459,6 +549,67 @@ export default function AdminUserDetail() { )} )} + + {/* Actions */} +
+
+ {t('admin.users.detail.actions.title')} +
+
+ + + + +
+
)} diff --git a/src/pages/AdminUsers.tsx b/src/pages/AdminUsers.tsx index 1831f93..ef01046 100644 --- a/src/pages/AdminUsers.tsx +++ b/src/pages/AdminUsers.tsx @@ -1,10 +1,9 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { useCurrency } from '../hooks/useCurrency'; import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers'; import { usePlatform } from '../platform/hooks/usePlatform'; -import { useNotify } from '../platform/hooks/useNotify'; // ============ Icons ============ @@ -58,204 +57,6 @@ const TelegramIcon = () => ( ); -const DotsVerticalIcon = () => ( - - - -); - -const TrashIcon = () => ( - - - -); - -const ArrowPathIcon = () => ( - - - -); - -const NoSymbolIcon = () => ( - - - -); - -const ExclamationTriangleIcon = () => ( - - - -); - -// ============ 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) => ( - - ))} -
- )} -
- ); -} - // ============ Components ============ interface StatCardProps { @@ -305,11 +106,10 @@ function StatusBadge({ status }: { status: string }) { interface UserRowProps { user: UserListItem; onClick: () => void; - onAction: (action: UserAction, user: UserListItem) => void; formatAmount: (rubAmount: number) => string; } -function UserRow({ user, onClick, onAction, formatAmount }: UserRowProps) { +function UserRow({ user, onClick, formatAmount }: UserRowProps) { const { t } = useTranslation(); return (
- {/* Actions Menu - hide chevron on mobile, show only dots */} - - -
- -
+
); } // ============ Main Page ============ -interface ConfirmModalState { - isOpen: boolean; - action: UserAction | null; - user: UserListItem | null; -} - export default function AdminUsers() { const { t } = useTranslation(); const { formatWithCurrency } = useCurrency(); const navigate = useNavigate(); - const notify = useNotify(); const { capabilities } = usePlatform(); const [users, setUsers] = useState([]); @@ -406,12 +194,6 @@ export default function AdminUsers() { const [sortBy, setSortBy] = useState('created_at'); const [offset, setOffset] = useState(0); const [total, setTotal] = useState(0); - const [confirmModal, setConfirmModal] = useState({ - isOpen: false, - action: null, - user: null, - }); - const [actionLoading, setActionLoading] = useState(false); const limit = 20; @@ -458,92 +240,6 @@ 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) { - notify.success(t(`admin.users.userActions.success.${action}`), t('common.success')); - loadUsers(); - loadStats(); - } else { - notify.error(result.message || t('admin.users.userActions.error'), t('common.error')); - } - } catch (error) { - console.error('Action failed:', error); - notify.error(t('admin.users.userActions.error'), t('common.error')); - } finally { - setActionLoading(false); - closeConfirmModal(); - } - }; - - const getConfirmModalContent = () => { - const { action } = confirmModal; - if (!action) return { title: '', message: '', confirmText: '', confirmButtonClass: '' }; - - 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'), - 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; @@ -682,7 +378,6 @@ export default function AdminUsers() { key={user.id} user={user} onClick={() => navigate(`/admin/users/${user.id}`)} - onAction={handleUserAction} formatAmount={(amount) => formatWithCurrency(amount)} /> )) @@ -720,15 +415,6 @@ export default function AdminUsers() { )} - - {/* User action confirmation modal */} - ); }