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.
This commit is contained in:
Felipe Cardoso
2025-11-06 12:08:10 +01:00
parent c10c1d1c39
commit 91bc4f190d
11 changed files with 1794 additions and 39 deletions

View File

@@ -0,0 +1,191 @@
/**
* UserActionMenu Component
* Dropdown menu for user row actions (Edit, Activate/Deactivate, Delete)
*/
'use client';
import { useState } from 'react';
import { MoreHorizontal, Edit, CheckCircle, XCircle, Trash } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
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 {
useActivateUser,
useDeactivateUser,
useDeleteUser,
} from '@/lib/api/hooks/useAdmin';
interface User {
id: string;
email: string;
first_name: string;
last_name: string | null;
is_active: boolean;
is_superuser: boolean;
}
interface UserActionMenuProps {
user: User;
isCurrentUser: boolean;
onEdit?: (user: User) => void;
}
type ConfirmAction = 'delete' | 'deactivate' | null;
export function UserActionMenu({ user, isCurrentUser, onEdit }: UserActionMenuProps) {
const [confirmAction, setConfirmAction] = useState<ConfirmAction>(null);
const [dropdownOpen, setDropdownOpen] = useState(false);
const activateUser = useActivateUser();
const deactivateUser = useDeactivateUser();
const deleteUser = useDeleteUser();
const fullName = user.last_name
? `${user.first_name} ${user.last_name}`
: user.first_name;
// Handle activate action
const handleActivate = async () => {
try {
await activateUser.mutateAsync(user.id);
toast.success(`${fullName} has been activated successfully.`);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to activate user');
}
};
// Handle deactivate action
const handleDeactivate = async () => {
try {
await deactivateUser.mutateAsync(user.id);
toast.success(`${fullName} has been deactivated successfully.`);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to deactivate user');
} finally {
setConfirmAction(null);
}
};
// Handle delete action
const handleDelete = async () => {
try {
await deleteUser.mutateAsync(user.id);
toast.success(`${fullName} has been deleted successfully.`);
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to delete user');
} finally {
setConfirmAction(null);
}
};
// Handle edit action
const handleEdit = () => {
setDropdownOpen(false);
if (onEdit) {
onEdit(user);
}
};
// Render confirmation dialog
const renderConfirmDialog = () => {
if (!confirmAction) return null;
const isDelete = confirmAction === 'delete';
const title = isDelete ? 'Delete User' : 'Deactivate User';
const description = isDelete
? `Are you sure you want to delete ${fullName}? This action cannot be undone.`
: `Are you sure you want to deactivate ${fullName}? They will not be able to log in until reactivated.`;
const action = isDelete ? handleDelete : handleDeactivate;
const actionLabel = isDelete ? 'Delete' : 'Deactivate';
return (
<AlertDialog open={!!confirmAction} onOpenChange={() => setConfirmAction(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={action}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{actionLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
return (
<>
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
aria-label={`Actions for ${fullName}`}
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>
<Edit className="mr-2 h-4 w-4" />
Edit User
</DropdownMenuItem>
<DropdownMenuSeparator />
{user.is_active ? (
<DropdownMenuItem
onClick={() => setConfirmAction('deactivate')}
disabled={isCurrentUser}
>
<XCircle className="mr-2 h-4 w-4" />
Deactivate
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={handleActivate}>
<CheckCircle className="mr-2 h-4 w-4" />
Activate
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setConfirmAction('delete')}
disabled={isCurrentUser}
className="text-destructive focus:text-destructive"
>
<Trash className="mr-2 h-4 w-4" />
Delete User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{renderConfirmDialog()}
</>
);
}