diff --git a/src/admin/public/style.css b/src/admin/public/style.css index 6b90e8f..0440b52 100644 --- a/src/admin/public/style.css +++ b/src/admin/public/style.css @@ -336,6 +336,79 @@ pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width .form-row input { flex: 1; } +.wallet-layout { + display: grid; + grid-template-columns: 280px 1fr; + gap: 1.5rem; + align-items: start; +} + +@media (max-width: 900px) { + .wallet-layout { grid-template-columns: 1fr; } +} + +.wallet-sidebar { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + max-height: 70vh; + overflow-y: auto; +} + +.wallet-sidebar h3 { + margin-bottom: 0.75rem; +} + +.wallet-user-list { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.wallet-user-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: var(--radius); + text-decoration: none; + color: var(--text); + font-size: 0.85rem; + transition: background 0.15s; +} + +.wallet-user-item:hover { + background: #f0f4ff; +} + +.wallet-user-item.selected { + background: var(--primary); + color: #fff; +} + +.wallet-user-id { + font-weight: 600; + min-width: 28px; +} + +.wallet-user-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.wallet-user-meta { + font-size: 0.75rem; + opacity: 0.7; + white-space: nowrap; +} + +.wallet-main { + min-width: 0; +} + @media (max-width: 640px) { .topnav { flex-direction: column; align-items: flex-start; } .logout-btn { margin-left: 0; } diff --git a/src/admin/routes/seed.js b/src/admin/routes/seed.js index 72f2489..7457171 100644 --- a/src/admin/routes/seed.js +++ b/src/admin/routes/seed.js @@ -59,10 +59,10 @@ router.post('/seed-demo', async (req, res) => { const subSoftware = subs.find(s => s.name === 'Software' && s.category_id === catDigital).id; await db.runAsync(`INSERT INTO users (telegram_id, username, country, city, district, status, total_balance, bonus_balance) VALUES - ('1001', 'alice', 'Russia', 'Moscow', 'Center', 1, 150.00, 25.00), - ('1002', 'bob', 'Russia', 'Moscow', 'Center', 1, 85.50, 10.00), - ('1003', 'charlie', 'Russia', 'Saint Petersburg', 'North', 1, 320.75, 50.00), - ('1004', 'diana', 'Germany', 'Berlin', 'Mitte', 1, 45.00, 5.00), + ('1001', 'alice', 'Russia', 'Moscow', 'Center', 0, 150.00, 25.00), + ('1002', 'bob', 'Russia', 'Moscow', 'Center', 0, 85.50, 10.00), + ('1003', 'charlie', 'Russia', 'Saint Petersburg', 'North', 0, 320.75, 50.00), + ('1004', 'diana', 'Germany', 'Berlin', 'Mitte', 0, 45.00, 5.00), ('1005', 'evan', 'Germany', 'Berlin', 'Mitte', 0, 0.00, 0.00)`); const users = await db.allAsync('SELECT id, username FROM users'); const uAlice = users.find(u => u.username === 'alice').id; diff --git a/src/admin/routes/users.js b/src/admin/routes/users.js index 2349cdb..c49ec1e 100644 --- a/src/admin/routes/users.js +++ b/src/admin/routes/users.js @@ -24,7 +24,7 @@ router.get('/:id', async (req, res) => { router.post('/:id/toggle-status', async (req, res) => { const user = await db.getAsync('SELECT * FROM users WHERE id = ?', [req.params.id]); if (!user) return res.status(404).send('User not found'); - const newStatus = user.status === 1 ? 0 : 1; + const newStatus = user.status === 0 ? 2 : 0; await db.runAsync('UPDATE users SET status = ? WHERE id = ?', [newStatus, user.id]); res.redirect('/users'); }); diff --git a/src/admin/routes/wallets.js b/src/admin/routes/wallets.js index 0b367a1..c82b2a6 100644 --- a/src/admin/routes/wallets.js +++ b/src/admin/routes/wallets.js @@ -1,14 +1,32 @@ import { Router } from 'express'; import db from '../../config/database.js'; -import { renderWalletList } from '../views/wallets.js'; +import { renderWalletLayout } from '../views/wallets.js'; const router = Router(); router.get('/', async (req, res) => { - const wallets = await db.allAsync( - 'SELECT * FROM crypto_wallets ORDER BY id DESC LIMIT 200' + const users = await db.allAsync( + `SELECT u.id, u.telegram_id, u.username, u.status, u.total_balance, u.bonus_balance, + COUNT(w.id) AS wallet_count + FROM users u + LEFT JOIN crypto_wallets w ON w.user_id = u.id AND w.wallet_type NOT LIKE '%#_%' ESCAPE '#' + GROUP BY u.id + ORDER BY u.id DESC` ); - res.send(renderWalletList(wallets)); + + const selectedId = req.query.user ? parseInt(req.query.user, 10) : (users.length > 0 ? users[0].id : null); + + let wallets = []; + let selectedUser = null; + if (selectedId) { + selectedUser = await db.getAsync('SELECT * FROM users WHERE id = ?', [selectedId]); + wallets = await db.allAsync( + `SELECT * FROM crypto_wallets WHERE user_id = ? AND wallet_type NOT LIKE '%#_%' ESCAPE '#' ORDER BY wallet_type`, + [selectedId] + ); + } + + res.send(renderWalletLayout(users, selectedUser, wallets)); }); -export default router; +export default router; \ No newline at end of file diff --git a/src/admin/views/users.js b/src/admin/views/users.js index 61f9251..ef2bb67 100644 --- a/src/admin/views/users.js +++ b/src/admin/views/users.js @@ -8,12 +8,12 @@ export function renderUserList(users, message) { ${u.username || '-'} ${u.country || '-'} ${u.city || '-'} - ${u.status === 1 ? 'Active' : 'Banned'} + ${u.status === 0 ? 'Active' : u.status === 2 ? 'Blocked' : 'Deleted'} $${(u.total_balance || 0).toFixed(2)} View
- +
`).join(''); @@ -37,7 +37,7 @@ export function renderUserDetail(user, purchases) {

Telegram ID: ${user.telegram_id}

Country: ${user.country || '-'}

City: ${user.city || '-'}

-

Status: ${user.status === 1 ? 'Active' : 'Banned'}

+

Status: ${user.status === 0 ? 'Active' : user.status === 2 ? 'Blocked' : 'Deleted'}

Balance: $${(user.total_balance || 0).toFixed(2)}

Bonus: $${(user.bonus_balance || 0).toFixed(2)}

diff --git a/src/admin/views/wallets.js b/src/admin/views/wallets.js index 6c28e54..27f25c4 100644 --- a/src/admin/views/wallets.js +++ b/src/admin/views/wallets.js @@ -1,17 +1,56 @@ -import { layout, table } from './layout.js'; +import { layout } from './layout.js'; +import { escapeHtml } from './escape.js'; -export function renderWalletList(wallets) { - const headers = ['ID', 'User ID', 'Type', 'Address', 'Balance', 'Created']; - const rows = wallets.map(w => ` - ${w.id} - ${w.user_id} - ${w.wallet_type} - ${(w.address || '').slice(0, 16)}... - ${(w.balance || 0).toFixed(8)} - ${w.created_at || '-'} - `).join(''); +export function renderWalletLayout(users, selectedUser, wallets) { + const userListHtml = users.map(u => { + const isActive = selectedUser && u.id === selectedUser.id; + const statusBadge = u.status === 0 ? 'active' : u.status === 2 ? 'banned' : 'banned'; + const statusText = u.status === 0 ? '✅' : '❌'; + return ` + #${u.id} + ${escapeHtml(u.username || u.telegram_id)} + ${statusText} ${u.wallet_count}w + `; + }).join(''); + + const walletRows = wallets.length > 0 + ? wallets.map(w => ` + ${escapeHtml(w.wallet_type)} + ${escapeHtml((w.address || '').slice(0, 20))}${w.address && w.address.length > 20 ? '...' : ''} + ${(w.balance || 0).toFixed(8)} + ${w.created_at || '-'} + `).join('') + : 'No wallets yet'; + + const balanceCard = selectedUser ? ` +
+

Balances

+

Main: $${(selectedUser.total_balance || 0).toFixed(2)}

+

Bonus: $${(selectedUser.bonus_balance || 0).toFixed(2)}

+

Available: $${((selectedUser.total_balance || 0) + (selectedUser.bonus_balance || 0)).toFixed(2)}

+

Status: ${selectedUser.status === 0 ? 'Active' : selectedUser.status === 2 ? 'Blocked' : 'Deleted'}

+
` : ''; + + const content = ` +
+ +
+ ${selectedUser ? ` +

${escapeHtml(selectedUser.username || 'User #' + selectedUser.id)} + — ${selectedUser.telegram_id} +

+ ${balanceCard} +

Crypto Wallets (${wallets.length})

+ + + ${walletRows} +
TypeAddressBalanceCreated
+ ` : '

Select a user to view wallets

'} +
+
`; - const content = table(headers, wallets, () => '') - .replace('', `${rows}`); return layout('Wallets', content, 'wallets'); -} +} \ No newline at end of file diff --git a/src/handlers/adminHandlers/adminUserHandler.js b/src/handlers/adminHandlers/adminUserHandler.js index fff82af..f3628c5 100644 --- a/src/handlers/adminHandlers/adminUserHandler.js +++ b/src/handlers/adminHandlers/adminUserHandler.js @@ -238,6 +238,8 @@ export default class AdminUserHandler { const message = ` 👤 User Profile: + Status: ${user.status === 0 ? '✅ Active' : user.status === 2 ? '🚫 Blocked' : '❌ Deleted'} + ID: ${telegramId} 📍 Location: ${detailedUser.country || 'Not set'}, ${detailedUser.city || 'Not set'}, ${detailedUser.district || 'Not set'} @@ -266,7 +268,7 @@ export default class AdminUserHandler { {text: '📍 Edit Location', callback_data: `edit_user_location_${telegramId}`} ], [ - {text: '🚫 Block User', callback_data: `block_user_${telegramId}`}, + {text: user.status === 2 ? '✅ Unblock User' : '🚫 Block User', callback_data: `block_user_${telegramId}`}, {text: '❌ Delete User', callback_data: `delete_user_${telegramId}`} ], [{text: '« Back to User List', callback_data: `list_users_0`}] @@ -364,17 +366,27 @@ export default class AdminUserHandler { const chatId = callbackQuery.message.chat.id; try { + const user = await UserService.getUserByTelegramId(telegramId); + if (!user) { + await bot.sendMessage(chatId, 'User not found.'); + return; + } + + const isBlocked = user.status === 2; + const actionText = isBlocked ? 'unblock' : 'block'; + const confirmText = isBlocked ? '✅ Confirm Unblock' : '✅ Confirm Block'; + const keyboard = { inline_keyboard: [ [ - {text: '✅ Confirm Block', callback_data: `confirm_block_user_${telegramId}`}, + {text: confirmText, callback_data: `confirm_block_user_${telegramId}`}, {text: '❌ Cancel', callback_data: `view_user_${telegramId}`} ] ] }; await bot.editMessageText( - `⚠️ Are you sure you want to block user ${telegramId}?`, + `⚠️ Are you sure you want to ${actionText} user ${telegramId}?`, { chat_id: chatId, message_id: callbackQuery.message.message_id, @@ -397,7 +409,15 @@ export default class AdminUserHandler { const chatId = callbackQuery.message.chat.id; try { - await UserService.updateUserStatus(telegramId, 2); + const user = await UserService.getUserByTelegramId(telegramId); + if (!user) { + await bot.sendMessage(chatId, 'User not found.'); + return; + } + + const isBlocked = user.status === 2; + const newStatus = isBlocked ? 0 : 2; + await UserService.updateUserStatus(telegramId, newStatus); const keyboard = { inline_keyboard: [ @@ -406,22 +426,25 @@ export default class AdminUserHandler { }; try { - await bot.sendMessage(telegramId, '⚠️Your account has been blocked by administrator'); + await bot.sendMessage(telegramId, isBlocked + ? '✅ Your account has been unblocked by administrator' + : '⚠️ Your account has been blocked by administrator'); } catch (e) { // ignore if we can't notify user } - await bot.editMessageText( - `✅ User ${telegramId} has been successfully blocked.`, - { - chat_id: chatId, - message_id: callbackQuery.message.message_id, - reply_markup: keyboard - } - ); + const resultText = isBlocked + ? `✅ User ${telegramId} has been successfully unblocked.` + : `✅ User ${telegramId} has been successfully blocked.`; + + await bot.editMessageText(resultText, { + chat_id: chatId, + message_id: callbackQuery.message.message_id, + reply_markup: keyboard + }); } catch (error) { logger.error({ err: error }, 'Error in handleConfirmBlock'); - await bot.sendMessage(chatId, 'Error blocking user. Please try again.'); + await bot.sendMessage(chatId, 'Error updating user status. Please try again.'); } }