refactor: implement permission grouping and updated granular access control model for roles
This commit is contained in:
@@ -1,14 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Shield, Plus, Search, X, Edit, Trash2, Copy, CheckCircle, XCircle, Eye } from 'lucide-react';
|
import { Shield, Plus, Search, X, Edit, Trash2, Copy, Check, ChevronDown, ChevronRight, BookOpen, FileSearch, ClipboardList, Settings, BarChart3, Bike, Users, Briefcase, Truck, Store, BatteryCharging, Building2, Wrench, DollarSign, TrendingUp, UserCog } from 'lucide-react';
|
||||||
|
|
||||||
interface Permission {
|
interface Permission {
|
||||||
module: string;
|
key: string;
|
||||||
view: boolean;
|
label: string;
|
||||||
create: boolean;
|
enabled: boolean;
|
||||||
edit: boolean;
|
}
|
||||||
delete: boolean;
|
|
||||||
|
interface PermissionGroup {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
permissions: Permission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Role {
|
interface Role {
|
||||||
@@ -16,23 +22,192 @@ interface Role {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
permissions: Permission[];
|
permissionGroups: PermissionGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultPermissions: Permission[] = [
|
const buildDefaultGroups = (): PermissionGroup[] => [
|
||||||
{ module: 'Dashboard', view: true, create: false, edit: false, delete: false },
|
|
||||||
{ module: 'KYC', view: false, create: false, edit: false, delete: false },
|
{
|
||||||
{ module: 'Rentals', view: false, create: false, edit: false, delete: false },
|
id: 'kyc',
|
||||||
{ module: 'Bikers', view: false, create: false, edit: false, delete: false },
|
title: 'KYC Requests & Verification',
|
||||||
{ module: 'Investors', view: false, create: false, edit: false, delete: false },
|
description: 'The Biker user will request from the app. Investor, Shop, Merchant will request from the website. Front desk officers (hub/head office) can request for Biker, Investor, Shop, Merchant and can upload remaining documents. Admin officers (head office) will approve or reject documents with notes and "make a Biker | Investor | Shop | Merchant".',
|
||||||
{ module: 'Fleet', view: false, create: false, edit: false, delete: false },
|
icon: FileSearch,
|
||||||
{ module: 'Merchants', view: false, create: false, edit: false, delete: false },
|
permissions: [
|
||||||
{ module: 'Swap Stations', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.request', label: 'KYC Request', enabled: false },
|
||||||
{ module: 'Hubs', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.view', label: 'View KYC Requests', enabled: false },
|
||||||
{ module: 'Maintenance', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.doc_upload', label: 'Document Upload', enabled: false },
|
||||||
{ module: 'Accounting', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.doc_approve', label: 'Document Approve', enabled: false },
|
||||||
{ module: 'Reports', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.doc_reject', label: 'Document Reject', enabled: false },
|
||||||
{ module: 'Users', view: false, create: false, edit: false, delete: false },
|
{ key: 'kyc.make_valid_user', label: 'Make a Biker | Investor | Shop | Merchant', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'plans',
|
||||||
|
title: 'Plan Selection + EV Condition',
|
||||||
|
description: 'Manage investment plans, swap station plans, and rider request plans',
|
||||||
|
icon: ClipboardList,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'plans.investment.view', label: 'View Investment Plan', enabled: false },
|
||||||
|
{ key: 'plans.investment.edit', label: 'Edit Investment Plan', enabled: false },
|
||||||
|
{ key: 'plans.swap_station.view', label: 'View Swap Station Plan', enabled: false },
|
||||||
|
{ key: 'plans.swap_station.edit', label: 'Edit Swap Station Plan', enabled: false },
|
||||||
|
{ key: 'plans.rider_request.view', label: 'View Rider Request Plan', enabled: false },
|
||||||
|
{ key: 'plans.rider_request.edit', label: 'Edit Rider Request Plan', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
|
title: 'Settings',
|
||||||
|
description: 'Manage system settings including KYC documents and plans',
|
||||||
|
icon: Settings,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'settings.kyc_documents.view', label: 'View KYC Documents', enabled: false },
|
||||||
|
{ key: 'settings.kyc_documents.edit', label: 'Edit KYC Documents', enabled: false },
|
||||||
|
{ key: 'settings.plan_selection.view', label: 'View Plan Selection with Condition', enabled: false },
|
||||||
|
{ key: 'settings.plan_selection.edit', label: 'Edit Plan Selection with Condition', enabled: false },
|
||||||
|
{ key: 'settings.company_policy.view', label: 'View Company Policy', enabled: false },
|
||||||
|
{ key: 'settings.company_policy.edit', label: 'Edit Company Policy', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
title: 'Dashboard',
|
||||||
|
description: 'Access to main dashboard',
|
||||||
|
icon: BarChart3,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'dashboard.view', label: 'View Dashboard', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rentals',
|
||||||
|
title: 'Rentals',
|
||||||
|
description: 'Manage rental operations',
|
||||||
|
icon: Bike,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'rentals.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'rentals.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'rentals.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'rentals.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bikers',
|
||||||
|
title: 'Bikers',
|
||||||
|
description: 'Manage bikers',
|
||||||
|
icon: Users,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'bikers.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'bikers.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'bikers.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'bikers.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'investors',
|
||||||
|
title: 'Investors',
|
||||||
|
description: 'Manage investors',
|
||||||
|
icon: Briefcase,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'investors.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'investors.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'investors.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'investors.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fleet',
|
||||||
|
title: 'Fleet',
|
||||||
|
description: 'Manage fleet vehicles',
|
||||||
|
icon: Truck,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'fleet.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'fleet.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'fleet.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'fleet.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'merchants',
|
||||||
|
title: 'Merchants',
|
||||||
|
description: 'Manage merchants',
|
||||||
|
icon: Store,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'merchants.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'merchants.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'merchants.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'merchants.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'swap_stations',
|
||||||
|
title: 'Swap Stations',
|
||||||
|
description: 'Manage swap stations',
|
||||||
|
icon: BatteryCharging,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'swap_stations.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'swap_stations.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'swap_stations.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'swap_stations.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hubs',
|
||||||
|
title: 'Hubs',
|
||||||
|
description: 'Manage hubs',
|
||||||
|
icon: Building2,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'hubs.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'hubs.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'hubs.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'hubs.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'maintenance',
|
||||||
|
title: 'Maintenance',
|
||||||
|
description: 'Manage maintenance requests',
|
||||||
|
icon: Wrench,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'maintenance.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'maintenance.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'maintenance.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'maintenance.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'accounting',
|
||||||
|
title: 'Accounting',
|
||||||
|
description: 'Manage financial records',
|
||||||
|
icon: DollarSign,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'accounting.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'accounting.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'accounting.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'accounting.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reports',
|
||||||
|
title: 'Reports',
|
||||||
|
description: 'View and generate reports',
|
||||||
|
icon: TrendingUp,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'reports.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'reports.export', label: 'Export', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'users',
|
||||||
|
title: 'Users',
|
||||||
|
description: 'Manage system users',
|
||||||
|
icon: UserCog,
|
||||||
|
permissions: [
|
||||||
|
{ key: 'users.view', label: 'View', enabled: false },
|
||||||
|
{ key: 'users.create', label: 'Create', enabled: false },
|
||||||
|
{ key: 'users.edit', label: 'Edit', enabled: false },
|
||||||
|
{ key: 'users.delete', label: 'Delete', enabled: false },
|
||||||
|
]
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const mockRoles: Role[] = [
|
const mockRoles: Role[] = [
|
||||||
@@ -41,76 +216,117 @@ const mockRoles: Role[] = [
|
|||||||
name: 'Admin',
|
name: 'Admin',
|
||||||
description: 'Full system access with all permissions',
|
description: 'Full system access with all permissions',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({ ...p, view: true, create: true, edit: true, delete: true }))
|
permissionGroups: buildDefaultGroups().map(g => ({
|
||||||
|
...g,
|
||||||
|
permissions: g.permissions.map(p => ({ ...p, enabled: true }))
|
||||||
|
}))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ROLE-002',
|
id: 'ROLE-002',
|
||||||
name: 'Manager',
|
name: 'Manager',
|
||||||
description: 'Management access with limited delete permissions',
|
description: 'Management access with limited delete permissions',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({
|
permissionGroups: buildDefaultGroups().map(g => ({
|
||||||
module: p.module,
|
...g,
|
||||||
view: true,
|
permissions: g.permissions.map(p => ({
|
||||||
create: ['Dashboard', 'Reports'].includes(p.module) ? false : true,
|
...p,
|
||||||
edit: ['Dashboard', 'Users'].includes(p.module) ? false : true,
|
enabled: !p.key.includes('delete')
|
||||||
delete: false
|
}))
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ROLE-003',
|
id: 'ROLE-003',
|
||||||
name: 'Biker',
|
name: 'Front Desk Officer',
|
||||||
description: 'Limited access for bike riders',
|
description: 'Hub/head office officer - can request KYC and upload documents',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({
|
permissionGroups: buildDefaultGroups().map(g => {
|
||||||
module: p.module,
|
if (g.id === 'kyc') {
|
||||||
view: ['Dashboard', 'Fleet', 'Rentals'].includes(p.module),
|
return {
|
||||||
create: p.module === 'Rentals',
|
...g,
|
||||||
edit: false,
|
permissions: g.permissions.map(p => ({
|
||||||
delete: false
|
...p,
|
||||||
}))
|
enabled: ['kyc.request', 'kyc.view', 'kyc.doc_upload'].includes(p.key)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (g.id === 'dashboard') {
|
||||||
|
return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) };
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ROLE-004',
|
id: 'ROLE-004',
|
||||||
name: 'Investor',
|
name: 'Admin Officer',
|
||||||
description: 'Access for investors to view portfolio',
|
description: 'Head office officer - can approve/reject KYC documents and make valid users',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({
|
permissionGroups: buildDefaultGroups().map(g => {
|
||||||
module: p.module,
|
if (g.id === 'kyc') {
|
||||||
view: ['Dashboard', 'Portfolio', 'Withdraw'].includes(p.module),
|
return {
|
||||||
create: p.module === 'Withdraw',
|
...g,
|
||||||
edit: false,
|
permissions: g.permissions.map(p => ({
|
||||||
delete: false
|
...p,
|
||||||
}))
|
enabled: ['kyc.view', 'kyc.doc_approve', 'kyc.doc_reject', 'kyc.make_valid_user'].includes(p.key)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (g.id === 'dashboard') {
|
||||||
|
return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) };
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ROLE-005',
|
id: 'ROLE-005',
|
||||||
name: 'Shop',
|
name: 'Biker',
|
||||||
description: 'Access for shop owners',
|
description: 'Limited access for bike riders - can view dashboard, request KYC, manage rentals',
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({
|
permissionGroups: buildDefaultGroups().map(g => {
|
||||||
module: p.module,
|
if (g.id === 'dashboard') {
|
||||||
view: ['Dashboard', 'Fleet', 'Deliveries'].includes(p.module),
|
return { ...g, permissions: g.permissions.map(p => ({ ...p, enabled: true })) };
|
||||||
create: p.module === 'Deliveries',
|
}
|
||||||
edit: p.module === 'Deliveries',
|
if (g.id === 'kyc') {
|
||||||
delete: false
|
return {
|
||||||
}))
|
...g,
|
||||||
},
|
permissions: g.permissions.map(p => ({
|
||||||
{
|
...p,
|
||||||
id: 'ROLE-006',
|
enabled: ['kyc.request', 'kyc.view'].includes(p.key)
|
||||||
name: 'Merchant',
|
}))
|
||||||
description: 'Access for merchants',
|
};
|
||||||
isDefault: true,
|
}
|
||||||
permissions: defaultPermissions.map(p => ({
|
if (g.id === 'rentals') {
|
||||||
module: p.module,
|
return {
|
||||||
view: ['Dashboard', 'Deliveries'].includes(p.module),
|
...g,
|
||||||
create: false,
|
permissions: g.permissions.map(p => ({
|
||||||
edit: ['Deliveries'].includes(p.module),
|
...p,
|
||||||
delete: false
|
enabled: ['rentals.view', 'rentals.create'].includes(p.key)
|
||||||
}))
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return g;
|
||||||
|
})
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const allModules = ['Dashboard', 'KYC', 'Rentals', 'Bikers', 'Investors', 'Fleet', 'Merchants', 'Swap Stations', 'Hubs', 'Maintenance', 'Accounting', 'Reports', 'Users', 'Portfolio', 'Withdraw', 'Deliveries'];
|
function Toggle({ checked, onChange, color = 'blue' }: { checked: boolean; onChange: () => void; color?: string }) {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
blue: checked ? 'bg-blue-600' : 'bg-slate-200',
|
||||||
|
green: checked ? 'bg-green-600' : 'bg-slate-200',
|
||||||
|
amber: checked ? 'bg-amber-600' : 'bg-slate-200',
|
||||||
|
red: checked ? 'bg-red-600' : 'bg-slate-200',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onChange}
|
||||||
|
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${colors[color] || colors.blue}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${checked ? 'translate-x-4' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function RolesPage() {
|
export default function RolesPage() {
|
||||||
const [roles, setRoles] = useState<Role[]>(mockRoles);
|
const [roles, setRoles] = useState<Role[]>(mockRoles);
|
||||||
@@ -118,20 +334,16 @@ export default function RolesPage() {
|
|||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||||
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({ name: '', description: '' });
|
||||||
name: '',
|
|
||||||
description: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredRoles = roles.filter(r =>
|
const filteredRoles = roles.filter(r =>
|
||||||
r.name.toLowerCase().includes(search.toLowerCase()) ||
|
r.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||||
r.description.toLowerCase().includes(search.toLowerCase())
|
r.description.toLowerCase().includes(search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!formData.name) return;
|
if (!formData.name) return;
|
||||||
|
|
||||||
if (editingRole) {
|
if (editingRole) {
|
||||||
setRoles(roles.map(r => r.id === editingRole.id ? { ...r, ...formData, isDefault: false } : r));
|
setRoles(roles.map(r => r.id === editingRole.id ? { ...r, ...formData, isDefault: false } : r));
|
||||||
} else {
|
} else {
|
||||||
@@ -139,11 +351,10 @@ export default function RolesPage() {
|
|||||||
id: `ROLE-${String(roles.length + 1).padStart(3, '0')}`,
|
id: `ROLE-${String(roles.length + 1).padStart(3, '0')}`,
|
||||||
...formData,
|
...formData,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: defaultPermissions.map(p => ({ ...p, view: false, create: false, edit: false, delete: false }))
|
permissionGroups: buildDefaultGroups()
|
||||||
};
|
};
|
||||||
setRoles([...roles, newRole]);
|
setRoles([...roles, newRole]);
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowCreateModal(false);
|
setShowCreateModal(false);
|
||||||
setEditingRole(null);
|
setEditingRole(null);
|
||||||
setFormData({ name: '', description: '' });
|
setFormData({ name: '', description: '' });
|
||||||
@@ -162,30 +373,44 @@ export default function RolesPage() {
|
|||||||
name: `${role.name} (Copy)`,
|
name: `${role.name} (Copy)`,
|
||||||
description: role.description,
|
description: role.description,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
permissions: role.permissions.map(p => ({ ...p }))
|
permissionGroups: role.permissionGroups.map(g => ({
|
||||||
|
...g,
|
||||||
|
permissions: g.permissions.map(p => ({ ...p }))
|
||||||
|
}))
|
||||||
};
|
};
|
||||||
setRoles([...roles, newRole]);
|
setRoles([...roles, newRole]);
|
||||||
|
setSelectedRole(newRole);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (role: Role) => {
|
const openEdit = (role: Role) => {
|
||||||
setEditingRole(role);
|
setEditingRole(role);
|
||||||
setFormData({
|
setFormData({ name: role.name, description: role.description });
|
||||||
name: role.name,
|
|
||||||
description: role.description
|
|
||||||
});
|
|
||||||
setShowCreateModal(true);
|
setShowCreateModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const togglePermission = (moduleIndex: number, action: 'view' | 'create' | 'edit' | 'delete') => {
|
const togglePermission = (groupIndex: number, permIndex: number) => {
|
||||||
if (!selectedRole) return;
|
if (!selectedRole) return;
|
||||||
const updated = [...selectedRole.permissions];
|
const updated = [...selectedRole.permissionGroups];
|
||||||
updated[moduleIndex][action] = !updated[moduleIndex][action];
|
const perm = updated[groupIndex].permissions[permIndex];
|
||||||
setSelectedRole({ ...selectedRole, permissions: updated });
|
perm.enabled = !perm.enabled;
|
||||||
setRoles(roles.map(r => r.id === selectedRole.id ? { ...selectedRole, permissions: updated } : r));
|
setSelectedRole({ ...selectedRole, permissionGroups: updated });
|
||||||
|
setRoles(roles.map(r => r.id === selectedRole.id ? { ...selectedRole, permissionGroups: updated } : r));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPermissionCount = (role: Role, action: keyof Permission) => {
|
const toggleGroupAll = (groupIndex: number, enabled: boolean) => {
|
||||||
return role.permissions.filter(p => p[action] === true).length;
|
if (!selectedRole) return;
|
||||||
|
const updated = [...selectedRole.permissionGroups];
|
||||||
|
updated[groupIndex].permissions = updated[groupIndex].permissions.map(p => ({ ...p, enabled }));
|
||||||
|
setSelectedRole({ ...selectedRole, permissionGroups: updated });
|
||||||
|
setRoles(roles.map(r => r.id === selectedRole.id ? { ...selectedRole, permissionGroups: updated } : r));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isGroupAllEnabled = (group: PermissionGroup) => group.permissions.every(p => p.enabled);
|
||||||
|
const getEnabledCount = (group: PermissionGroup) => group.permissions.filter(p => p.enabled).length;
|
||||||
|
const getTotalEnabled = (role: Role) => role.permissionGroups.reduce((a, g) => a + g.permissions.filter(p => p.enabled).length, 0);
|
||||||
|
|
||||||
|
const toggleGroup = (groupId: string) => {
|
||||||
|
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -195,7 +420,7 @@ export default function RolesPage() {
|
|||||||
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Roles & Permissions</h1>
|
<h1 className="text-2xl lg:text-3xl font-extrabold text-slate-800">Roles & Permissions</h1>
|
||||||
<p className="text-sm text-slate-500 mt-1">Manage roles and access permissions</p>
|
<p className="text-sm text-slate-500 mt-1">Manage roles and access permissions</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingRole(null);
|
setEditingRole(null);
|
||||||
setFormData({ name: '', description: '' });
|
setFormData({ name: '', description: '' });
|
||||||
@@ -217,12 +442,12 @@ export default function RolesPage() {
|
|||||||
<p className="text-sm text-slate-500">Default Roles</p>
|
<p className="text-sm text-slate-500">Default Roles</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||||
<p className="text-2xl font-extrabold text-blue-600">{roles.reduce((a, r) => a + getPermissionCount(r, 'create'), 0)}</p>
|
<p className="text-2xl font-extrabold text-blue-600">{roles.reduce((a, r) => a + getTotalEnabled(r), 0)}</p>
|
||||||
<p className="text-sm text-slate-500">Total Create Perms</p>
|
<p className="text-sm text-slate-500">Total Permissions</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||||||
<p className="text-2xl font-extrabold text-purple-600">{roles.reduce((a, r) => a + getPermissionCount(r, 'view'), 0)}</p>
|
<p className="text-2xl font-extrabold text-purple-600">{selectedRole ? getTotalEnabled(selectedRole) : '-'}</p>
|
||||||
<p className="text-sm text-slate-500">Total View Perms</p>
|
<p className="text-sm text-slate-500">Selected Role Perms</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -231,9 +456,9 @@ export default function RolesPage() {
|
|||||||
<div className="p-4 border-b border-slate-100">
|
<div className="p-4 border-b border-slate-100">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search roles..."
|
placeholder="Search roles..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent"
|
||||||
@@ -242,8 +467,8 @@ export default function RolesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="divide-y divide-slate-50 max-h-[600px] overflow-y-auto">
|
<div className="divide-y divide-slate-50 max-h-[600px] overflow-y-auto">
|
||||||
{filteredRoles.map(role => (
|
{filteredRoles.map(role => (
|
||||||
<div
|
<div
|
||||||
key={role.id}
|
key={role.id}
|
||||||
className={`p-4 hover:bg-slate-50 cursor-pointer transition-colors ${selectedRole?.id === role.id ? 'bg-accent-light' : ''}`}
|
className={`p-4 hover:bg-slate-50 cursor-pointer transition-colors ${selectedRole?.id === role.id ? 'bg-accent-light' : ''}`}
|
||||||
onClick={() => setSelectedRole(role)}
|
onClick={() => setSelectedRole(role)}
|
||||||
>
|
>
|
||||||
@@ -257,13 +482,7 @@ export default function RolesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mt-2 text-xs text-slate-400">
|
<div className="flex items-center gap-2 mt-2 text-xs text-slate-400">
|
||||||
<span>{getPermissionCount(role, 'view')} view</span>
|
<span>{getTotalEnabled(role)} permissions</span>
|
||||||
<span>•</span>
|
|
||||||
<span>{getPermissionCount(role, 'create')} create</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{getPermissionCount(role, 'edit')} edit</span>
|
|
||||||
<span>•</span>
|
|
||||||
<span>{getPermissionCount(role, 'delete')} delete</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -292,80 +511,65 @@ export default function RolesPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4 space-y-3 max-h-[600px] overflow-y-auto">
|
||||||
<div className="flex items-center justify-between mb-4">
|
{selectedRole.permissionGroups.map((group, gi) => (
|
||||||
<h4 className="font-medium text-slate-800">Permissions</h4>
|
<div key={group.id} className="border border-slate-200 rounded-lg">
|
||||||
<div className="flex gap-4 text-xs">
|
<div
|
||||||
<span className="text-slate-500">Check all <span className="font-medium text-green-600">view</span></span>
|
className="flex items-center justify-between p-3 cursor-pointer hover:bg-slate-50"
|
||||||
<span className="text-slate-500">Check all <span className="font-medium text-blue-600">create</span></span>
|
onClick={() => toggleGroup(group.id)}
|
||||||
<span className="text-slate-500">Check all <span className="font-medium text-amber-600">edit</span></span>
|
>
|
||||||
<span className="text-slate-500">Check all <span className="font-medium text-red-600">delete</span></span>
|
<div className="flex items-center gap-3">
|
||||||
|
<group.icon className="w-5 h-5 text-slate-600" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-700">{group.title}</p>
|
||||||
|
<p className="text-xs text-slate-400">{getEnabledCount(group)}/{group.permissions.length} enabled</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleGroupAll(gi, !isGroupAllEnabled(group));
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1 bg-slate-100 hover:bg-slate-200 rounded text-slate-600"
|
||||||
|
>
|
||||||
|
{isGroupAllEnabled(group) ? 'Uncheck All' : 'Check All'}
|
||||||
|
</button>
|
||||||
|
{expandedGroups[group.id] ? (
|
||||||
|
<ChevronDown className="w-4 h-4 text-slate-400" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight className="w-4 h-4 text-slate-400" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{expandedGroups[group.id] && (
|
||||||
|
<div className="border-t border-slate-200">
|
||||||
|
<div className="px-3 py-2 bg-slate-50">
|
||||||
|
<p className="text-xs text-slate-500">{group.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="divide-y divide-slate-100">
|
||||||
|
{group.permissions.map((perm, pi) => (
|
||||||
|
<div key={perm.key} className="flex items-center justify-between px-3 py-2.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{perm.enabled ? (
|
||||||
|
<Check className="w-4 h-4 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<X className="w-4 h-4 text-slate-300" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm text-slate-700">{perm.label}</span>
|
||||||
|
<span className="text-xs text-slate-400 font-mono">{perm.key}</span>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={perm.enabled}
|
||||||
|
onChange={() => togglePermission(gi, pi)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full">
|
|
||||||
<thead className="bg-slate-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-4 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Module</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-slate-500 uppercase">View</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-slate-500 uppercase">Create</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-slate-500 uppercase">Edit</th>
|
|
||||||
<th className="px-4 py-3 text-center text-xs font-semibold text-slate-500 uppercase">Delete</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-50">
|
|
||||||
{selectedRole.permissions.map((perm, i) => (
|
|
||||||
<tr key={i} className="hover:bg-slate-50">
|
|
||||||
<td className="px-4 py-3 text-sm font-medium text-slate-700">{perm.module}</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePermission(i, 'view')}
|
|
||||||
className={`w-5 h-5 rounded flex items-center justify-center ${
|
|
||||||
perm.view ? 'bg-green-100 text-green-600' : 'bg-slate-100 text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{perm.view && <CheckCircle className="w-4 h-4" />}
|
|
||||||
{!perm.view && <XCircle className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePermission(i, 'create')}
|
|
||||||
className={`w-5 h-5 rounded flex items-center justify-center ${
|
|
||||||
perm.create ? 'bg-blue-100 text-blue-600' : 'bg-slate-100 text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{perm.create && <CheckCircle className="w-4 h-4" />}
|
|
||||||
{!perm.create && <XCircle className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePermission(i, 'edit')}
|
|
||||||
className={`w-5 h-5 rounded flex items-center justify-center ${
|
|
||||||
perm.edit ? 'bg-amber-100 text-amber-600' : 'bg-slate-100 text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{perm.edit && <CheckCircle className="w-4 h-4" />}
|
|
||||||
{!perm.edit && <XCircle className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
<button
|
|
||||||
onClick={() => togglePermission(i, 'delete')}
|
|
||||||
className={`w-5 h-5 rounded flex items-center justify-center ${
|
|
||||||
perm.delete ? 'bg-red-100 text-red-600' : 'bg-slate-100 text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{perm.delete && <CheckCircle className="w-4 h-4" />}
|
|
||||||
{!perm.delete && <XCircle className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -409,13 +613,13 @@ export default function RolesPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowCreateModal(false)}
|
onClick={() => setShowCreateModal(false)}
|
||||||
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
|
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={!formData.name}
|
disabled={!formData.name}
|
||||||
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
|
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
|
||||||
|
|||||||
Reference in New Issue
Block a user