mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add campaign/partner filters, delete_user action, and fix modal positioning in AdminBulkActions
Add campaign and partner filter dropdowns for user filtering, implement delete_user bulk action with delete_from_panel checkbox, fix ActionModal to use createPortal with viewport-fixed positioning and safe area insets.
This commit is contained in:
@@ -12,7 +12,8 @@ export type BulkActionType =
|
|||||||
| 'assign_promo_group'
|
| 'assign_promo_group'
|
||||||
| 'grant_subscription'
|
| 'grant_subscription'
|
||||||
| 'set_devices'
|
| 'set_devices'
|
||||||
| 'delete_subscription';
|
| 'delete_subscription'
|
||||||
|
| 'delete_user';
|
||||||
|
|
||||||
export interface BulkActionRequest {
|
export interface BulkActionRequest {
|
||||||
action: BulkActionType;
|
action: BulkActionType;
|
||||||
@@ -30,6 +31,7 @@ export interface BulkActionParams {
|
|||||||
balance_description?: string;
|
balance_description?: string;
|
||||||
promo_group_id?: number | null;
|
promo_group_id?: number | null;
|
||||||
device_limit?: number;
|
device_limit?: number;
|
||||||
|
delete_from_panel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BulkActionErrorItem {
|
export interface BulkActionErrorItem {
|
||||||
|
|||||||
@@ -411,6 +411,11 @@ export const adminUsersApi = {
|
|||||||
search?: string;
|
search?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
status?: 'active' | 'blocked' | 'deleted';
|
status?: 'active' | 'blocked' | 'deleted';
|
||||||
|
subscription_status?: string;
|
||||||
|
tariff_id?: string;
|
||||||
|
promo_group_id?: number;
|
||||||
|
campaign_id?: number;
|
||||||
|
partner_id?: number;
|
||||||
sort_by?:
|
sort_by?:
|
||||||
| 'created_at'
|
| 'created_at'
|
||||||
| 'balance'
|
| 'balance'
|
||||||
|
|||||||
@@ -2043,12 +2043,18 @@
|
|||||||
"assignPromoGroup": "Assign promo group",
|
"assignPromoGroup": "Assign promo group",
|
||||||
"grantSubscription": "Grant subscription",
|
"grantSubscription": "Grant subscription",
|
||||||
"setDevices": "Set devices",
|
"setDevices": "Set devices",
|
||||||
"deleteSubscription": "Delete subscriptions"
|
"deleteSubscription": "Delete subscriptions",
|
||||||
|
"deleteUser": "Delete users"
|
||||||
},
|
},
|
||||||
"deleteSubscription": {
|
"deleteSubscription": {
|
||||||
"warning": "Selected subscriptions will be permanently deleted from the bot and RemnaWave panel!",
|
"warning": "Selected subscriptions will be permanently deleted from the bot and RemnaWave panel!",
|
||||||
"hint": "Users will lose VPN access for deleted subscriptions. This action cannot be undone."
|
"hint": "Users will lose VPN access for deleted subscriptions. This action cannot be undone."
|
||||||
},
|
},
|
||||||
|
"deleteUser": {
|
||||||
|
"warning": "Selected users will be permanently deleted from the bot! All their subscriptions, balance, and data will be lost!",
|
||||||
|
"hint": "This action cannot be undone. Users will need to restart the bot to create a new account.",
|
||||||
|
"deleteFromPanel": "Also delete from RemnaWave panel"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"days": "Number of days",
|
"days": "Number of days",
|
||||||
"tariff": "Select tariff",
|
"tariff": "Select tariff",
|
||||||
@@ -2088,6 +2094,8 @@
|
|||||||
"allStatuses": "All statuses",
|
"allStatuses": "All statuses",
|
||||||
"allTariffs": "All tariffs",
|
"allTariffs": "All tariffs",
|
||||||
"allGroups": "All groups",
|
"allGroups": "All groups",
|
||||||
|
"allCampaigns": "All campaigns",
|
||||||
|
"allPartners": "All partners",
|
||||||
"trialOnly": "Trial only",
|
"trialOnly": "Trial only",
|
||||||
"tariffsSelected": "{{count}} tariffs",
|
"tariffsSelected": "{{count}} tariffs",
|
||||||
"selectAll": "Select all",
|
"selectAll": "Select all",
|
||||||
|
|||||||
@@ -3437,6 +3437,113 @@
|
|||||||
"zh": "چینی",
|
"zh": "چینی",
|
||||||
"fa": "فارسی"
|
"fa": "فارسی"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"bulkActions": {
|
||||||
|
"title": "عملیات دستهای",
|
||||||
|
"subtitle": "مدیریت کاربران",
|
||||||
|
"selectedCount": "انتخاب شده: {{count}}",
|
||||||
|
"selectAll": "انتخاب همه",
|
||||||
|
"deselectAll": "لغو انتخاب همه",
|
||||||
|
"selectUser": "انتخاب کاربر {{name}}",
|
||||||
|
"deselectUser": "لغو انتخاب کاربر {{name}}",
|
||||||
|
"noResults": "کاربری یافت نشد",
|
||||||
|
"perPage": "در هر صفحه",
|
||||||
|
"actions": {
|
||||||
|
"extend": "تمدید اشتراک",
|
||||||
|
"cancel": "غیرفعالسازی اشتراکها",
|
||||||
|
"activate": "فعالسازی اشتراکها",
|
||||||
|
"changeTariff": "تغییر تعرفه",
|
||||||
|
"addTraffic": "افزودن ترافیک",
|
||||||
|
"addBalance": "افزودن موجودی",
|
||||||
|
"assignPromoGroup": "اختصاص گروه تبلیغاتی",
|
||||||
|
"grantSubscription": "اعطای اشتراک",
|
||||||
|
"setDevices": "تنظیم دستگاهها",
|
||||||
|
"deleteSubscription": "حذف اشتراکها",
|
||||||
|
"deleteUser": "حذف کاربران"
|
||||||
|
},
|
||||||
|
"deleteSubscription": {
|
||||||
|
"warning": "اشتراکهای انتخاب شده برای همیشه از ربات و پنل RemnaWave حذف خواهند شد!",
|
||||||
|
"hint": "کاربران دسترسی VPN اشتراکهای حذف شده را از دست خواهند داد. این عملیات قابل بازگشت نیست."
|
||||||
|
},
|
||||||
|
"deleteUser": {
|
||||||
|
"warning": "کاربران انتخاب شده برای همیشه از ربات حذف خواهند شد! تمام اشتراکها، موجودی و دادههای آنها از بین خواهد رفت!",
|
||||||
|
"hint": "این عملیات قابل بازگشت نیست. کاربران باید ربات را دوباره راهاندازی کنند تا حساب جدید بسازند.",
|
||||||
|
"deleteFromPanel": "همچنین از پنل RemnaWave حذف شود"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"days": "تعداد روز",
|
||||||
|
"tariff": "انتخاب تعرفه",
|
||||||
|
"trafficGb": "حجم ترافیک (GB)",
|
||||||
|
"balanceRub": "مبلغ (روبل)",
|
||||||
|
"promoGroup": "گروه تبلیغاتی",
|
||||||
|
"removePromoGroup": "حذف گروه تبلیغاتی",
|
||||||
|
"deviceLimit": "تعداد دستگاه"
|
||||||
|
},
|
||||||
|
"grantSubscription": {
|
||||||
|
"warning": "کاربرانی که قبلا اشتراک تعرفه انتخاب شده را دارند رد خواهند شد"
|
||||||
|
},
|
||||||
|
"confirm": "اعمال",
|
||||||
|
"cancel": "انصراف",
|
||||||
|
"executing": "در حال اجرا...",
|
||||||
|
"complete": "انجام شد",
|
||||||
|
"successCount": "موفق: {{count}}",
|
||||||
|
"errorCount": "خطا: {{count}}",
|
||||||
|
"progress": {
|
||||||
|
"processed": "پردازش شده: {{current}} / {{total}}",
|
||||||
|
"succeeded": "موفق",
|
||||||
|
"failed": "ناموفق",
|
||||||
|
"ok": "OK",
|
||||||
|
"errorGeneric": "خطا",
|
||||||
|
"doNotClose": "لطفا تا پایان عملیات این پنجره را نبندید",
|
||||||
|
"summarySuccess": "{{count}} موفق",
|
||||||
|
"summaryErrors": "{{count}} خطا"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"title": "{{count}} خطا — نمایش جزئیات"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"search": "جستجو بر اساس شناسه، نام کاربری، ایمیل",
|
||||||
|
"status": "وضعیت اشتراک",
|
||||||
|
"tariff": "تعرفه",
|
||||||
|
"promoGroup": "گروه تبلیغاتی",
|
||||||
|
"allStatuses": "همه وضعیتها",
|
||||||
|
"allTariffs": "همه تعرفهها",
|
||||||
|
"allGroups": "همه گروهها",
|
||||||
|
"allCampaigns": "همه کمپینها",
|
||||||
|
"allPartners": "همه شرکا",
|
||||||
|
"trialOnly": "فقط آزمایشی",
|
||||||
|
"tariffsSelected": "{{count}} تعرفه",
|
||||||
|
"selectAll": "انتخاب همه",
|
||||||
|
"deselectAll": "لغو انتخاب همه"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"active": "فعال",
|
||||||
|
"expired": "منقضی",
|
||||||
|
"trial": "آزمایشی",
|
||||||
|
"limited": "محدود",
|
||||||
|
"disabled": "غیرفعال"
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"user": "کاربر",
|
||||||
|
"status": "وضعیت",
|
||||||
|
"tariff": "تعرفه",
|
||||||
|
"balance": "موجودی",
|
||||||
|
"daysRemaining": "روز",
|
||||||
|
"promoGroup": "گروه",
|
||||||
|
"spent": "خرج شده"
|
||||||
|
},
|
||||||
|
"subscriptionsSelected": "{{count}} اشتراک انتخاب شده",
|
||||||
|
"usersSelected": "{{count}} کاربر انتخاب شده",
|
||||||
|
"expandSubscriptions": "نمایش اشتراکها",
|
||||||
|
"collapseSubscriptions": "پنهان کردن اشتراکها",
|
||||||
|
"noSubscriptions": "بدون اشتراک",
|
||||||
|
"subscriptionTarget": "هدف: اشتراکها",
|
||||||
|
"userTarget": "هدف: کاربران",
|
||||||
|
"daysUnit": "روز",
|
||||||
|
"trafficOf": "از",
|
||||||
|
"trafficGbUnit": "GB",
|
||||||
|
"selectAllSubs": "انتخاب همه اشتراکها",
|
||||||
|
"deselectAllSubs": "لغو انتخاب همه اشتراکها"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"adminUpdates": {
|
"adminUpdates": {
|
||||||
|
|||||||
@@ -3791,12 +3791,18 @@
|
|||||||
"assignPromoGroup": "Назначить промогруппу",
|
"assignPromoGroup": "Назначить промогруппу",
|
||||||
"grantSubscription": "Выдать подписку",
|
"grantSubscription": "Выдать подписку",
|
||||||
"setDevices": "Изменить устройства",
|
"setDevices": "Изменить устройства",
|
||||||
"deleteSubscription": "Удалить подписки"
|
"deleteSubscription": "Удалить подписки",
|
||||||
|
"deleteUser": "Удалить пользователей"
|
||||||
},
|
},
|
||||||
"deleteSubscription": {
|
"deleteSubscription": {
|
||||||
"warning": "Выбранные подписки будут безвозвратно удалены из бота и панели RemnaWave!",
|
"warning": "Выбранные подписки будут безвозвратно удалены из бота и панели RemnaWave!",
|
||||||
"hint": "Пользователи потеряют доступ к VPN по удалённым подпискам. Это действие нельзя отменить."
|
"hint": "Пользователи потеряют доступ к VPN по удалённым подпискам. Это действие нельзя отменить."
|
||||||
},
|
},
|
||||||
|
"deleteUser": {
|
||||||
|
"warning": "Выбранные пользователи будут безвозвратно удалены из бота! Все их подписки, баланс и данные будут потеряны!",
|
||||||
|
"hint": "Это действие нельзя отменить. Пользователям придётся заново запустить бота для создания нового аккаунта.",
|
||||||
|
"deleteFromPanel": "Также удалить из панели RemnaWave"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"days": "Количество дней",
|
"days": "Количество дней",
|
||||||
"tariff": "Выберите тариф",
|
"tariff": "Выберите тариф",
|
||||||
@@ -3836,6 +3842,8 @@
|
|||||||
"allStatuses": "Все статусы",
|
"allStatuses": "Все статусы",
|
||||||
"allTariffs": "Все тарифы",
|
"allTariffs": "Все тарифы",
|
||||||
"allGroups": "Все группы",
|
"allGroups": "Все группы",
|
||||||
|
"allCampaigns": "Все кампании",
|
||||||
|
"allPartners": "Все партнёры",
|
||||||
"trialOnly": "Только триал",
|
"trialOnly": "Только триал",
|
||||||
"tariffsSelected": "{{count}} тарифов",
|
"tariffsSelected": "{{count}} тарифов",
|
||||||
"selectAll": "Выбрать все",
|
"selectAll": "Выбрать все",
|
||||||
|
|||||||
@@ -3436,6 +3436,113 @@
|
|||||||
"zh": "中文",
|
"zh": "中文",
|
||||||
"fa": "波斯语"
|
"fa": "波斯语"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"bulkActions": {
|
||||||
|
"title": "批量操作",
|
||||||
|
"subtitle": "用户管理",
|
||||||
|
"selectedCount": "已选择: {{count}}",
|
||||||
|
"selectAll": "全选",
|
||||||
|
"deselectAll": "取消全选",
|
||||||
|
"selectUser": "选择用户 {{name}}",
|
||||||
|
"deselectUser": "取消选择用户 {{name}}",
|
||||||
|
"noResults": "未找到用户",
|
||||||
|
"perPage": "每页",
|
||||||
|
"actions": {
|
||||||
|
"extend": "延长订阅",
|
||||||
|
"cancel": "停用订阅",
|
||||||
|
"activate": "激活订阅",
|
||||||
|
"changeTariff": "更改套餐",
|
||||||
|
"addTraffic": "添加流量",
|
||||||
|
"addBalance": "添加余额",
|
||||||
|
"assignPromoGroup": "分配促销组",
|
||||||
|
"grantSubscription": "授予订阅",
|
||||||
|
"setDevices": "设置设备数",
|
||||||
|
"deleteSubscription": "删除订阅",
|
||||||
|
"deleteUser": "删除用户"
|
||||||
|
},
|
||||||
|
"deleteSubscription": {
|
||||||
|
"warning": "选中的订阅将从机器人和RemnaWave面板中永久删除!",
|
||||||
|
"hint": "用户将失去已删除订阅的VPN访问权限。此操作无法撤销。"
|
||||||
|
},
|
||||||
|
"deleteUser": {
|
||||||
|
"warning": "选中的用户将从机器人中永久删除!他们的所有订阅、余额和数据将丢失!",
|
||||||
|
"hint": "此操作无法撤销。用户需要重新启动机器人以创建新账户。",
|
||||||
|
"deleteFromPanel": "同时从RemnaWave面板删除"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"days": "天数",
|
||||||
|
"tariff": "选择套餐",
|
||||||
|
"trafficGb": "流量 (GB)",
|
||||||
|
"balanceRub": "金额 (卢布)",
|
||||||
|
"promoGroup": "促销组",
|
||||||
|
"removePromoGroup": "移除促销组",
|
||||||
|
"deviceLimit": "设备数量"
|
||||||
|
},
|
||||||
|
"grantSubscription": {
|
||||||
|
"warning": "已拥有所选套餐订阅的用户将被跳过"
|
||||||
|
},
|
||||||
|
"confirm": "应用",
|
||||||
|
"cancel": "取消",
|
||||||
|
"executing": "执行中...",
|
||||||
|
"complete": "完成",
|
||||||
|
"successCount": "成功: {{count}}",
|
||||||
|
"errorCount": "错误: {{count}}",
|
||||||
|
"progress": {
|
||||||
|
"processed": "已处理: {{current}} / {{total}}",
|
||||||
|
"succeeded": "成功",
|
||||||
|
"failed": "失败",
|
||||||
|
"ok": "OK",
|
||||||
|
"errorGeneric": "错误",
|
||||||
|
"doNotClose": "操作进行中请勿关闭此窗口",
|
||||||
|
"summarySuccess": "{{count}} 成功",
|
||||||
|
"summaryErrors": "{{count}} 错误"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"title": "{{count}} 个错误 — 显示详情"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"search": "按ID、用户名、邮箱搜索",
|
||||||
|
"status": "订阅状态",
|
||||||
|
"tariff": "套餐",
|
||||||
|
"promoGroup": "促销组",
|
||||||
|
"allStatuses": "所有状态",
|
||||||
|
"allTariffs": "所有套餐",
|
||||||
|
"allGroups": "所有组",
|
||||||
|
"allCampaigns": "所有活动",
|
||||||
|
"allPartners": "所有合作伙伴",
|
||||||
|
"trialOnly": "仅试用",
|
||||||
|
"tariffsSelected": "{{count}} 个套餐",
|
||||||
|
"selectAll": "全选",
|
||||||
|
"deselectAll": "取消全选"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"active": "活跃",
|
||||||
|
"expired": "已过期",
|
||||||
|
"trial": "试用",
|
||||||
|
"limited": "受限",
|
||||||
|
"disabled": "已禁用"
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"user": "用户",
|
||||||
|
"status": "状态",
|
||||||
|
"tariff": "套餐",
|
||||||
|
"balance": "余额",
|
||||||
|
"daysRemaining": "天数",
|
||||||
|
"promoGroup": "组",
|
||||||
|
"spent": "已消费"
|
||||||
|
},
|
||||||
|
"subscriptionsSelected": "已选择 {{count}} 个订阅",
|
||||||
|
"usersSelected": "已选择 {{count}} 个用户",
|
||||||
|
"expandSubscriptions": "显示订阅",
|
||||||
|
"collapseSubscriptions": "隐藏订阅",
|
||||||
|
"noSubscriptions": "无订阅",
|
||||||
|
"subscriptionTarget": "目标: 订阅",
|
||||||
|
"userTarget": "目标: 用户",
|
||||||
|
"daysUnit": "天",
|
||||||
|
"trafficOf": "/",
|
||||||
|
"trafficGbUnit": "GB",
|
||||||
|
"selectAllSubs": "选择所有订阅",
|
||||||
|
"deselectAllSubs": "取消选择所有订阅"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"adminUpdates": {
|
"adminUpdates": {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
import { adminUsersApi, type UserListItem, type UserListItemSubscription } from '../api/adminUsers';
|
import { adminUsersApi, type UserListItem, type UserListItemSubscription } from '../api/adminUsers';
|
||||||
import { tariffsApi, type TariffListItem } from '../api/tariffs';
|
import { tariffsApi, type TariffListItem } from '../api/tariffs';
|
||||||
import { promocodesApi, type PromoGroup } from '../api/promocodes';
|
import { promocodesApi, type PromoGroup } from '../api/promocodes';
|
||||||
|
import { campaignsApi, type CampaignListItem } from '../api/campaigns';
|
||||||
|
import { partnerApi, type AdminPartnerItem } from '../api/partners';
|
||||||
import {
|
import {
|
||||||
adminBulkActionsApi,
|
adminBulkActionsApi,
|
||||||
type BulkActionType,
|
type BulkActionType,
|
||||||
@@ -760,6 +762,7 @@ function ActionModal({
|
|||||||
const [grantTariffId, setGrantTariffId] = useState<number>(tariffs[0]?.id ?? 0);
|
const [grantTariffId, setGrantTariffId] = useState<number>(tariffs[0]?.id ?? 0);
|
||||||
const [grantDays, setGrantDays] = useState(30);
|
const [grantDays, setGrantDays] = useState(30);
|
||||||
const [deviceLimit, setDeviceLimit] = useState(1);
|
const [deviceLimit, setDeviceLimit] = useState(1);
|
||||||
|
const [deleteFromPanel, setDeleteFromPanel] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tariffs.length > 0 && tariffId === 0) {
|
if (tariffs.length > 0 && tariffId === 0) {
|
||||||
@@ -802,6 +805,7 @@ function ActionModal({
|
|||||||
grant_subscription: 'admin.bulkActions.actions.grantSubscription',
|
grant_subscription: 'admin.bulkActions.actions.grantSubscription',
|
||||||
set_devices: 'admin.bulkActions.actions.setDevices',
|
set_devices: 'admin.bulkActions.actions.setDevices',
|
||||||
delete_subscription: 'admin.bulkActions.actions.deleteSubscription',
|
delete_subscription: 'admin.bulkActions.actions.deleteSubscription',
|
||||||
|
delete_user: 'admin.bulkActions.actions.deleteUser',
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
@@ -829,6 +833,9 @@ function ActionModal({
|
|||||||
case 'set_devices':
|
case 'set_devices':
|
||||||
params.device_limit = deviceLimit;
|
params.device_limit = deviceLimit;
|
||||||
break;
|
break;
|
||||||
|
case 'delete_user':
|
||||||
|
params.delete_from_panel = deleteFromPanel;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
onExecute(params);
|
onExecute(params);
|
||||||
};
|
};
|
||||||
@@ -979,14 +986,55 @@ function ActionModal({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
case 'delete_user':
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="rounded-xl border border-error-500/20 bg-error-500/5 px-3 py-2.5">
|
||||||
|
<p className="text-sm font-medium text-error-400">
|
||||||
|
{t('admin.bulkActions.deleteUser.warning')}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-xs text-error-300/70">
|
||||||
|
{t('admin.bulkActions.deleteUser.hint')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setDeleteFromPanel((prev) => !prev)}
|
||||||
|
className={cn(
|
||||||
|
'flex h-5 w-5 items-center justify-center rounded-md border-2 transition-all duration-150',
|
||||||
|
deleteFromPanel
|
||||||
|
? 'border-error-500 bg-error-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]'
|
||||||
|
: 'border-dark-500 bg-dark-700/60 hover:border-error-500/50 hover:bg-dark-600/60',
|
||||||
|
)}
|
||||||
|
aria-pressed={deleteFromPanel}
|
||||||
|
>
|
||||||
|
{deleteFromPanel && <CheckIcon />}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-sm',
|
||||||
|
deleteFromPanel ? 'font-medium text-error-400' : 'text-dark-400',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t('admin.bulkActions.deleteUser.deleteFromPanel')}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
className="fixed inset-0 z-[9999] flex items-center justify-center"
|
||||||
|
style={{
|
||||||
|
paddingTop: 'max(1rem, env(safe-area-inset-top))',
|
||||||
|
paddingBottom: 'max(1rem, env(safe-area-inset-bottom))',
|
||||||
|
paddingLeft: 'max(1rem, env(safe-area-inset-left))',
|
||||||
|
paddingRight: 'max(1rem, env(safe-area-inset-right))',
|
||||||
|
}}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
>
|
>
|
||||||
@@ -997,7 +1045,7 @@ function ActionModal({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Modal */}
|
{/* Modal */}
|
||||||
<div className="relative w-full max-w-md rounded-2xl border border-dark-700 bg-dark-900 p-6 shadow-2xl">
|
<div className="relative max-h-[calc(100dvh-2rem)] w-full max-w-md overflow-y-auto rounded-2xl border border-dark-700 bg-dark-900 p-6 shadow-2xl">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-5 flex items-center justify-between">
|
<div className="mb-5 flex items-center justify-between">
|
||||||
<h3 className="text-lg font-bold text-dark-100">{t(actionLabelKeys[modal.action])}</h3>
|
<h3 className="text-lg font-bold text-dark-100">{t(actionLabelKeys[modal.action])}</h3>
|
||||||
@@ -1112,7 +1160,8 @@ function ActionModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1241,6 +1290,26 @@ function FloatingActionBar({
|
|||||||
icon: <span aria-hidden="true">%</span>,
|
icon: <span aria-hidden="true">%</span>,
|
||||||
colorClass: 'text-accent-300 hover:bg-accent-300/10',
|
colorClass: 'text-accent-300 hover:bg-accent-300/10',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'delete_user',
|
||||||
|
labelKey: 'admin.bulkActions.actions.deleteUser',
|
||||||
|
icon: (
|
||||||
|
<svg
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M22 10.5h-6m-8.25-4.5a3.75 3.75 0 117.5 0 3.75 3.75 0 01-7.5 0zM1.5 21a8.25 8.25 0 0115 0"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
colorClass: 'text-error-400 hover:bg-error-500/10',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1397,6 +1466,8 @@ export default function AdminBulkActions() {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [tariffs, setTariffs] = useState<TariffListItem[]>([]);
|
const [tariffs, setTariffs] = useState<TariffListItem[]>([]);
|
||||||
const [promoGroups, setPromoGroups] = useState<PromoGroup[]>([]);
|
const [promoGroups, setPromoGroups] = useState<PromoGroup[]>([]);
|
||||||
|
const [campaigns, setCampaigns] = useState<CampaignListItem[]>([]);
|
||||||
|
const [partners, setPartners] = useState<AdminPartnerItem[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
@@ -1406,6 +1477,8 @@ export default function AdminBulkActions() {
|
|||||||
const [trialOnly, setTrialOnly] = useState(false);
|
const [trialOnly, setTrialOnly] = useState(false);
|
||||||
const [tariffFilter, setTariffFilter] = useState<number[]>([]);
|
const [tariffFilter, setTariffFilter] = useState<number[]>([]);
|
||||||
const [promoGroupFilter, setPromoGroupFilter] = useState('');
|
const [promoGroupFilter, setPromoGroupFilter] = useState('');
|
||||||
|
const [campaignFilter, setCampaignFilter] = useState('');
|
||||||
|
const [partnerFilter, setPartnerFilter] = useState('');
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
@@ -1447,6 +1520,8 @@ export default function AdminBulkActions() {
|
|||||||
if (statusFilter) params.subscription_status = statusFilter;
|
if (statusFilter) params.subscription_status = statusFilter;
|
||||||
if (tariffFilter.length > 0) params.tariff_id = tariffFilter.join(',');
|
if (tariffFilter.length > 0) params.tariff_id = tariffFilter.join(',');
|
||||||
if (promoGroupFilter) params.promo_group_id = Number(promoGroupFilter);
|
if (promoGroupFilter) params.promo_group_id = Number(promoGroupFilter);
|
||||||
|
if (campaignFilter) params.campaign_id = Number(campaignFilter);
|
||||||
|
if (partnerFilter) params.partner_id = Number(partnerFilter);
|
||||||
|
|
||||||
const data = await adminUsersApi.getUsers(
|
const data = await adminUsersApi.getUsers(
|
||||||
params as Parameters<typeof adminUsersApi.getUsers>[0],
|
params as Parameters<typeof adminUsersApi.getUsers>[0],
|
||||||
@@ -1466,22 +1541,35 @@ export default function AdminBulkActions() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [offset, limit, committedSearch, statusFilter, tariffFilter, promoGroupFilter]);
|
}, [
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
committedSearch,
|
||||||
|
statusFilter,
|
||||||
|
tariffFilter,
|
||||||
|
promoGroupFilter,
|
||||||
|
campaignFilter,
|
||||||
|
partnerFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
}, [loadUsers]);
|
}, [loadUsers]);
|
||||||
|
|
||||||
// Load tariffs and promo groups once
|
// Load tariffs, promo groups, campaigns, and partners once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const [tariffData, pgData] = await Promise.all([
|
const [tariffData, pgData, campaignData, partnerData] = await Promise.all([
|
||||||
tariffsApi.getTariffs(true),
|
tariffsApi.getTariffs(true),
|
||||||
promocodesApi.getPromoGroups({ limit: 200 }),
|
promocodesApi.getPromoGroups({ limit: 200 }),
|
||||||
|
campaignsApi.getCampaigns(true, 0, 200),
|
||||||
|
partnerApi.getPartners({ limit: 200 }),
|
||||||
]);
|
]);
|
||||||
setTariffs(tariffData.tariffs);
|
setTariffs(tariffData.tariffs);
|
||||||
setPromoGroups(pgData.items);
|
setPromoGroups(pgData.items);
|
||||||
|
setCampaigns(campaignData.campaigns);
|
||||||
|
setPartners(partnerData.items);
|
||||||
} catch {
|
} catch {
|
||||||
// silently fail
|
// silently fail
|
||||||
}
|
}
|
||||||
@@ -1533,6 +1621,16 @@ export default function AdminBulkActions() {
|
|||||||
setOffset(0);
|
setOffset(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCampaignFilterChange = (v: string) => {
|
||||||
|
setCampaignFilter(v);
|
||||||
|
setOffset(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePartnerFilterChange = (v: string) => {
|
||||||
|
setPartnerFilter(v);
|
||||||
|
setOffset(0);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
};
|
};
|
||||||
@@ -2017,6 +2115,19 @@ export default function AdminBulkActions() {
|
|||||||
...promoGroups.map((pg) => ({ value: String(pg.id), label: pg.name })),
|
...promoGroups.map((pg) => ({ value: String(pg.id), label: pg.name })),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const campaignOptions: DropdownOption[] = [
|
||||||
|
{ value: '', label: t('admin.bulkActions.filters.allCampaigns') },
|
||||||
|
...campaigns.map((c) => ({ value: String(c.id), label: c.name })),
|
||||||
|
];
|
||||||
|
|
||||||
|
const partnerOptions: DropdownOption[] = [
|
||||||
|
{ value: '', label: t('admin.bulkActions.filters.allPartners') },
|
||||||
|
...partners.map((p) => ({
|
||||||
|
value: String(p.user_id),
|
||||||
|
label: p.username ? `@${p.username}` : p.first_name || String(p.telegram_id),
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative animate-fade-in">
|
<div className="relative animate-fade-in">
|
||||||
<ProgressBar loading={loading} />
|
<ProgressBar loading={loading} />
|
||||||
@@ -2091,6 +2202,20 @@ export default function AdminBulkActions() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Campaign & Partner filters */}
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<DropdownSelect
|
||||||
|
value={campaignFilter}
|
||||||
|
options={campaignOptions}
|
||||||
|
onChange={handleCampaignFilterChange}
|
||||||
|
/>
|
||||||
|
<DropdownSelect
|
||||||
|
value={partnerFilter}
|
||||||
|
options={partnerOptions}
|
||||||
|
onChange={handlePartnerFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Trial filter checkbox */}
|
{/* Trial filter checkbox */}
|
||||||
<label className="inline-flex cursor-pointer items-center gap-2">
|
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user