diff --git a/src/api/adminBulkActions.ts b/src/api/adminBulkActions.ts index 43bb19f..9afcd01 100644 --- a/src/api/adminBulkActions.ts +++ b/src/api/adminBulkActions.ts @@ -32,6 +32,7 @@ export interface BulkActionParams { promo_group_id?: number | null; device_limit?: number; delete_from_panel?: boolean; + force_delete_active_paid?: boolean; } export interface BulkActionErrorItem { diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index 604bf94..c348e46 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -38,6 +38,7 @@ export interface UserListItemSubscription { tariff_id: number | null; tariff_name: string | null; status: string; + is_trial: boolean; end_date: string | null; days_remaining: number; traffic_used_gb: number; diff --git a/src/locales/en.json b/src/locales/en.json index 9b10375..607ddcc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2050,7 +2050,9 @@ }, "deleteSubscription": { "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.", + "activePaidWarning": "{{count}} of {{total}} selected subscriptions are active paid", + "forceDeleteConfirm": "I confirm deletion of active paid subscriptions" }, "deleteUser": { "warning": "Selected users will be permanently deleted from the bot! All their subscriptions, balance, and data will be lost!", diff --git a/src/locales/ru.json b/src/locales/ru.json index c381d48..7c03a0f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -3812,7 +3812,9 @@ }, "deleteSubscription": { "warning": "Выбранные подписки будут безвозвратно удалены из бота и панели RemnaWave!", - "hint": "Пользователи потеряют доступ к VPN по удалённым подпискам. Это действие нельзя отменить." + "hint": "Пользователи потеряют доступ к VPN по удалённым подпискам. Это действие нельзя отменить.", + "activePaidWarning": "{{count}} из {{total}} выбранных подписок — активные платные", + "forceDeleteConfirm": "Подтверждаю удаление активных платных подписок" }, "deleteUser": { "warning": "Выбранные пользователи будут безвозвратно удалены из бота! Все их подписки, баланс и данные будут потеряны!", diff --git a/src/pages/AdminBulkActions.tsx b/src/pages/AdminBulkActions.tsx index 0a9c286..ad5ad9a 100644 --- a/src/pages/AdminBulkActions.tsx +++ b/src/pages/AdminBulkActions.tsx @@ -741,6 +741,8 @@ interface ActionModalProps { selectedCount: number; tariffs: TariffListItem[]; promoGroups: PromoGroup[]; + users: UserListItem[]; + selectedSubscriptionIds: number[]; onClose: () => void; onExecute: (params: BulkActionParams) => void; } @@ -750,6 +752,8 @@ function ActionModal({ selectedCount, tariffs, promoGroups, + users, + selectedSubscriptionIds, onClose, onExecute, }: ActionModalProps) { @@ -763,6 +767,7 @@ function ActionModal({ const [grantDays, setGrantDays] = useState(30); const [deviceLimit, setDeviceLimit] = useState(1); const [deleteFromPanel, setDeleteFromPanel] = useState(true); + const [forceDeleteActivePaid, setForceDeleteActivePaid] = useState(false); useEffect(() => { if (tariffs.length > 0 && tariffId === 0) { @@ -779,10 +784,15 @@ function ActionModal({ } }, [promoGroups, promoGroupId]); - // Reset deleteFromPanel to default when modal opens + // Reset modal-specific state when modal opens useEffect(() => { - if (modal.open && modal.action === 'delete_user') { - setDeleteFromPanel(true); + if (modal.open) { + if (modal.action === 'delete_user') { + setDeleteFromPanel(true); + } + if (modal.action === 'delete_subscription') { + setForceDeleteActivePaid(false); + } } }, [modal.open, modal.action]); @@ -798,6 +808,25 @@ function ActionModal({ return () => document.removeEventListener('keydown', handleKeyDown); }, [modal.open, modal.loading, onClose]); + // Count active paid subscriptions among selected ones + const activePaidCount = useMemo(() => { + if (modal.action !== 'delete_subscription') return 0; + const selectedSubIdSet = new Set(selectedSubscriptionIds); + let count = 0; + for (const user of users) { + for (const sub of user.subscriptions ?? []) { + if (selectedSubIdSet.has(sub.id) && sub.status === 'active' && !sub.is_trial) { + count++; + } + } + } + return count; + }, [modal.action, selectedSubscriptionIds, users]); + + const isConfirmDisabled = + modal.loading || + (modal.action === 'delete_subscription' && activePaidCount > 0 && !forceDeleteActivePaid); + if (!modal.open || !modal.action) return null; const actionLabelKeys: Record = { @@ -840,6 +869,9 @@ function ActionModal({ case 'set_devices': params.device_limit = deviceLimit; break; + case 'delete_subscription': + params.force_delete_active_paid = forceDeleteActivePaid; + break; case 'delete_user': params.delete_from_panel = deleteFromPanel; break; @@ -982,17 +1014,56 @@ function ActionModal({ ); - case 'delete_subscription': + case 'delete_subscription': { + const totalSelected = selectedSubscriptionIds.length; + return ( -
-

- {t('admin.bulkActions.deleteSubscription.warning')} -

-

- {t('admin.bulkActions.deleteSubscription.hint')} -

+
+
+

+ {t('admin.bulkActions.deleteSubscription.warning')} +

+

+ {t('admin.bulkActions.deleteSubscription.hint')} +

+
+ {activePaidCount > 0 && ( + <> +
+

+ {t('admin.bulkActions.deleteSubscription.activePaidWarning', { + count: activePaidCount, + total: totalSelected, + })} +

+
+ + + )}
); + } case 'delete_user': return (
@@ -1158,7 +1229,7 @@ function ActionModal({