feat: admin bulk actions page with TanStack Table

Full-featured admin page for mass operations on users:

- TanStack Table with checkbox selection (select all / individual)
- Server-side pagination, search, and filters (status, tariff, promo group)
- Columns: user, subscription status, tariff, balance, days left, promo group
- Floating glass-effect action bar with 7 bulk operations:
  extend subscription, deactivate, activate, change tariff,
  add traffic, add balance, assign promo group
- Action modal dialogs with input fields, loading state, result summary
- Responsive design for desktop, mobile, and Telegram Mini App
- Full i18n support (ru, en)
- Dark theme with accent highlights and status color badges
This commit is contained in:
Fringg
2026-04-24 04:15:28 +03:00
parent 33f32648f0
commit 1772d9632e
6 changed files with 1382 additions and 2 deletions

View File

@@ -123,6 +123,7 @@ const AdminRemnawave = lazyWithRetry(() => import('./pages/AdminRemnawave'));
const AdminRemnawaveSquadDetail = lazyWithRetry(() => import('./pages/AdminRemnawaveSquadDetail'));
const AdminEmailTemplates = lazyWithRetry(() => import('./pages/AdminEmailTemplates'));
const AdminTrafficUsage = lazyWithRetry(() => import('./pages/AdminTrafficUsage'));
const AdminBulkActions = lazyWithRetry(() => import('./pages/AdminBulkActions'));
const AdminSalesStats = lazyWithRetry(() => import('./pages/AdminSalesStats'));
const AdminUpdates = lazyWithRetry(() => import('./pages/AdminUpdates'));
const AdminUserDetail = lazyWithRetry(() => import('./pages/AdminUserDetail'));
@@ -966,6 +967,16 @@ function App() {
</PermissionRoute>
}
/>
<Route
path="/admin/bulk-actions"
element={
<PermissionRoute permission="users:read">
<LazyPage>
<AdminBulkActions />
</LazyPage>
</PermissionRoute>
}
/>
<Route
path="/admin/payments"
element={

View File

@@ -0,0 +1,42 @@
import apiClient from './client';
export type BulkActionType =
| 'extend'
| 'cancel'
| 'activate'
| 'change_tariff'
| 'add_traffic'
| 'add_balance'
| 'assign_promo_group';
export interface BulkActionRequest {
action: BulkActionType;
user_ids: number[];
params: BulkActionParams;
}
export interface BulkActionParams {
days?: number;
tariff_id?: number;
traffic_gb?: number;
balance_kopeks?: number;
promo_group_id?: number | null;
}
export interface BulkActionResult {
success: boolean;
total: number;
success_count: number;
error_count: number;
errors: Array<{
user_id: number;
error: string;
}>;
}
export const adminBulkActionsApi = {
execute: async (data: BulkActionRequest): Promise<BulkActionResult> => {
const response = await apiClient.post('/cabinet/admin/bulk/execute', data);
return response.data;
},
};

View File

@@ -1125,7 +1125,8 @@
"salesStats": "Sales Statistics",
"landings": "Landings",
"referralNetwork": "Referral Network",
"news": "News"
"news": "News",
"bulkActions": "Bulk Actions"
},
"panel": {
"title": "Admin Panel",
@@ -2021,6 +2022,62 @@
"telegramOidcClientSecret": "Client Secret"
}
},
"bulkActions": {
"title": "Bulk Actions",
"subtitle": "User management",
"selectedCount": "Selected: {{count}}",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"noResults": "No users found",
"actions": {
"extend": "Extend subscription",
"cancel": "Deactivate subscriptions",
"activate": "Activate subscriptions",
"changeTariff": "Change tariff",
"addTraffic": "Add traffic",
"addBalance": "Add balance",
"assignPromoGroup": "Assign promo group"
},
"params": {
"days": "Number of days",
"tariff": "Select tariff",
"trafficGb": "Traffic amount (GB)",
"balanceRub": "Amount (RUB)",
"promoGroup": "Promo group",
"removePromoGroup": "Remove promo group"
},
"confirm": "Apply",
"cancel": "Cancel",
"executing": "Executing...",
"complete": "Complete",
"successCount": "Succeeded: {{count}}",
"errorCount": "Errors: {{count}}",
"filters": {
"search": "Search by ID, username, email",
"status": "Subscription status",
"tariff": "Tariff",
"promoGroup": "Promo group",
"allStatuses": "All statuses",
"allTariffs": "All tariffs",
"allGroups": "All groups"
},
"statuses": {
"active": "Active",
"expired": "Expired",
"trial": "Trial",
"limited": "Limited",
"disabled": "Disabled"
},
"columns": {
"user": "User",
"status": "Status",
"tariff": "Tariff",
"balance": "Balance",
"daysRemaining": "Days",
"promoGroup": "Group",
"spent": "Spent"
}
},
"theme": {
"accentColor": "Accent color",
"customizeColors": "Customize colors",

View File

@@ -1146,7 +1146,8 @@
"salesStats": "Статистика продаж",
"landings": "Лендинги",
"referralNetwork": "Реферальная сеть",
"news": "Новости"
"news": "Новости",
"bulkActions": "Массовые действия"
},
"panel": {
"title": "Панель администратора",
@@ -3769,6 +3770,62 @@
"users": "Пользователи",
"security": "Безопасность"
},
"bulkActions": {
"title": "Массовые действия",
"subtitle": "Управление пользователями",
"selectedCount": "Выбрано: {{count}}",
"selectAll": "Выбрать всех",
"deselectAll": "Снять выделение",
"noResults": "Пользователи не найдены",
"actions": {
"extend": "Продлить подписку",
"cancel": "Деактивировать подписки",
"activate": "Активировать подписки",
"changeTariff": "Сменить тариф",
"addTraffic": "Начислить трафик",
"addBalance": "Начислить баланс",
"assignPromoGroup": "Назначить промогруппу"
},
"params": {
"days": "Количество дней",
"tariff": "Выберите тариф",
"trafficGb": "Объём трафика (ГБ)",
"balanceRub": "Сумма (руб.)",
"promoGroup": "Промогруппа",
"removePromoGroup": "Убрать промогруппу"
},
"confirm": "Применить",
"cancel": "Отмена",
"executing": "Выполнение...",
"complete": "Выполнено",
"successCount": "Успешно: {{count}}",
"errorCount": "Ошибок: {{count}}",
"filters": {
"search": "Поиск по ID, нику, email",
"status": "Статус подписки",
"tariff": "Тариф",
"promoGroup": "Промогруппа",
"allStatuses": "Все статусы",
"allTariffs": "Все тарифы",
"allGroups": "Все группы"
},
"statuses": {
"active": "Активна",
"expired": "Истекла",
"trial": "Триал",
"limited": "Ограничена",
"disabled": "Отключена"
},
"columns": {
"user": "Пользователь",
"status": "Статус",
"tariff": "Тариф",
"balance": "Баланс",
"daysRemaining": "Дни",
"promoGroup": "Группа",
"spent": "Потрачено"
}
},
"theme": {
"accentColor": "Акцентный цвет",
"customizeColors": "Настроить цвета",

File diff suppressed because it is too large Load Diff

View File

@@ -333,6 +333,12 @@ const icons = {
<path d="M15 8h-5M15 12h-5" />
</SvgIcon>
),
'list-checks': (
<SvgIcon>
<path d="M10 6h11M10 12h11M10 18h11" />
<path d="m3 6 1 1 2-2M3 12l1 1 2-2M3 18l1 1 2-2" />
</SvgIcon>
),
search: (
<SvgIcon>
<circle cx="11" cy="11" r="8" />
@@ -412,6 +418,12 @@ const sections: AdminSection[] = [
gradient: 'linear-gradient(135deg, rgb(var(--color-accent-400)), rgb(var(--color-error-400)))',
items: [
{ name: 'admin.nav.users', icon: 'users', to: '/admin/users', permission: 'users:read' },
{
name: 'admin.nav.bulkActions',
icon: 'list-checks',
to: '/admin/bulk-actions',
permission: 'users:read',
},
{
name: 'admin.nav.tickets',
icon: 'ticket',