431 lines
19 KiB
TypeScript
431 lines
19 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { Shield, Plus, Search, X, Edit, Trash2, Copy, CheckCircle, XCircle, Eye } from 'lucide-react';
|
||
|
|
|
||
|
|
interface Permission {
|
||
|
|
module: string;
|
||
|
|
view: boolean;
|
||
|
|
create: boolean;
|
||
|
|
edit: boolean;
|
||
|
|
delete: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Role {
|
||
|
|
id: string;
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
isDefault: boolean;
|
||
|
|
permissions: Permission[];
|
||
|
|
}
|
||
|
|
|
||
|
|
const defaultPermissions: Permission[] = [
|
||
|
|
{ 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 },
|
||
|
|
{ module: 'Bikers', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Investors', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Fleet', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Merchants', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Swap Stations', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Hubs', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Maintenance', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Accounting', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Reports', view: false, create: false, edit: false, delete: false },
|
||
|
|
{ module: 'Users', view: false, create: false, edit: false, delete: false },
|
||
|
|
];
|
||
|
|
|
||
|
|
const mockRoles: Role[] = [
|
||
|
|
{
|
||
|
|
id: 'ROLE-001',
|
||
|
|
name: 'Admin',
|
||
|
|
description: 'Full system access with all permissions',
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({ ...p, view: true, create: true, edit: true, delete: true }))
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ROLE-002',
|
||
|
|
name: 'Manager',
|
||
|
|
description: 'Management access with limited delete permissions',
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({
|
||
|
|
module: p.module,
|
||
|
|
view: true,
|
||
|
|
create: ['Dashboard', 'Reports'].includes(p.module) ? false : true,
|
||
|
|
edit: ['Dashboard', 'Users'].includes(p.module) ? false : true,
|
||
|
|
delete: false
|
||
|
|
}))
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ROLE-003',
|
||
|
|
name: 'Biker',
|
||
|
|
description: 'Limited access for bike riders',
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({
|
||
|
|
module: p.module,
|
||
|
|
view: ['Dashboard', 'Fleet', 'Rentals'].includes(p.module),
|
||
|
|
create: p.module === 'Rentals',
|
||
|
|
edit: false,
|
||
|
|
delete: false
|
||
|
|
}))
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ROLE-004',
|
||
|
|
name: 'Investor',
|
||
|
|
description: 'Access for investors to view portfolio',
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({
|
||
|
|
module: p.module,
|
||
|
|
view: ['Dashboard', 'Portfolio', 'Withdraw'].includes(p.module),
|
||
|
|
create: p.module === 'Withdraw',
|
||
|
|
edit: false,
|
||
|
|
delete: false
|
||
|
|
}))
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ROLE-005',
|
||
|
|
name: 'Shop',
|
||
|
|
description: 'Access for shop owners',
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({
|
||
|
|
module: p.module,
|
||
|
|
view: ['Dashboard', 'Fleet', 'Deliveries'].includes(p.module),
|
||
|
|
create: p.module === 'Deliveries',
|
||
|
|
edit: p.module === 'Deliveries',
|
||
|
|
delete: false
|
||
|
|
}))
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'ROLE-006',
|
||
|
|
name: 'Merchant',
|
||
|
|
description: 'Access for merchants',
|
||
|
|
isDefault: true,
|
||
|
|
permissions: defaultPermissions.map(p => ({
|
||
|
|
module: p.module,
|
||
|
|
view: ['Dashboard', 'Deliveries'].includes(p.module),
|
||
|
|
create: false,
|
||
|
|
edit: ['Deliveries'].includes(p.module),
|
||
|
|
delete: false
|
||
|
|
}))
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const allModules = ['Dashboard', 'KYC', 'Rentals', 'Bikers', 'Investors', 'Fleet', 'Merchants', 'Swap Stations', 'Hubs', 'Maintenance', 'Accounting', 'Reports', 'Users', 'Portfolio', 'Withdraw', 'Deliveries'];
|
||
|
|
|
||
|
|
export default function RolesPage() {
|
||
|
|
const [roles, setRoles] = useState<Role[]>(mockRoles);
|
||
|
|
const [search, setSearch] = useState('');
|
||
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||
|
|
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||
|
|
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||
|
|
|
||
|
|
const [formData, setFormData] = useState({
|
||
|
|
name: '',
|
||
|
|
description: '',
|
||
|
|
});
|
||
|
|
|
||
|
|
const filteredRoles = roles.filter(r =>
|
||
|
|
r.name.toLowerCase().includes(search.toLowerCase()) ||
|
||
|
|
r.description.toLowerCase().includes(search.toLowerCase())
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleSave = () => {
|
||
|
|
if (!formData.name) return;
|
||
|
|
|
||
|
|
if (editingRole) {
|
||
|
|
setRoles(roles.map(r => r.id === editingRole.id ? { ...r, ...formData, isDefault: false } : r));
|
||
|
|
} else {
|
||
|
|
const newRole: Role = {
|
||
|
|
id: `ROLE-${String(roles.length + 1).padStart(3, '0')}`,
|
||
|
|
...formData,
|
||
|
|
isDefault: false,
|
||
|
|
permissions: defaultPermissions.map(p => ({ ...p, view: false, create: false, edit: false, delete: false }))
|
||
|
|
};
|
||
|
|
setRoles([...roles, newRole]);
|
||
|
|
}
|
||
|
|
|
||
|
|
setShowCreateModal(false);
|
||
|
|
setEditingRole(null);
|
||
|
|
setFormData({ name: '', description: '' });
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDelete = (id: string) => {
|
||
|
|
if (confirm('Are you sure you want to delete this role?')) {
|
||
|
|
setRoles(roles.filter(r => r.id !== id));
|
||
|
|
setSelectedRole(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleDuplicate = (role: Role) => {
|
||
|
|
const newRole: Role = {
|
||
|
|
id: `ROLE-${String(roles.length + 1).padStart(3, '0')}`,
|
||
|
|
name: `${role.name} (Copy)`,
|
||
|
|
description: role.description,
|
||
|
|
isDefault: false,
|
||
|
|
permissions: role.permissions.map(p => ({ ...p }))
|
||
|
|
};
|
||
|
|
setRoles([...roles, newRole]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const openEdit = (role: Role) => {
|
||
|
|
setEditingRole(role);
|
||
|
|
setFormData({
|
||
|
|
name: role.name,
|
||
|
|
description: role.description
|
||
|
|
});
|
||
|
|
setShowCreateModal(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const togglePermission = (moduleIndex: number, action: 'view' | 'create' | 'edit' | 'delete') => {
|
||
|
|
if (!selectedRole) return;
|
||
|
|
const updated = [...selectedRole.permissions];
|
||
|
|
updated[moduleIndex][action] = !updated[moduleIndex][action];
|
||
|
|
setSelectedRole({ ...selectedRole, permissions: updated });
|
||
|
|
setRoles(roles.map(r => r.id === selectedRole.id ? { ...selectedRole, permissions: updated } : r));
|
||
|
|
};
|
||
|
|
|
||
|
|
const getPermissionCount = (role: Role, action: keyof Permission) => {
|
||
|
|
return role.permissions.filter(p => p[action] === true).length;
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-4 lg:p-6">
|
||
|
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6">
|
||
|
|
<div>
|
||
|
|
<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>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setEditingRole(null);
|
||
|
|
setFormData({ name: '', description: '' });
|
||
|
|
setShowCreateModal(true);
|
||
|
|
}}
|
||
|
|
className="py-2.5 px-4 bg-accent text-white rounded-lg font-semibold text-sm hover:bg-accent-dark transition-colors flex items-center gap-2"
|
||
|
|
>
|
||
|
|
<Plus className="w-4 h-4" /> Create Role
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||
|
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||
|
|
<p className="text-2xl font-extrabold text-slate-800">{roles.length}</p>
|
||
|
|
<p className="text-sm text-slate-500">Total Roles</p>
|
||
|
|
</div>
|
||
|
|
<div className="bg-white rounded-xl p-5 shadow-sm border border-slate-100">
|
||
|
|
<p className="text-2xl font-extrabold text-green-600">{roles.filter(r => r.isDefault).length}</p>
|
||
|
|
<p className="text-sm text-slate-500">Default Roles</p>
|
||
|
|
</div>
|
||
|
|
<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-sm text-slate-500">Total Create Perms</p>
|
||
|
|
</div>
|
||
|
|
<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-sm text-slate-500">Total View Perms</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid lg:grid-cols-3 gap-6">
|
||
|
|
<div className="lg:col-span-1 bg-white rounded-xl shadow-sm border border-slate-100">
|
||
|
|
<div className="p-4 border-b border-slate-100">
|
||
|
|
<div className="relative">
|
||
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
placeholder="Search roles..."
|
||
|
|
value={search}
|
||
|
|
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"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="divide-y divide-slate-50 max-h-[600px] overflow-y-auto">
|
||
|
|
{filteredRoles.map(role => (
|
||
|
|
<div
|
||
|
|
key={role.id}
|
||
|
|
className={`p-4 hover:bg-slate-50 cursor-pointer transition-colors ${selectedRole?.id === role.id ? 'bg-accent-light' : ''}`}
|
||
|
|
onClick={() => setSelectedRole(role)}
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium text-slate-700">{role.name}</p>
|
||
|
|
<p className="text-xs text-slate-400 truncate">{role.description}</p>
|
||
|
|
</div>
|
||
|
|
{role.isDefault && (
|
||
|
|
<span className="text-xs px-2 py-0.5 bg-accent-light text-accent rounded-full">Default</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-2 mt-2 text-xs text-slate-400">
|
||
|
|
<span>{getPermissionCount(role, 'view')} view</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>
|
||
|
|
|
||
|
|
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border border-slate-100">
|
||
|
|
{selectedRole ? (
|
||
|
|
<>
|
||
|
|
<div className="p-4 border-b border-slate-100 flex items-center justify-between">
|
||
|
|
<div>
|
||
|
|
<h3 className="text-lg font-semibold text-slate-800">{selectedRole.name}</h3>
|
||
|
|
<p className="text-sm text-slate-500">{selectedRole.description}</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<button onClick={() => handleDuplicate(selectedRole)} className="p-2 hover:bg-slate-100 rounded-lg text-slate-600">
|
||
|
|
<Copy className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
<button onClick={() => openEdit(selectedRole)} className="p-2 hover:bg-slate-100 rounded-lg text-slate-600">
|
||
|
|
<Edit className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
{!selectedRole.isDefault && (
|
||
|
|
<button onClick={() => handleDelete(selectedRole.id)} className="p-2 hover:bg-red-50 rounded-lg text-red-500">
|
||
|
|
<Trash2 className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="p-4">
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<h4 className="font-medium text-slate-800">Permissions</h4>
|
||
|
|
<div className="flex gap-4 text-xs">
|
||
|
|
<span className="text-slate-500">Check all <span className="font-medium text-green-600">view</span></span>
|
||
|
|
<span className="text-slate-500">Check all <span className="font-medium text-blue-600">create</span></span>
|
||
|
|
<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>
|
||
|
|
</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 className="p-12 text-center text-slate-500">
|
||
|
|
<Shield className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
||
|
|
<p className="text-sm">Select a role to view and edit permissions</p>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{showCreateModal && (
|
||
|
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
|
|
<div className="bg-white rounded-xl shadow-xl w-full max-w-md">
|
||
|
|
<div className="p-4 border-b border-slate-100 flex justify-between items-center">
|
||
|
|
<h3 className="font-semibold text-slate-800">{editingRole ? 'Edit Role' : 'Create New Role'}</h3>
|
||
|
|
<button onClick={() => setShowCreateModal(false)} className="text-slate-400 hover:text-slate-600">
|
||
|
|
<X className="w-5 h-5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="p-4 space-y-4">
|
||
|
|
<div>
|
||
|
|
<label className="text-sm text-slate-600">Role Name *</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
value={formData.name}
|
||
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||
|
|
placeholder="e.g., Super Manager"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="text-sm text-slate-600">Description</label>
|
||
|
|
<textarea
|
||
|
|
value={formData.description}
|
||
|
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||
|
|
className="w-full px-3 py-2 border border-slate-200 rounded-lg text-sm mt-1"
|
||
|
|
rows={2}
|
||
|
|
placeholder="Describe what this role can do"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="p-4 border-t border-slate-100 flex justify-end gap-2">
|
||
|
|
<button
|
||
|
|
onClick={() => setShowCreateModal(false)}
|
||
|
|
className="px-4 py-2 border border-slate-200 text-slate-600 rounded-lg text-sm"
|
||
|
|
>
|
||
|
|
Cancel
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
onClick={handleSave}
|
||
|
|
disabled={!formData.name}
|
||
|
|
className="px-4 py-2 bg-accent text-white rounded-lg text-sm disabled:opacity-50"
|
||
|
|
>
|
||
|
|
{editingRole ? 'Update' : 'Create'}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|