147 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
}
|