Files
eventify_command_center/src/features/users/components/BulkActionBar.tsx

147 lines
6.3 KiB
TypeScript

import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Tooltip, TooltipContent, TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Shield, Tag, Mail, Download, Trash2, X,
CheckCircle2, Ban, Loader2,
} from 'lucide-react';
import { toast } from 'sonner';
import { cn } from '@/lib/utils';
import { bulkExportUsers, bulkBanUsers, bulkDeleteUsers, bulkVerifyUsers } from '@/lib/actions/bulk-users';
import type { User } from '@/lib/types/user';
interface BulkActionBarProps {
selectedUsers: User[];
onClearSelection: () => void;
onOpenSuspendDialog: () => void;
onOpenTagDialog: () => void;
onOpenEmailComposer: () => void;
onComplete: () => void;
}
export function BulkActionBar({
selectedUsers,
onClearSelection,
onOpenSuspendDialog,
onOpenTagDialog,
onOpenEmailComposer,
onComplete,
}: BulkActionBarProps) {
const [loadingAction, setLoadingAction] = useState<string | null>(null);
const count = selectedUsers.length;
if (count === 0) return null;
const userIds = selectedUsers.map(u => u.id);
const runAction = async (key: string, action: () => Promise<{ success: boolean; message: string }>) => {
setLoadingAction(key);
try {
const res = await action();
if (res.success) {
toast.success(res.message);
onClearSelection();
onComplete();
} else {
toast.error(res.message);
}
} catch {
toast.error('Action failed');
} finally {
setLoadingAction(null);
}
};
const handleExport = () => {
runAction('export', async () => {
const res = await bulkExportUsers(userIds);
if (res.success && res.csvData) {
// Trigger download
const blob = new Blob([res.csvData], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `users_export_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
}
return res;
});
};
const handleBan = () => {
if (!confirm(`Permanently ban ${count} user(s)? This is a severe action.`)) return;
runAction('ban', () => bulkBanUsers(userIds, { reason: 'Bulk ban from action bar' }));
};
const handleDelete = () => {
if (!confirm(`Permanently delete ${count} user(s)? This CANNOT be undone.`)) return;
runAction('delete', () => bulkDeleteUsers(userIds));
};
const handleVerify = () => {
runAction('verify', () => bulkVerifyUsers(userIds));
};
const actions = [
{ key: 'suspend', icon: Shield, label: 'Suspend', color: 'text-orange-500 hover:bg-orange-500/10', onClick: onOpenSuspendDialog },
{ key: 'ban', icon: Ban, label: 'Ban', color: 'text-red-500 hover:bg-red-500/10', onClick: handleBan },
{ key: 'tag', icon: Tag, label: 'Tag', color: 'text-blue-500 hover:bg-blue-500/10', onClick: onOpenTagDialog },
{ key: 'email', icon: Mail, label: 'Email', color: 'text-violet-500 hover:bg-violet-500/10', onClick: onOpenEmailComposer },
{ key: 'verify', icon: CheckCircle2, label: 'Verify', color: 'text-emerald-500 hover:bg-emerald-500/10', onClick: handleVerify },
{ key: 'export', icon: Download, label: 'Export CSV', color: 'text-sky-500 hover:bg-sky-500/10', onClick: handleExport },
{ key: 'delete', icon: Trash2, label: 'Delete', color: 'text-red-600 hover:bg-red-600/10', onClick: handleDelete, destructive: true },
];
return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in slide-in-from-bottom-4 fade-in-0 duration-300">
<div className="flex items-center gap-1 bg-background/95 backdrop-blur-xl border border-border/60 shadow-2xl rounded-2xl px-2 py-2">
{/* Selection Count */}
<div className="flex items-center gap-2 px-3 pr-4 border-r border-border/40">
<Badge variant="default" className="h-7 min-w-7 p-0 flex items-center justify-center rounded-full text-xs font-bold">
{count}
</Badge>
<span className="text-sm font-medium text-foreground whitespace-nowrap">
User{count > 1 ? 's' : ''} Selected
</span>
<Button variant="ghost" size="icon" className="h-6 w-6 rounded-full hover:bg-destructive/10" onClick={onClearSelection}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-0.5 px-1">
{actions.map((action, i) => (
<span key={action.key}>
{action.destructive && <div className="w-px h-6 bg-border/40 mx-1" />}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn('h-9 w-9 rounded-xl transition-all', action.color)}
onClick={action.onClick}
disabled={loadingAction !== null}
>
{loadingAction === action.key ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<action.icon className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{action.label}
</TooltipContent>
</Tooltip>
</span>
))}
</div>
</div>
</div>
);
}