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 */} + ); }