- Add useRbac hook with roles, permissions, and user role management - Add RBAC API endpoints to api.ts (roles, permissions, user assignments) - Add RolesPage for managing roles and viewing permissions - Add PortfolioPage dashboard with stats and quick links - Add ProductsPage with search, filters, and pagination - Add CategoriesPage with tree view and expand/collapse - Fix useMyEarnings export conflict between useMlm and useCommissions - Export useMyCommissionEarnings as alias to avoid naming conflict Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
242 lines
11 KiB
TypeScript
242 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { Shield, Plus, Search, Edit, Trash2, Lock } from 'lucide-react';
|
|
import {
|
|
useRoles,
|
|
useDeleteRole,
|
|
usePermissions,
|
|
getPermissionCategoryLabel,
|
|
} from '@/hooks/useRbac';
|
|
|
|
export default function RolesPage() {
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [selectedRole, setSelectedRole] = useState<string | null>(null);
|
|
|
|
const { data: roles, isLoading } = useRoles();
|
|
const { data: permissions } = usePermissions();
|
|
const deleteMutation = useDeleteRole();
|
|
|
|
const handleDelete = async (id: string, isSystem: boolean) => {
|
|
if (isSystem) {
|
|
alert('System roles cannot be deleted.');
|
|
return;
|
|
}
|
|
if (window.confirm('Are you sure you want to delete this role?')) {
|
|
await deleteMutation.mutateAsync(id);
|
|
}
|
|
};
|
|
|
|
const filteredRoles = roles?.filter((role: any) =>
|
|
role.name.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
|
|
const selectedRoleData = roles?.find((r: any) => r.id === selectedRole);
|
|
|
|
// Group permissions by category
|
|
const permissionsByCategory = permissions?.reduce((acc: any, perm: any) => {
|
|
if (!acc[perm.category]) {
|
|
acc[perm.category] = [];
|
|
}
|
|
acc[perm.category].push(perm);
|
|
return acc;
|
|
}, {});
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Roles & Permissions</h1>
|
|
<p className="text-gray-500 dark:text-gray-400">
|
|
Manage user roles and their permissions
|
|
</p>
|
|
</div>
|
|
<Link
|
|
to="/dashboard/rbac/roles/new"
|
|
className="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
New Role
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
{/* Roles List */}
|
|
<div className="lg:col-span-1">
|
|
<div className="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
|
<div className="border-b border-gray-200 p-4 dark:border-gray-700">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search roles..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="max-h-[600px] overflow-y-auto p-4">
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-t-transparent" />
|
|
</div>
|
|
) : filteredRoles?.length ? (
|
|
<div className="space-y-2">
|
|
{filteredRoles.map((role: any) => (
|
|
<button
|
|
key={role.id}
|
|
onClick={() => setSelectedRole(role.id)}
|
|
className={`w-full rounded-lg p-3 text-left transition-colors ${
|
|
selectedRole === role.id
|
|
? 'bg-blue-50 ring-2 ring-blue-500 dark:bg-blue-900/20'
|
|
: 'hover:bg-gray-50 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`rounded-lg p-2 ${
|
|
role.is_system
|
|
? 'bg-purple-100 dark:bg-purple-900/20'
|
|
: 'bg-gray-100 dark:bg-gray-700'
|
|
}`}>
|
|
<Shield className={`h-5 w-5 ${
|
|
role.is_system
|
|
? 'text-purple-600 dark:text-purple-400'
|
|
: 'text-gray-600 dark:text-gray-400'
|
|
}`} />
|
|
</div>
|
|
<div>
|
|
<p className="font-medium text-gray-900 dark:text-white">{role.name}</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
{role.permissions?.length || 0} permissions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{role.is_system && (
|
|
<Lock className="h-4 w-4 text-gray-400" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="py-8 text-center text-gray-500 dark:text-gray-400">
|
|
No roles found.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Role Details */}
|
|
<div className="lg:col-span-2">
|
|
{selectedRoleData ? (
|
|
<div className="rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800">
|
|
<div className="flex items-center justify-between border-b border-gray-200 p-4 dark:border-gray-700">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
{selectedRoleData.name}
|
|
</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{selectedRoleData.description || 'No description'}
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
{!selectedRoleData.is_system && (
|
|
<>
|
|
<Link
|
|
to={`/dashboard/rbac/roles/${selectedRoleData.id}/edit`}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
>
|
|
<Edit className="h-4 w-4" />
|
|
Edit
|
|
</Link>
|
|
<button
|
|
onClick={() => handleDelete(selectedRoleData.id, selectedRoleData.is_system)}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-700 hover:bg-red-50 dark:border-red-600 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-red-900/20"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Delete
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Role Info */}
|
|
<div className="border-b border-gray-200 p-4 dark:border-gray-700">
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Type</p>
|
|
<p className="font-medium text-gray-900 dark:text-white">
|
|
{selectedRoleData.is_system ? 'System' : 'Custom'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Default</p>
|
|
<p className="font-medium text-gray-900 dark:text-white">
|
|
{selectedRoleData.is_default ? 'Yes' : 'No'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Slug</p>
|
|
<p className="font-mono text-sm text-gray-900 dark:text-white">
|
|
{selectedRoleData.slug}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Permissions */}
|
|
<div className="p-4">
|
|
<h3 className="mb-4 font-medium text-gray-900 dark:text-white">Permissions</h3>
|
|
{permissionsByCategory && Object.keys(permissionsByCategory).length > 0 ? (
|
|
<div className="space-y-4">
|
|
{Object.entries(permissionsByCategory).map(([category, perms]: [string, any]) => {
|
|
const rolePermSlugs = selectedRoleData.permissions?.map((p: any) => p.slug) || [];
|
|
const categoryPerms = perms.filter((p: any) => rolePermSlugs.includes(p.slug));
|
|
if (categoryPerms.length === 0) return null;
|
|
|
|
return (
|
|
<div key={category}>
|
|
<h4 className="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{getPermissionCategoryLabel(category)}
|
|
</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{categoryPerms.map((perm: any) => (
|
|
<span
|
|
key={perm.id}
|
|
className="inline-flex items-center rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-800 dark:bg-blue-900/20 dark:text-blue-400"
|
|
>
|
|
{perm.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
No permissions assigned to this role.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex h-full items-center justify-center rounded-lg border border-gray-200 bg-white p-12 dark:border-gray-700 dark:bg-gray-800">
|
|
<div className="text-center">
|
|
<Shield className="mx-auto h-12 w-12 text-gray-300 dark:text-gray-600" />
|
|
<p className="mt-4 text-gray-500 dark:text-gray-400">
|
|
Select a role to view its details and permissions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|