Files
pragma-stack/frontend/src/components/admin/users/BulkActionToolbar.tsx
Felipe Cardoso 91bc4f190d Introduce comprehensive user management functionality for admin
- Added React Query hooks for user-related actions: `useCreateUser`, `useUpdateUser`, `useDeleteUser`, `useActivateUser`, `useDeactivateUser`, and `useBulkUserAction`.
- Implemented primary user management components: `UserFormDialog`, `UserManagementContent`, `UserListTable`, `BulkActionToolbar`, and `UserActionMenu`.
- Replaced placeholder content on the Users page with full user management capabilities.
- Included role-based validation, search, pagination, filtering, and bulk operations.
- Enhanced form validation with `zod` schema for robust user input handling.
- Added feedback mechanisms (toasts and alert dialogs) for user actions.
- Improved UI accessibility and usability across the admin user management feature.
2025-11-06 12:08:10 +01:00

187 lines
5.4 KiB
TypeScript

/**
* BulkActionToolbar Component
* Toolbar for performing bulk actions on selected users
*/
'use client';
import { useState } from 'react';
import { CheckCircle, XCircle, Trash, X } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { toast } from 'sonner';
import { useBulkUserAction } from '@/lib/api/hooks/useAdmin';
interface BulkActionToolbarProps {
selectedCount: number;
onClearSelection: () => void;
selectedUserIds: string[];
}
type BulkAction = 'activate' | 'deactivate' | 'delete' | null;
export function BulkActionToolbar({
selectedCount,
onClearSelection,
selectedUserIds,
}: BulkActionToolbarProps) {
const [pendingAction, setPendingAction] = useState<BulkAction>(null);
const bulkAction = useBulkUserAction();
if (selectedCount === 0) {
return null;
}
const handleAction = (action: BulkAction) => {
setPendingAction(action);
};
const confirmAction = async () => {
if (!pendingAction) return;
try {
await bulkAction.mutateAsync({
action: pendingAction,
userIds: selectedUserIds,
});
toast.success(
`Successfully ${pendingAction}d ${selectedCount} user${selectedCount > 1 ? 's' : ''}`
);
onClearSelection();
setPendingAction(null);
} catch (error) {
toast.error(
error instanceof Error ? error.message : `Failed to ${pendingAction} users`
);
setPendingAction(null);
}
};
const cancelAction = () => {
setPendingAction(null);
};
const getActionDescription = () => {
switch (pendingAction) {
case 'activate':
return `Are you sure you want to activate ${selectedCount} user${selectedCount > 1 ? 's' : ''}? They will be able to log in.`;
case 'deactivate':
return `Are you sure you want to deactivate ${selectedCount} user${selectedCount > 1 ? 's' : ''}? They will not be able to log in until reactivated.`;
case 'delete':
return `Are you sure you want to delete ${selectedCount} user${selectedCount > 1 ? 's' : ''}? This action cannot be undone.`;
default:
return '';
}
};
const getActionTitle = () => {
switch (pendingAction) {
case 'activate':
return 'Activate Users';
case 'deactivate':
return 'Deactivate Users';
case 'delete':
return 'Delete Users';
default:
return '';
}
};
return (
<>
<div
className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50"
data-testid="bulk-action-toolbar"
>
<div className="bg-background border rounded-lg shadow-lg p-4 flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">
{selectedCount} user{selectedCount > 1 ? 's' : ''} selected
</span>
<Button
variant="ghost"
size="sm"
onClick={onClearSelection}
aria-label="Clear selection"
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="h-6 w-px bg-border" />
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleAction('activate')}
disabled={bulkAction.isPending}
>
<CheckCircle className="mr-2 h-4 w-4" />
Activate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleAction('deactivate')}
disabled={bulkAction.isPending}
>
<XCircle className="mr-2 h-4 w-4" />
Deactivate
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleAction('delete')}
disabled={bulkAction.isPending}
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
>
<Trash className="mr-2 h-4 w-4" />
Delete
</Button>
</div>
</div>
</div>
{/* Confirmation Dialog */}
<AlertDialog open={!!pendingAction} onOpenChange={() => cancelAction()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{getActionTitle()}</AlertDialogTitle>
<AlertDialogDescription>
{getActionDescription()}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmAction}
className={
pendingAction === 'delete'
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: ''
}
>
{pendingAction === 'activate' && 'Activate'}
{pendingAction === 'deactivate' && 'Deactivate'}
{pendingAction === 'delete' && 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}