Merge from dev to loaf-prod for DEMO #25
@@ -39,6 +39,7 @@ const AdminRoles = () => {
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showPermissionsModal, setShowPermissionsModal] = useState(false);
|
||||
const [expandedModules, setExpandedModules] = useState({});
|
||||
const [savingPermissions, setSavingPermissions] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
code: '',
|
||||
name: '',
|
||||
@@ -46,6 +47,15 @@ const AdminRoles = () => {
|
||||
permissions: []
|
||||
});
|
||||
|
||||
const formatRoleSlug = (value) => (
|
||||
value
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/_+/g, '-')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoles();
|
||||
fetchPermissions();
|
||||
@@ -133,6 +143,7 @@ const AdminRoles = () => {
|
||||
};
|
||||
|
||||
const handleSavePermissions = async () => {
|
||||
setSavingPermissions(true);
|
||||
try {
|
||||
await api.put(`/admin/roles/${selectedRole.id}/permissions`, {
|
||||
permission_codes: selectedPermissions
|
||||
@@ -142,6 +153,8 @@ const AdminRoles = () => {
|
||||
fetchRoles();
|
||||
} catch (error) {
|
||||
toast.error('Failed to update permissions');
|
||||
} finally {
|
||||
setSavingPermissions(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -155,6 +168,14 @@ const AdminRoles = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const addPermissions = (permissionCodes) => {
|
||||
setSelectedPermissions(prev => [...new Set([...prev, ...permissionCodes])]);
|
||||
};
|
||||
|
||||
const removePermissions = (permissionCodes) => {
|
||||
setSelectedPermissions(prev => prev.filter(code => !permissionCodes.includes(code)));
|
||||
};
|
||||
|
||||
const toggleModule = (module) => {
|
||||
setExpandedModules(prev => ({
|
||||
...prev,
|
||||
@@ -282,8 +303,28 @@ const AdminRoles = () => {
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
<div>
|
||||
<Label>Role Code *</Label>
|
||||
<Label>Role Name *</Label>
|
||||
<Input
|
||||
placeholder="e.g., Content Editor, Finance Manager"
|
||||
value={formData.name}
|
||||
onChange={(e) => {
|
||||
const nextName = e.target.value;
|
||||
setFormData(prev => {
|
||||
const prevAuto = formatRoleSlug(prev.name);
|
||||
const isAuto = !prev.code || prev.code === prevAuto;
|
||||
return {
|
||||
...prev,
|
||||
name: nextName,
|
||||
code: isAuto ? formatRoleSlug(nextName) : prev.code
|
||||
};
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Role Slug *</Label>
|
||||
<Input
|
||||
placeholder="e.g., content_editor, finance_manager"
|
||||
value={formData.code}
|
||||
@@ -294,15 +335,6 @@ const AdminRoles = () => {
|
||||
</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
|
||||
@@ -382,10 +414,6 @@ const AdminRoles = () => {
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Role Code</Label>
|
||||
<Input value={selectedRole?.code || ''} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Role Name *</Label>
|
||||
@@ -395,6 +423,11 @@ const AdminRoles = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Role Slug</Label>
|
||||
<Input value={selectedRole?.code || ''} disabled />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Textarea
|
||||
@@ -426,48 +459,77 @@ const AdminRoles = () => {
|
||||
</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>
|
||||
))}
|
||||
{Object.entries(groupedPermissions).map(([module, perms]) => {
|
||||
const moduleCodes = perms.map(perm => perm.code);
|
||||
const selectedCount = moduleCodes.filter(code => selectedPermissions.includes(code)).length;
|
||||
const hasPermissions = moduleCodes.length > 0;
|
||||
const isAllSelected = hasPermissions && selectedCount === moduleCodes.length;
|
||||
const isNoneSelected = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<div key={module} className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleModule(module)}
|
||||
className="flex items-center text-left font-medium text-lg 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>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addPermissions(moduleCodes)}
|
||||
disabled={!hasPermissions || isAllSelected}
|
||||
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Select all
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removePermissions(moduleCodes)}
|
||||
disabled={!hasPermissions || isNoneSelected}
|
||||
className="text-xs font-medium text-gray-500 hover:text-brand-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Deselect all
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{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 onClick={handleSavePermissions} disabled={savingPermissions}>
|
||||
{savingPermissions ? 'Saving...' : 'Save Permissions'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -491,6 +553,15 @@ const AdminRoles = () => {
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{savingPermissions && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||
<div className="bg-white rounded-xl shadow-lg px-6 py-5 text-center">
|
||||
<div className="mx-auto h-10 w-10 animate-spin rounded-full border-4 border-[var(--neutral-800)] border-t-transparent" />
|
||||
<p className="mt-4 text-sm font-medium text-gray-700">Saving permissions...</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user