From afffab17d316bc7c37fde10cf6c125320b7828c6 Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 29 Apr 2026 11:48:59 +0300 Subject: [PATCH] feat: bulk delete subscription protection for active paid subs Add force_delete_active_paid guard to prevent accidental deletion of active paid subscriptions. Shows warning with count and requires explicit checkbox confirmation. Also fixes allVisibleSubscriptionIds to use filteredUsers and getFilteredSubs to respect trialOnly filter on subscription sub-rows. --- src/api/adminBulkActions.ts | 1 + src/api/adminUsers.ts | 1 + src/locales/en.json | 4 +- src/locales/ru.json | 4 +- src/pages/AdminBulkActions.tsx | 205 +++++++++++++++++++++++---------- 5 files changed, 150 insertions(+), 65 deletions(-) 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({