diff --git a/src/components/admin/bulkActions/FloatingActionBar.tsx b/src/components/admin/bulkActions/FloatingActionBar.tsx new file mode 100644 index 0000000..cbc7df0 --- /dev/null +++ b/src/components/admin/bulkActions/FloatingActionBar.tsx @@ -0,0 +1,304 @@ +import { useEffect, useRef, useState, type ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; +import { ChevronDownIcon } from './DropdownSelect'; +import { isSubscriptionLevelAction } from './actionTargets'; +import type { BulkActionType } from '../../../api/adminBulkActions'; + +// ────────────────────────────────────────────────────────────────── +// FloatingActionBar +// +// Bottom-dock selection toolbar for AdminBulkActions. Shows selected +// user/subscription counts, the "select-all subscriptions" toggle +// (multi-tariff only), and the action dropdown grouped into +// subscription-level vs user-level when multi-tariff is on. +// +// Pure presentational — parent owns selection state, supplies onAction +// (opens the modal with the picked action type) and onToggleAll. +// ────────────────────────────────────────────────────────────────── + +interface ActionConfig { + type: BulkActionType; + labelKey: string; + icon: ReactNode; + colorClass: string; +} + +export interface FloatingActionBarProps { + selectedUserCount: number; + selectedSubscriptionCount: number; + isMultiTariff: boolean; + totalVisibleSubscriptionCount: number; + onAction: (type: BulkActionType) => void; + onToggleAllSubscriptions: () => void; +} + +export function FloatingActionBar({ + selectedUserCount, + selectedSubscriptionCount, + isMultiTariff, + totalVisibleSubscriptionCount, + onAction, + onToggleAllSubscriptions, +}: FloatingActionBarProps) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + + const hasAnySelection = selectedUserCount > 0 || selectedSubscriptionCount > 0; + if (!hasAnySelection) return null; + + const actions: ActionConfig[] = [ + { + type: 'extend_subscription', + labelKey: 'admin.bulkActions.actions.extend', + icon: , + colorClass: 'text-success-400 hover:bg-success-500/10', + }, + { + type: 'activate_subscription', + labelKey: 'admin.bulkActions.actions.activate', + icon: , + colorClass: 'text-success-400 hover:bg-success-500/10', + }, + { + type: 'cancel_subscription', + labelKey: 'admin.bulkActions.actions.cancel', + icon: , + colorClass: 'text-error-400 hover:bg-error-500/10', + }, + { + type: 'change_tariff', + labelKey: 'admin.bulkActions.actions.changeTariff', + icon: , + colorClass: 'text-accent-400 hover:bg-accent-500/10', + }, + { + type: 'add_traffic', + labelKey: 'admin.bulkActions.actions.addTraffic', + icon: , + colorClass: 'text-accent-400 hover:bg-accent-500/10', + }, + { + type: 'set_devices', + labelKey: 'admin.bulkActions.actions.setDevices', + icon: ( + + + + ), + colorClass: 'text-accent-400 hover:bg-accent-500/10', + }, + { + type: 'delete_subscription', + labelKey: 'admin.bulkActions.actions.deleteSubscription', + icon: ( + + + + ), + colorClass: 'text-error-400 hover:bg-error-500/10', + }, + { + type: 'add_balance', + labelKey: 'admin.bulkActions.actions.addBalance', + icon: , + colorClass: 'text-warning-400 hover:bg-warning-500/10', + }, + { + type: 'grant_subscription', + labelKey: 'admin.bulkActions.actions.grantSubscription', + icon: , + colorClass: 'text-success-400 hover:bg-success-500/10', + }, + { + type: 'assign_promo_group', + labelKey: 'admin.bulkActions.actions.assignPromoGroup', + icon: , + colorClass: 'text-accent-300 hover:bg-accent-300/10', + }, + { + type: 'delete_user', + labelKey: 'admin.bulkActions.actions.deleteUser', + icon: ( + + + + ), + colorClass: 'text-error-400 hover:bg-error-500/10', + }, + ]; + + return ( +
+
+
+ {selectedUserCount > 0 && ( +
+
+ {selectedUserCount} +
+ + {t('admin.bulkActions.usersSelected', { count: selectedUserCount })} + +
+ )} + {isMultiTariff && selectedSubscriptionCount > 0 && ( +
+ {selectedUserCount > 0 &&
} +
+ {selectedSubscriptionCount} +
+ + {t('admin.bulkActions.subscriptionsSelected', { + count: selectedSubscriptionCount, + })} + +
+ )} +
+ + {isMultiTariff && totalVisibleSubscriptionCount > 0 && ( + <> +
+ + + )} + +
+ +
+ + + {open && ( +
+ {isMultiTariff && ( +
+ + {t('admin.bulkActions.subscriptionTarget')} + +
+ )} + {actions + .filter((a) => isSubscriptionLevelAction(a.type)) + .map((a) => { + const count = isMultiTariff ? selectedSubscriptionCount : selectedUserCount; + const disabled = count === 0; + return ( + + ); + })} + + {isMultiTariff && ( +
+ + {t('admin.bulkActions.userTarget')} + +
+ )} + {actions + .filter((a) => !isSubscriptionLevelAction(a.type)) + .map((a) => { + const disabled = selectedUserCount === 0; + return ( + + ); + })} +
+ )} +
+
+
+ ); +} diff --git a/src/components/admin/bulkActions/actionTargets.ts b/src/components/admin/bulkActions/actionTargets.ts new file mode 100644 index 0000000..659b3bd --- /dev/null +++ b/src/components/admin/bulkActions/actionTargets.ts @@ -0,0 +1,27 @@ +import type { BulkActionType } from '../../../api/adminBulkActions'; + +// ────────────────────────────────────────────────────────────────── +// Bulk-action target classification. +// +// Some actions operate on user-rows (assign promo group, add balance, +// delete user); others operate on individual subscription-rows +// (extend, cancel, change tariff, etc.). The set below is the source +// of truth shared between FloatingActionBar (renders two grouped +// menus when multi-tariff is on) and the parent page (gates the +// selection-count semantics and the active-paid count in delete). +// ────────────────────────────────────────────────────────────────── + +export const SUBSCRIPTION_LEVEL_ACTIONS: Set = new Set([ + 'extend_subscription', + 'add_days', + 'cancel_subscription', + 'activate_subscription', + 'change_tariff', + 'add_traffic', + 'set_devices', + 'delete_subscription', +]); + +export function isSubscriptionLevelAction(action: BulkActionType): boolean { + return SUBSCRIPTION_LEVEL_ACTIONS.has(action); +} diff --git a/src/pages/AdminBulkActions.tsx b/src/pages/AdminBulkActions.tsx index 8ffb851..2a05ec2 100644 --- a/src/pages/AdminBulkActions.tsx +++ b/src/pages/AdminBulkActions.tsx @@ -29,17 +29,14 @@ import { ChevronDownIcon, type DropdownOption, } from '@/components/admin/bulkActions/DropdownSelect'; +import { FloatingActionBar } from '@/components/admin/bulkActions/FloatingActionBar'; +import { isSubscriptionLevelAction } from '@/components/admin/bulkActions/actionTargets'; // ============ Types ============ type SubscriptionStatusFilter = '' | 'active' | 'expired' | 'trial' | 'limited' | 'disabled'; -interface ActionConfig { - type: BulkActionType; - labelKey: string; - icon: React.ReactNode; - colorClass: string; -} +// (ActionConfig moved into ./bulkActions/FloatingActionBar.tsx as a private type) // (ProgressState + ModalState moved into ; ModalState is re-exported.) @@ -116,22 +113,7 @@ const ChevronExpandIcon = ({ expanded }: { expanded: boolean }) => ( ); -// ============ Action target helpers ============ - -const SUBSCRIPTION_LEVEL_ACTIONS: Set = new Set([ - 'extend_subscription', - 'add_days', - 'cancel_subscription', - 'activate_subscription', - 'change_tariff', - 'add_traffic', - 'set_devices', - 'delete_subscription', -]); - -function isSubscriptionLevelAction(action: BulkActionType): boolean { - return SUBSCRIPTION_LEVEL_ACTIONS.has(action); -} +// (SUBSCRIPTION_LEVEL_ACTIONS + isSubscriptionLevelAction moved into ./bulkActions/actionTargets.ts) // ============ Subscription sub-row ============ @@ -531,291 +513,7 @@ function MultiSelectDropdown({ // (ActionModal moved into ./bulkActions/ActionModal.tsx) -// ============ Floating Action Bar ============ - -interface FloatingActionBarProps { - selectedUserCount: number; - selectedSubscriptionCount: number; - isMultiTariff: boolean; - totalVisibleSubscriptionCount: number; - onAction: (type: BulkActionType) => void; - onToggleAllSubscriptions: () => void; -} - -function FloatingActionBar({ - selectedUserCount, - selectedSubscriptionCount, - isMultiTariff, - totalVisibleSubscriptionCount, - onAction, - onToggleAllSubscriptions, -}: FloatingActionBarProps) { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const menuRef = useRef(null); - - useEffect(() => { - const handler = (e: MouseEvent) => { - if (menuRef.current && !menuRef.current.contains(e.target as Node)) { - setOpen(false); - } - }; - document.addEventListener('mousedown', handler); - return () => document.removeEventListener('mousedown', handler); - }, []); - - const hasAnySelection = selectedUserCount > 0 || selectedSubscriptionCount > 0; - if (!hasAnySelection) return null; - - const actions: ActionConfig[] = [ - { - type: 'extend_subscription', - labelKey: 'admin.bulkActions.actions.extend', - icon: , - colorClass: 'text-success-400 hover:bg-success-500/10', - }, - { - type: 'activate_subscription', - labelKey: 'admin.bulkActions.actions.activate', - icon: , - colorClass: 'text-success-400 hover:bg-success-500/10', - }, - { - type: 'cancel_subscription', - labelKey: 'admin.bulkActions.actions.cancel', - icon: , - colorClass: 'text-error-400 hover:bg-error-500/10', - }, - { - type: 'change_tariff', - labelKey: 'admin.bulkActions.actions.changeTariff', - icon: , - colorClass: 'text-accent-400 hover:bg-accent-500/10', - }, - { - type: 'add_traffic', - labelKey: 'admin.bulkActions.actions.addTraffic', - icon: , - colorClass: 'text-accent-400 hover:bg-accent-500/10', - }, - { - type: 'set_devices', - labelKey: 'admin.bulkActions.actions.setDevices', - icon: ( - - - - ), - colorClass: 'text-accent-400 hover:bg-accent-500/10', - }, - { - type: 'delete_subscription', - labelKey: 'admin.bulkActions.actions.deleteSubscription', - icon: ( - - - - ), - colorClass: 'text-error-400 hover:bg-error-500/10', - }, - { - type: 'add_balance', - labelKey: 'admin.bulkActions.actions.addBalance', - icon: , - colorClass: 'text-warning-400 hover:bg-warning-500/10', - }, - { - type: 'grant_subscription', - labelKey: 'admin.bulkActions.actions.grantSubscription', - icon: , - colorClass: 'text-success-400 hover:bg-success-500/10', - }, - { - type: 'assign_promo_group', - labelKey: 'admin.bulkActions.actions.assignPromoGroup', - icon: , - colorClass: 'text-accent-300 hover:bg-accent-300/10', - }, - { - type: 'delete_user', - labelKey: 'admin.bulkActions.actions.deleteUser', - icon: ( - - - - ), - colorClass: 'text-error-400 hover:bg-error-500/10', - }, - ]; - - return ( -
-
- {/* Selection counts */} -
- {selectedUserCount > 0 && ( -
-
- {selectedUserCount} -
- - {t('admin.bulkActions.usersSelected', { count: selectedUserCount })} - -
- )} - {isMultiTariff && selectedSubscriptionCount > 0 && ( -
- {selectedUserCount > 0 &&
} -
- {selectedSubscriptionCount} -
- - {t('admin.bulkActions.subscriptionsSelected', { - count: selectedSubscriptionCount, - })} - -
- )} -
- - {/* Toggle all subscriptions button */} - {isMultiTariff && totalVisibleSubscriptionCount > 0 && ( - <> -
- - - )} - -
- - {/* Actions dropdown */} -
- - - {open && ( -
- {/* Subscription-level actions */} - {isMultiTariff && ( -
- - {t('admin.bulkActions.subscriptionTarget')} - -
- )} - {actions - .filter((a) => isSubscriptionLevelAction(a.type)) - .map((a) => { - const count = isMultiTariff ? selectedSubscriptionCount : selectedUserCount; - const disabled = count === 0; - return ( - - ); - })} - - {/* User-level actions */} - {isMultiTariff && ( -
- - {t('admin.bulkActions.userTarget')} - -
- )} - {actions - .filter((a) => !isSubscriptionLevelAction(a.type)) - .map((a) => { - const disabled = selectedUserCount === 0; - return ( - - ); - })} -
- )} -
-
-
- ); -} +// (FloatingActionBar moved into ./bulkActions/FloatingActionBar.tsx) // ============ Helpers ============