Files
membership-fe/src/pages/admin/AdminRoles.js
2025-12-16 20:04:00 +07:00

499 lines
17 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import api from '../../utils/api';
import { Card } from '../../components/ui/card';
import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input';
import { Label } from '../../components/ui/label';
import { Textarea } from '../../components/ui/textarea';
import { Checkbox } from '../../components/ui/checkbox';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../../components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '../../components/ui/alert-dialog';
import { toast } from 'sonner';
import { Shield, Plus, Edit, Trash2, Lock, ChevronDown, ChevronUp } from 'lucide-react';
const AdminRoles = () => {
const [roles, setRoles] = useState([]);
const [permissions, setPermissions] = useState([]);
const [selectedRole, setSelectedRole] = useState(null);
const [rolePermissions, setRolePermissions] = useState([]);
const [selectedPermissions, setSelectedPermissions] = useState([]);
const [loading, setLoading] = useState(true);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showPermissionsModal, setShowPermissionsModal] = useState(false);
const [expandedModules, setExpandedModules] = useState({});
const [formData, setFormData] = useState({
code: '',
name: '',
description: '',
permissions: []
});
useEffect(() => {
fetchRoles();
fetchPermissions();
}, []);
const fetchRoles = async () => {
try {
const response = await api.get('/admin/roles');
setRoles(response.data);
setLoading(false);
} catch (error) {
toast.error('Failed to fetch roles');
setLoading(false);
}
};
const fetchPermissions = async () => {
try {
const response = await api.get('/admin/permissions');
setPermissions(response.data);
// Expand all modules by default
const modules = [...new Set(response.data.map(p => p.module))];
const expanded = {};
modules.forEach(module => {
expanded[module] = true;
});
setExpandedModules(expanded);
} catch (error) {
toast.error('Failed to fetch permissions');
}
};
const fetchRolePermissions = async (roleId) => {
try {
const response = await api.get(`/admin/roles/${roleId}/permissions`);
setRolePermissions(response.data.permissions);
setSelectedPermissions(response.data.permissions.map(p => p.code));
} catch (error) {
toast.error('Failed to fetch role permissions');
}
};
const handleCreateRole = async () => {
try {
await api.post('/admin/roles', {
code: formData.code,
name: formData.name,
description: formData.description,
permission_codes: formData.permissions
});
toast.success('Role created successfully');
setShowCreateModal(false);
setFormData({ code: '', name: '', description: '', permissions: [] });
fetchRoles();
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to create role');
}
};
const handleUpdateRole = async () => {
try {
await api.put(`/admin/roles/${selectedRole.id}`, {
name: formData.name,
description: formData.description
});
toast.success('Role updated successfully');
setShowEditModal(false);
fetchRoles();
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to update role');
}
};
const handleDeleteRole = async () => {
try {
await api.delete(`/admin/roles/${selectedRole.id}`);
toast.success('Role deleted successfully');
setShowDeleteDialog(false);
setSelectedRole(null);
fetchRoles();
} catch (error) {
toast.error(error.response?.data?.detail || 'Failed to delete role');
}
};
const handleSavePermissions = async () => {
try {
await api.put(`/admin/roles/${selectedRole.id}/permissions`, {
permission_codes: selectedPermissions
});
toast.success('Permissions updated successfully');
setShowPermissionsModal(false);
fetchRoles();
} catch (error) {
toast.error('Failed to update permissions');
}
};
const togglePermission = (permissionCode) => {
setSelectedPermissions(prev => {
if (prev.includes(permissionCode)) {
return prev.filter(p => p !== permissionCode);
} else {
return [...prev, permissionCode];
}
});
};
const toggleModule = (module) => {
setExpandedModules(prev => ({
...prev,
[module]: !prev[module]
}));
};
const groupPermissionsByModule = () => {
const grouped = {};
permissions.forEach(perm => {
if (!grouped[perm.module]) {
grouped[perm.module] = [];
}
grouped[perm.module].push(perm);
});
return grouped;
};
if (loading) {
return (
<div className="flex justify-center items-center h-64">
<div className="text-gray-500">Loading roles...</div>
</div>
);
}
const groupedPermissions = groupPermissionsByModule();
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Role Management</h1>
<p className="text-gray-600 mt-1">
Create and manage custom roles with specific permissions
</p>
</div>
<Button onClick={() => setShowCreateModal(true)}>
<Plus className="w-4 h-4 mr-2" />
Create Role
</Button>
</div>
{/* Roles Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{roles.map(role => (
<Card key={role.id} className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center">
<Shield className="w-5 h-5 text-blue-600 mr-2" />
<div>
<h3 className="font-semibold">{role.name}</h3>
<p className="text-sm text-gray-500">{role.code}</p>
</div>
</div>
{role.is_system_role && (
<Lock className="w-4 h-4 text-gray-400" title="System Role" />
)}
</div>
<p className="text-sm text-gray-600 mb-4 min-h-[40px]">
{role.description || 'No description'}
</p>
<div className="flex items-center justify-between mb-4">
<span className="text-sm text-gray-500">
{role.permission_count} permissions
</span>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedRole(role);
fetchRolePermissions(role.id);
setShowPermissionsModal(true);
}}
>
Manage Permissions
</Button>
{!role.is_system_role && (
<>
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedRole(role);
setFormData({
name: role.name,
description: role.description || ''
});
setShowEditModal(true);
}}
>
<Edit className="w-3 h-3" />
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => {
setSelectedRole(role);
setShowDeleteDialog(true);
}}
>
<Trash2 className="w-3 h-3" />
</Button>
</>
)}
</div>
</Card>
))}
</div>
{/* Create Role Modal */}
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Role</DialogTitle>
<DialogDescription>
Create a custom role with specific permissions for your team members.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Role Code *</Label>
<Input
placeholder="e.g., content_editor, finance_manager"
value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
/>
<p className="text-xs text-gray-500 mt-1">
Lowercase, no spaces. Used internally to identify the role.
</p>
</div>
<div>
<Label>Role Name *</Label>
<Input
placeholder="e.g., Content Editor, Finance Manager"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
placeholder="Describe what this role can do..."
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
<div>
<Label>Permissions</Label>
<p className="text-sm text-gray-600 mb-3">
Select permissions for this role. You can also add permissions later.
</p>
<div className="border rounded-lg p-4 max-h-64 overflow-y-auto">
{Object.entries(groupedPermissions).map(([module, perms]) => (
<div key={module} className="mb-4">
<button
onClick={() => toggleModule(module)}
className="flex items-center w-full text-left font-medium mb-2 hover:text-blue-600"
>
{expandedModules[module] ? (
<ChevronUp className="w-4 h-4 mr-1" />
) : (
<ChevronDown className="w-4 h-4 mr-1" />
)}
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
</button>
{expandedModules[module] && (
<div className="space-y-2 ml-5">
{perms.map(perm => (
<div key={perm.code} className="flex items-center">
<Checkbox
checked={formData.permissions.includes(perm.code)}
onCheckedChange={() => {
setFormData(prev => ({
...prev,
permissions: prev.permissions.includes(perm.code)
? prev.permissions.filter(p => p !== perm.code)
: [...prev.permissions, perm.code]
}));
}}
/>
<label className="ml-2 text-sm">
<span className="font-medium">{perm.name}</span>
<span className="text-gray-500 ml-2">({perm.code})</span>
</label>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowCreateModal(false)}>
Cancel
</Button>
<Button onClick={handleCreateRole}>
Create Role
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Role Modal */}
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Role</DialogTitle>
<DialogDescription>
Update role name and description. Code cannot be changed.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Role Code</Label>
<Input value={selectedRole?.code || ''} disabled />
</div>
<div>
<Label>Role Name *</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
</div>
<div>
<Label>Description</Label>
<Textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowEditModal(false)}>
Cancel
</Button>
<Button onClick={handleUpdateRole}>
Save Changes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Manage Permissions Modal */}
<Dialog open={showPermissionsModal} onOpenChange={setShowPermissionsModal}>
<DialogContent className="max-w-3xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Manage Permissions: {selectedRole?.name}</DialogTitle>
<DialogDescription>
Select which permissions this role should have.
</DialogDescription>
</DialogHeader>
<div className="border rounded-lg p-4">
{Object.entries(groupedPermissions).map(([module, perms]) => (
<div key={module} className="mb-6">
<button
onClick={() => toggleModule(module)}
className="flex items-center w-full text-left font-medium text-lg mb-3 hover:text-blue-600"
>
{expandedModules[module] ? (
<ChevronUp className="w-5 h-5 mr-2" />
) : (
<ChevronDown className="w-5 h-5 mr-2" />
)}
{module.charAt(0).toUpperCase() + module.slice(1)} ({perms.length})
</button>
{expandedModules[module] && (
<div className="space-y-3 ml-7">
{perms.map(perm => (
<div key={perm.code} className="flex items-start">
<Checkbox
checked={selectedPermissions.includes(perm.code)}
onCheckedChange={() => togglePermission(perm.code)}
/>
<div className="ml-3">
<label className="font-medium text-sm">
{perm.name}
</label>
<p className="text-xs text-gray-500">{perm.description}</p>
<span className="text-xs text-gray-400">{perm.code}</span>
</div>
</div>
))}
</div>
)}
</div>
))}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowPermissionsModal(false)}>
Cancel
</Button>
<Button onClick={handleSavePermissions}>
Save Permissions
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Delete Confirmation */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Role?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the role "{selectedRole?.name}"?
This action cannot be undone. Users with this role will need to be reassigned.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteRole} className="bg-red-600">
Delete Role
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
export default AdminRoles;