feat(users): implement bulk actions logic
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// BulkActionsDropdown - Bulk operations menu for selected users
|
// BulkActionsDropdown - Bulk operations menu for selected users
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -37,6 +37,13 @@ import { mockTags } from '../data/mockUserCrmData';
|
|||||||
import type { User } from '@/lib/types/user';
|
import type { User } from '@/lib/types/user';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
bulkSuspendUsers,
|
||||||
|
bulkBanUsers,
|
||||||
|
bulkDeleteUsers,
|
||||||
|
bulkVerifyUsers,
|
||||||
|
bulkTagUsers,
|
||||||
|
} from '@/lib/actions/user-management';
|
||||||
|
|
||||||
interface BulkActionsDropdownProps {
|
interface BulkActionsDropdownProps {
|
||||||
selectedUsers: User[];
|
selectedUsers: User[];
|
||||||
@@ -55,54 +62,73 @@ export function BulkActionsDropdown({
|
|||||||
}: BulkActionsDropdownProps) {
|
}: BulkActionsDropdownProps) {
|
||||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||||
const [actionType, setActionType] = useState<'suspend' | 'ban' | 'delete' | 'export' | 'verify'>('suspend');
|
const [actionType, setActionType] = useState<'suspend' | 'ban' | 'delete' | 'export' | 'verify'>('suspend');
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const count = selectedUsers.length;
|
const count = selectedUsers.length;
|
||||||
|
const userIds = selectedUsers.map(u => u.id);
|
||||||
|
|
||||||
const handleAction = (action: 'suspend' | 'ban' | 'delete' | 'export' | 'verify') => {
|
const handleAction = (action: 'suspend' | 'ban' | 'delete' | 'export' | 'verify') => {
|
||||||
setActionType(action);
|
setActionType(action);
|
||||||
setConfirmDialogOpen(true);
|
setConfirmDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const executeServerAction = async (action: () => Promise<{ success: boolean; message: string }>) => {
|
||||||
setIsProcessing(true);
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
// Simulate API call
|
const result = await action();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
if (result.success) {
|
||||||
|
toast.success(result.message);
|
||||||
switch (actionType) {
|
setConfirmDialogOpen(false);
|
||||||
case 'suspend':
|
onClearSelection();
|
||||||
toast.success(`Suspended ${count} user${count > 1 ? 's' : ''}`);
|
} else {
|
||||||
break;
|
toast.error(result.message);
|
||||||
case 'ban':
|
}
|
||||||
toast.success(`Banned ${count} user${count > 1 ? 's' : ''}`);
|
} catch (error) {
|
||||||
break;
|
toast.error("An unexpected error occurred.");
|
||||||
case 'delete':
|
|
||||||
toast.success(`Deleted ${count} user${count > 1 ? 's' : ''}`);
|
|
||||||
break;
|
|
||||||
case 'export':
|
|
||||||
toast.success(`Exported ${count} user${count > 1 ? 's' : ''} to CSV`);
|
|
||||||
break;
|
|
||||||
case 'verify':
|
|
||||||
toast.success(`Verified ${count} user${count > 1 ? 's' : ''}`);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setConfirmDialogOpen(false);
|
const handleConfirm = async () => {
|
||||||
onClearSelection();
|
switch (actionType) {
|
||||||
} catch (error) {
|
case 'suspend':
|
||||||
toast.error('Operation failed');
|
executeServerAction(() => bulkSuspendUsers(userIds));
|
||||||
} finally {
|
break;
|
||||||
setIsProcessing(false);
|
case 'ban':
|
||||||
|
executeServerAction(() => bulkBanUsers(userIds));
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
executeServerAction(() => bulkDeleteUsers(userIds));
|
||||||
|
break;
|
||||||
|
case 'verify':
|
||||||
|
executeServerAction(() => bulkVerifyUsers(userIds));
|
||||||
|
break;
|
||||||
|
case 'export':
|
||||||
|
// Export is usually client-side or separate download endpoint,
|
||||||
|
// but we can simulate a server request or just handle it here.
|
||||||
|
toast.success(`Exporting ${count} users...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.success("Export Complete (Mock CSV Download)");
|
||||||
|
setConfirmDialogOpen(false);
|
||||||
|
onClearSelection();
|
||||||
|
}, 1000);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagUsers = async (tagId: string) => {
|
const handleTagUsers = (tagId: string) => {
|
||||||
const tag = mockTags.find((t) => t.id === tagId);
|
const tag = mockTags.find((t) => t.id === tagId);
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
toast.success(`Added tag "${tag.name}" to ${count} user${count > 1 ? 's' : ''}`);
|
startTransition(async () => {
|
||||||
onClearSelection();
|
const result = await bulkTagUsers(userIds, tagId);
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(result.message, { description: `Applied tag: ${tag.name}` });
|
||||||
|
onClearSelection();
|
||||||
|
} else {
|
||||||
|
toast.error(result.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionConfig = {
|
const actionConfig = {
|
||||||
@@ -246,16 +272,16 @@ export function BulkActionsDropdown({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setConfirmDialogOpen(false)} disabled={isProcessing}>
|
<Button variant="outline" onClick={() => setConfirmDialogOpen(false)} disabled={isPending}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className={config.buttonClass}
|
className={config.buttonClass}
|
||||||
onClick={handleConfirm}
|
onClick={handleConfirm}
|
||||||
disabled={isProcessing}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isPending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Processing...
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> Processing...
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -144,3 +144,102 @@ export async function createSupportTicket(userId: string, summary: string) {
|
|||||||
return { success: false, message: "Failed to create ticket." };
|
return { success: false, message: "Failed to create ticket." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Bulk Actions ---
|
||||||
|
|
||||||
|
export async function bulkSuspendUsers(userIds: string[]) {
|
||||||
|
try {
|
||||||
|
const admin = await verifyAdmin();
|
||||||
|
|
||||||
|
// Simulate DB update
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// Log for each user or a single bulk log
|
||||||
|
await logAdminAction({
|
||||||
|
actorId: admin.id,
|
||||||
|
action: 'BULK_SUSPEND',
|
||||||
|
targetId: 'multiple',
|
||||||
|
details: { count: userIds.length, userIds },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully suspended ${userIds.length} users.` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: "Failed to perform bulk suspension." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkBanUsers(userIds: string[]) {
|
||||||
|
try {
|
||||||
|
const admin = await verifyAdmin();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
await logAdminAction({
|
||||||
|
actorId: admin.id,
|
||||||
|
action: 'BULK_BAN',
|
||||||
|
targetId: 'multiple',
|
||||||
|
details: { count: userIds.length, userIds },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully banned ${userIds.length} users.` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: "Failed to perform bulk ban." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkDeleteUsers(userIds: string[]) {
|
||||||
|
try {
|
||||||
|
const admin = await verifyAdmin();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
await logAdminAction({
|
||||||
|
actorId: admin.id,
|
||||||
|
action: 'BULK_DELETE',
|
||||||
|
targetId: 'multiple',
|
||||||
|
details: { count: userIds.length, userIds },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully deleted ${userIds.length} users.` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: "Failed to perform bulk delete." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkVerifyUsers(userIds: string[]) {
|
||||||
|
try {
|
||||||
|
const admin = await verifyAdmin();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
await logAdminAction({
|
||||||
|
actorId: admin.id,
|
||||||
|
action: 'BULK_VERIFY',
|
||||||
|
targetId: 'multiple',
|
||||||
|
details: { count: userIds.length, userIds },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Successfully verified ${userIds.length} users.` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: "Failed to verify users." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function bulkTagUsers(userIds: string[], tagId: string) {
|
||||||
|
try {
|
||||||
|
const admin = await verifyAdmin();
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
await logAdminAction({
|
||||||
|
actorId: admin.id,
|
||||||
|
action: 'BULK_TAG_ADD',
|
||||||
|
targetId: 'multiple',
|
||||||
|
details: { count: userIds.length, userIds, tagId },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: `Added tag to ${userIds.length} users.` };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: "Failed to tag users." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user