error
import { useState, useRef, useEffect, useImperativeHandle, forwardRef } from "react";import { Button } from "../../../Shared/Components/Button/Button";import { Input } from "../../../Shared/Components/Input/Input";import { Card, CardContent } from "../../../Shared/Components/Card/Card";import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,} from "../../../Shared/Components/Dialog/Dialog";import { Collapsible, CollapsibleContent, CollapsibleTrigger,} from "../../../Shared/Components/Collapsible/Collapsible";import { toast } from "sonner";import { Switch } from "../../../Shared/Components/Switch/Switch";import { Textarea } from "../../../Shared/Components/Textarea/Textarea";import { ChevronDown, ChevronRight, RefreshCcw } from "lucide-react";import { apiService } from "../../../Services/apiService";
const ROLES_STORAGE_KEY = 'user_management_roles_legacy';const PERMISSIONS_STORAGE_KEY = 'user_management_permissions_legacy';
const buildDefaultPermissionAreas = () => [ { label: "User Management", key: "userManagement", children: ["view","edit","create","delete"].map(a => ({ label: a.charAt(0).toUpperCase()+a.slice(1), value: a, field: `user_management_${a}` })) }, { label: "Settings", key: "settings", children: ["view","edit","create","delete"].map(a => ({ label: a.charAt(0).toUpperCase()+a.slice(1), value: a, field: `settings_${a}` })) },];
function normalizePermissionSchema(raw) { if (!raw) return buildDefaultPermissionAreas(); // API returns: { status: "success", data: { permissions: [...] } } if (Array.isArray(raw)) { return raw.map((area) => { const key = area.key || area.area_key || area.module_key || (area.label ? area.label.toLowerCase().replace(/\s+/g,'_') : 'unknown'); const childrenRaw = area.children || area.actions || area.permissions || []; const children = Array.isArray(childrenRaw) ? childrenRaw.map((c) => ({ label: c.label || c.name || c.value || c.action || c.permission || '', value: c.value || c.action || c.permission || c.label?.toLowerCase() || c.name?.toLowerCase() || 'unknown', field: c.field || `${key}_${c.value || c.action || c.permission || c.label || c.name}` })) : Object.keys(childrenRaw).map(k => ({ label: k.charAt(0).toUpperCase()+k.slice(1), value: k, field: `${key}_${k}` })); return { label: area.label || key, key, children }; }); } if (typeof raw === 'object') { return Object.keys(raw).map(areaKey => { const val = raw[areaKey]; let children = []; if (Array.isArray(val)) { children = val.map(v => ({ label: v.charAt(0).toUpperCase()+v.slice(1), value: v, field: `${areaKey}_${v}` })); } else if (typeof val === 'object') { children = Object.keys(val).map(v => ({ label: v.charAt(0).toUpperCase()+v.slice(1), value: v, field: `${areaKey}_${v}` })); } return { label: areaKey.replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase()), key: areaKey, children }; }); } return buildDefaultPermissionAreas();}
function PermissionToggle({ label, description, checked, onCheckedChange,}) { return ( <div className="flex items-center justify-between py-2"> <div className="flex-1"> <div className="flex items-center gap-3"> <Switch checked={checked} onCheckedChange={onCheckedChange} /> <div> <p className="text-sm font-medium">{label}</p> <p className="text-xs text-gray-500">{description}</p> </div> </div> </div> </div> );}
function PermissionSection({ title, permissions, permissionKeys, onPermissionChange,}) { const [isOpen, setIsOpen] = useState(true);
const permissionDescriptions = (key) => { return `Can ${key.toLowerCase()} ${title.toLowerCase()}`; };
return ( <Collapsible open={isOpen} onOpenChange={setIsOpen}> <CollapsibleTrigger className="flex items-center justify-between w-full p-3 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors"> <h4 className="font-medium">{title}</h4> {isOpen ? ( <ChevronDown className="w-4 h-4" /> ) : ( <ChevronRight className="w-4 h-4" /> )} </CollapsibleTrigger> <CollapsibleContent className="px-3 pb-3"> <div className="space-y-2 pt-2"> {permissionKeys.map((key) => ( <PermissionToggle key={key} label={key.charAt(0).toUpperCase() + key.slice(1)} description={permissionDescriptions(key)} checked={permissions[key] || false} onCheckedChange={(checked) => onPermissionChange(key, checked)} /> ))} </div> </CollapsibleContent> </Collapsible> );}
const UserRoles = forwardRef(({ currentUser }, ref) => { const [showCreateForm, setShowCreateForm] = useState(false); const [editingRole, setEditingRole] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [deletingRole, setDeletingRole] = useState(null); const [permissionAreas, setPermissionAreas] = useState([]); const [userRoleFormData, setUserRoleFormData] = useState({ role_id: "", description: "", active: true, role_name: "", permissions: {} }); const [refreshTrigger, setRefreshTrigger] = useState(0); const [userRoleRowData, setUserRoleRowData] = useState([]); const [loading, setLoading] = useState(true); const fetchingRef = useRef(false); const lastFetchedUserIdRef = useRef(null);
const fetchRolesFromAPI = async (force = false) => { if (!currentUser?.user_id) { console.debug('[UserRoles] No currentUser.user_id yet; skipping fetch'); if (loading) setLoading(false); return; } if (fetchingRef.current) return; if (!force && lastFetchedUserIdRef.current === currentUser.user_id && userRoleRowData.length > 0) return;
try { fetchingRef.current = true; setLoading(true); console.debug('[UserRoles] Fetching roles for user_id:', currentUser.user_id, 'force=', force); const payload = { user_id: currentUser.user_id }; const res = await apiService.fetchUserRoles(payload); if (res.status === 'success' && res.data) { let roles = []; if (res.data.roles) roles = res.data.roles; else if (res.data.table_data?.body_content) roles = res.data.table_data.body_content; else if (Array.isArray(res.data)) roles = res.data;
setUserRoleRowData(roles); lastFetchedUserIdRef.current = currentUser.user_id; console.debug('[UserRoles] Roles fetched count:', roles.length); } else { toast.error(res.message || 'Failed to fetch roles'); console.warn('[UserRoles] Fetch roles failed:', res.message); } } catch (e) { console.error('Failed to fetch roles', e); toast.error('Error fetching roles'); } finally { fetchingRef.current = false; setLoading(false); } };
useImperativeHandle(ref, () => ({ openCreateDialog: () => { handleCreate(); } }));
useEffect(() => { fetchRolesFromAPI(); }, [refreshTrigger, currentUser?.user_id]);
const loadPermissionAreas = async (targetRole) => { if (!currentUser?.user_id) { setPermissionAreas(buildDefaultPermissionAreas()); return; } try { const payload = { user_id: currentUser.user_id }; if (targetRole) payload.role_id = targetRole.role_id; const res = await apiService.fetchRoleConfig(payload); // FIXED: Properly access data.permissions from API response // API structure: { status: "success", data: { permissions: [...] } } let rawSchema = res?.data?.permissions; console.log('[loadPermissionAreas] API Response:', res); console.log('[loadPermissionAreas] Raw schema extracted:', rawSchema); if (!rawSchema) { // Fallback chain for different API response structures rawSchema = res?.data?.permission_areas || res?.data?.modules || res?.data; if (!rawSchema && res?.data?.table_data?.permission_areas) { rawSchema = res.data.table_data.permission_areas; } } const areas = normalizePermissionSchema(rawSchema); console.log('[loadPermissionAreas] Normalized areas:', areas); setPermissionAreas(areas); return areas; } catch (e) { console.warn('Permission schema fetch failed, using fallback:', e); const fallback = buildDefaultPermissionAreas(); setPermissionAreas(fallback); return fallback; } };
const handleEdit = async (role) => { try { const areas = await loadPermissionAreas(role) || buildDefaultPermissionAreas(); const initializedPermissions = areas.reduce( (acc, area) => ({ ...acc, [area.key]: area.children.reduce( (permAcc, perm) => ({ ...permAcc, [perm.value]: role.permissions?.[area.key]?.[perm.value] || false, }), {} ), }), {} ); setUserRoleFormData({ role_name: role.role_name, description: role.description, permissions: initializedPermissions, role_id: role.role_id, }); setEditingRole(role); setShowCreateForm(true); } catch (error) { console.error("Failed to edit user role:", error); toast.error("Failed to load role details. Please try again."); } };
const handleCreate = async () => { try { const areas = await loadPermissionAreas() || buildDefaultPermissionAreas(); const initializedPermissions = areas.reduce( (acc, area) => ({ ...acc, [area.key]: area.children.reduce( (permAcc, perm) => ({ ...permAcc, [perm.value]: false, }), {} ), }), {} ); setUserRoleFormData({ role_name: "", description: "", permissions: initializedPermissions, role_id: "", }); setShowCreateForm(true); } catch (error) { console.error("Failed to initialize form:", error); toast.error("Failed to load permissions. Please try again."); } };
const handlePermissionChange = (section, permission, value) => { setUserRoleFormData((prev) => ({ ...prev, permissions: { ...(prev.permissions ?? {}), [section]: { ...(prev.permissions?.[section] ?? {}), [permission]: value, }, }, })); };
const handleDelete = (role) => { setDeletingRole(role); setShowDeleteDialog(true); };
const confirmDeleteRole = async () => { if (!deletingRole || !currentUser?.user_id) return; try { const payload = { user_id: currentUser.user_id, delete_role_id: deletingRole.role_id }; const res = await apiService.deleteRole(payload); if (res.status === 'success') { toast.success(`Role "${deletingRole.role_name}" deleted.`); setShowDeleteDialog(false); setDeletingRole(null); fetchRolesFromAPI(true); } else { toast.error(res.message || 'Failed to delete role'); } } catch (e) { console.error('Delete role failed', e); toast.error('Error deleting role'); } };
const handleSave = async () => { if (!userRoleFormData.role_name.trim()) { toast.error("Role name is required"); return; } if (!currentUser?.user_id) { toast.error('User context missing'); return; }
try { const payloadBase = { user_id: currentUser.user_id, role_name: userRoleFormData.role_name, role_description: userRoleFormData.description, permissions: userRoleFormData.permissions };
if (editingRole) { const editPayload = { ...payloadBase, role_id: editingRole.role_id }; const res = await apiService.editSaveRole(editPayload); if (res.status === 'success') { toast.success('Role updated successfully'); handleCancel(); fetchRolesFromAPI(true); } else { toast.error(res.message || 'Failed to update role'); } } else { const createPayload = { ...payloadBase }; const res = await apiService.createRole(createPayload); if (res.status === 'success') { toast.success('Role created successfully'); handleCancel(); fetchRolesFromAPI(true); } else { toast.error(res.message || 'Failed to create role'); } } } catch (e) { console.error('Save role failed', e); toast.error('Error saving role'); } };
const handleCancel = () => { setUserRoleFormData({ role_name: "", description: "", permissions: {}, role_id: "", }); setShowCreateForm(false); setEditingRole(null); setPermissionAreas([]); };
if (showCreateForm) { return ( <div className="space-y-4"> <div className="flex justify-between items-center"> <div> <h3 className="text-lg font-medium"> {editingRole ? "Edit User Role" : "Create New User Role"} </h3> <p className="text-sm text-gray-600 mt-1"> {editingRole ? "Update the role information and permissions below." : "Fill in the role information and configure permissions below."} </p> </div> <Button variant="outline" onClick={handleCancel}> Back to Roles </Button> </div> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="space-y-6"> <Card> <CardContent className="p-6"> <h4 className="font-medium mb-4">Role Information</h4> <div className="space-y-4"> <div> <label className="text-sm font-medium text-black mb-2 block"> Role Name * </label> <Input value={userRoleFormData.role_name} onChange={(e) => setUserRoleFormData({ ...userRoleFormData, role_name: e.target.value, }) } placeholder="Enter role name" className="w-full" required /> </div> <div> <label className="text-sm font-medium text-black mb-2 block"> Description </label> <Textarea value={userRoleFormData.description} onChange={(e) => setUserRoleFormData({ ...userRoleFormData, description: e.target.value, }) } placeholder="Enter role description" className="w-full min-h-[80px]" rows={3} /> </div> </div> </CardContent> </Card> </div> <div className="space-y-6"> <Card> <CardContent className="p-6"> <h4 className="font-medium mb-4">Access Permissions</h4> <div className="space-y-3"> {permissionAreas.map((area) => ( <PermissionSection key={area.key} title={area.label} permissions={userRoleFormData.permissions ? userRoleFormData.permissions[area.key] || {} : {}} permissionKeys={area.children.map((perm) => perm.value)} onPermissionChange={(permission, value) => handlePermissionChange(area.key, permission, value) } /> ))} {!permissionAreas.length && ( <p className="text-sm text-gray-500"> No permission areas available. Please try again. </p> )} </div> </CardContent> </Card> <div className="flex justify-end gap-3"> <Button variant="outline" onClick={handleCancel}> Cancel </Button> <Button onClick={handleSave} className="bg-[#00052e] text-white hover:bg-[#00052e]/90" > {editingRole ? "Update Role" : "Create Role"} </Button> </div> </div> </div> </div> ); }
return ( <div className="space-y-4"> <div className="flex items-center justify-between"> <h3 className="text-lg font-medium">User Roles</h3> <div className="flex gap-2"> <Button variant="outline" onClick={() => fetchRolesFromAPI(true)} disabled={loading} className="flex items-center gap-2"> <RefreshCcw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> <span className="hidden sm:inline">Refresh</span> </Button> <Button onClick={handleCreate} className="bg-[#00052e] text-white hover:bg-[#00052e]/90">Create Role</Button> </div> </div> <div className="border border-gray-200 rounded-lg overflow-hidden"> <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role Name</th> <th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th> <th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {loading ? ( <tr> <td colSpan={3} className="px-3 sm:px-6 py-8 text-center"> <div className="flex justify-center items-center"> <div className="text-gray-500">Loading...</div> </div> </td> </tr> ) : userRoleRowData.length === 0 ? ( <tr> <td colSpan={3} className="px-3 sm:px-6 py-8 text-center text-gray-500 text-sm"> No user roles found. </td> </tr> ) : ( userRoleRowData.map((role) => ( <tr key={role.id} className="hover:bg-gray-50"> <td className="px-3 sm:px-6 py-4 whitespace-nowrap text-xs sm:text-sm text-gray-900"> {role.role_name} </td> <td className="px-3 sm:px-6 py-4 text-xs sm:text-sm text-gray-900 max-w-48 sm:max-w-none truncate"> {role.description} </td> <td className="px-3 sm:px-6 py-4 whitespace-nowrap text-xs sm:text-sm font-medium"> <div className="flex gap-1 sm:gap-2"> <button onClick={() => handleEdit(role)} className="p-1 hover:bg-gray-100 rounded" style={{ color: '#00052e' }} title="Edit Role" > <svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> </svg> </button> <button onClick={() => handleDelete(role)} className="p-1 hover:bg-gray-100 rounded" style={{ color: '#00052e' }} title="Delete Role" > <svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> </svg> </button> </div> </td> </tr> )) )} </tbody> </table> </div> </div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 sm:p-4 bg-gray-50 border border-gray-200 rounded-lg"> <div className="flex items-center space-x-6 text-xs sm:text-sm"> <div className="flex items-center gap-1"> <span className="font-normal">Total Roles</span> <span>:</span> <span className="font-medium">{userRoleRowData.length}</span> </div> </div> </div> <Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}> <DialogContent className="max-w-md"> <DialogHeader> <DialogTitle>Delete Role</DialogTitle> <DialogDescription> Are you sure you want to delete the role "{deletingRole?.role_name}"? This action cannot be undone. </DialogDescription> </DialogHeader> <div className="py-4"> <p className="text-sm text-gray-600"> This will permanently remove the role from the system. Users assigned to this role may lose their permissions. </p> </div> <div className="flex justify-end gap-3"> <Button variant="outline" onClick={() => setShowDeleteDialog(false)}> Cancel </Button> <Button onClick={confirmDeleteRole} className="bg-[#00052e] hover:bg-[#00052e]/90 text-white" > Delete Role </Button> </div> </DialogContent> </Dialog> </div> );});
UserRoles.displayName = 'UserRoles';
export default UserRoles;
import { useState, useRef, useEffect, useImperativeHandle, forwardRef } from "react";
import { Button } from "../../../Shared/Components/Button/Button";
import { Input } from "../../../Shared/Components/Input/Input";
import { Card, CardContent } from "../../../Shared/Components/Card/Card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "../../../Shared/Components/Dialog/Dialog";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "../../../Shared/Components/Collapsible/Collapsible";
import { toast } from "sonner";
import { Switch } from "../../../Shared/Components/Switch/Switch";
import { Textarea } from "../../../Shared/Components/Textarea/Textarea";
import { ChevronDown, ChevronRight, RefreshCcw } from "lucide-react";
import { apiService } from "../../../Services/apiService";
const ROLES_STORAGE_KEY = 'user_management_roles_legacy';
const PERMISSIONS_STORAGE_KEY = 'user_management_permissions_legacy';
const buildDefaultPermissionAreas = () => [
{
label: "User Management",
key: "userManagement",
children: ["view","edit","create","delete"].map(a => ({ label: a.charAt(0).toUpperCase()+a.slice(1), value: a, field: `user_management_${a}` }))
},
{
label: "Settings",
key: "settings",
children: ["view","edit","create","delete"].map(a => ({ label: a.charAt(0).toUpperCase()+a.slice(1), value: a, field: `settings_${a}` }))
},
];
function normalizePermissionSchema(raw) {
if (!raw) return buildDefaultPermissionAreas();
// API returns: { status: "success", data: { permissions: [...] } }
if (Array.isArray(raw)) {
return raw.map((area) => {
const key = area.key || area.area_key || area.module_key || (area.label ? area.label.toLowerCase().replace(/\s+/g,'_') : 'unknown');
const childrenRaw = area.children || area.actions || area.permissions || [];
const children = Array.isArray(childrenRaw)
? childrenRaw.map((c) => ({
label: c.label || c.name || c.value || c.action || c.permission || '',
value: c.value || c.action || c.permission || c.label?.toLowerCase() || c.name?.toLowerCase() || 'unknown',
field: c.field || `${key}_${c.value || c.action || c.permission || c.label || c.name}`
}))
: Object.keys(childrenRaw).map(k => ({
label: k.charAt(0).toUpperCase()+k.slice(1),
value: k,
field: `${key}_${k}`
}));
return { label: area.label || key, key, children };
});
}
if (typeof raw === 'object') {
return Object.keys(raw).map(areaKey => {
const val = raw[areaKey];
let children = [];
if (Array.isArray(val)) {
children = val.map(v => ({
label: v.charAt(0).toUpperCase()+v.slice(1),
value: v,
field: `${areaKey}_${v}`
}));
} else if (typeof val === 'object') {
children = Object.keys(val).map(v => ({
label: v.charAt(0).toUpperCase()+v.slice(1),
value: v,
field: `${areaKey}_${v}`
}));
}
return { label: areaKey.replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase()), key: areaKey, children };
});
}
return buildDefaultPermissionAreas();
}
function PermissionToggle({
label,
description,
checked,
onCheckedChange,
}) {
return (
<div className="flex items-center justify-between py-2">
<div className="flex-1">
<div className="flex items-center gap-3">
<Switch checked={checked} onCheckedChange={onCheckedChange} />
<div>
<p className="text-sm font-medium">{label}</p>
<p className="text-xs text-gray-500">{description}</p>
</div>
</div>
</div>
</div>
);
}
function PermissionSection({
title,
permissions,
permissionKeys,
onPermissionChange,
}) {
const [isOpen, setIsOpen] = useState(true);
const permissionDescriptions = (key) => {
return `Can ${key.toLowerCase()} ${title.toLowerCase()}`;
};
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger className="flex items-center justify-between w-full p-3 bg-gray-50 hover:bg-gray-100 rounded-lg transition-colors">
<h4 className="font-medium">{title}</h4>
{isOpen ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)}
</CollapsibleTrigger>
<CollapsibleContent className="px-3 pb-3">
<div className="space-y-2 pt-2">
{permissionKeys.map((key) => (
<PermissionToggle
key={key}
label={key.charAt(0).toUpperCase() + key.slice(1)}
description={permissionDescriptions(key)}
checked={permissions[key] || false}
onCheckedChange={(checked) => onPermissionChange(key, checked)}
/>
))}
</div>
</CollapsibleContent>
</Collapsible>
);
}
const UserRoles = forwardRef(({ currentUser }, ref) => {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingRole, setEditingRole] = useState(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [deletingRole, setDeletingRole] = useState(null);
const [permissionAreas, setPermissionAreas] = useState([]);
const [userRoleFormData, setUserRoleFormData] = useState({
role_id: "",
description: "",
active: true,
role_name: "",
permissions: {}
});
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [userRoleRowData, setUserRoleRowData] = useState([]);
const [loading, setLoading] = useState(true);
const fetchingRef = useRef(false);
const lastFetchedUserIdRef = useRef(null);
const fetchRolesFromAPI = async (force = false) => {
if (!currentUser?.user_id) {
console.debug('[UserRoles] No currentUser.user_id yet; skipping fetch');
if (loading) setLoading(false);
return;
}
if (fetchingRef.current) return;
if (!force && lastFetchedUserIdRef.current === currentUser.user_id && userRoleRowData.length > 0) return;
try {
fetchingRef.current = true;
setLoading(true);
console.debug('[UserRoles] Fetching roles for user_id:', currentUser.user_id, 'force=', force);
const payload = { user_id: currentUser.user_id };
const res = await apiService.fetchUserRoles(payload);
if (res.status === 'success' && res.data) {
let roles = [];
if (res.data.roles) roles = res.data.roles;
else if (res.data.table_data?.body_content) roles = res.data.table_data.body_content;
else if (Array.isArray(res.data)) roles = res.data;
setUserRoleRowData(roles);
lastFetchedUserIdRef.current = currentUser.user_id;
console.debug('[UserRoles] Roles fetched count:', roles.length);
} else {
toast.error(res.message || 'Failed to fetch roles');
console.warn('[UserRoles] Fetch roles failed:', res.message);
}
} catch (e) {
console.error('Failed to fetch roles', e);
toast.error('Error fetching roles');
} finally {
fetchingRef.current = false;
setLoading(false);
}
};
useImperativeHandle(ref, () => ({
openCreateDialog: () => {
handleCreate();
}
}));
useEffect(() => {
fetchRolesFromAPI();
}, [refreshTrigger, currentUser?.user_id]);
const loadPermissionAreas = async (targetRole) => {
if (!currentUser?.user_id) {
setPermissionAreas(buildDefaultPermissionAreas());
return;
}
try {
const payload = { user_id: currentUser.user_id };
if (targetRole) payload.role_id = targetRole.role_id;
const res = await apiService.fetchRoleConfig(payload);
// FIXED: Properly access data.permissions from API response
// API structure: { status: "success", data: { permissions: [...] } }
let rawSchema = res?.data?.permissions;
console.log('[loadPermissionAreas] API Response:', res);
console.log('[loadPermissionAreas] Raw schema extracted:', rawSchema);
if (!rawSchema) {
// Fallback chain for different API response structures
rawSchema = res?.data?.permission_areas || res?.data?.modules || res?.data;
if (!rawSchema && res?.data?.table_data?.permission_areas) {
rawSchema = res.data.table_data.permission_areas;
}
}
const areas = normalizePermissionSchema(rawSchema);
console.log('[loadPermissionAreas] Normalized areas:', areas);
setPermissionAreas(areas);
return areas;
} catch (e) {
console.warn('Permission schema fetch failed, using fallback:', e);
const fallback = buildDefaultPermissionAreas();
setPermissionAreas(fallback);
return fallback;
}
};
const handleEdit = async (role) => {
try {
const areas = await loadPermissionAreas(role) || buildDefaultPermissionAreas();
const initializedPermissions = areas.reduce(
(acc, area) => ({
...acc,
[area.key]: area.children.reduce(
(permAcc, perm) => ({
...permAcc,
[perm.value]: role.permissions?.[area.key]?.[perm.value] || false,
}),
{}
),
}),
{}
);
setUserRoleFormData({
role_name: role.role_name,
description: role.description,
permissions: initializedPermissions,
role_id: role.role_id,
});
setEditingRole(role);
setShowCreateForm(true);
} catch (error) {
console.error("Failed to edit user role:", error);
toast.error("Failed to load role details. Please try again.");
}
};
const handleCreate = async () => {
try {
const areas = await loadPermissionAreas() || buildDefaultPermissionAreas();
const initializedPermissions = areas.reduce(
(acc, area) => ({
...acc,
[area.key]: area.children.reduce(
(permAcc, perm) => ({
...permAcc,
[perm.value]: false,
}),
{}
),
}),
{}
);
setUserRoleFormData({
role_name: "",
description: "",
permissions: initializedPermissions,
role_id: "",
});
setShowCreateForm(true);
} catch (error) {
console.error("Failed to initialize form:", error);
toast.error("Failed to load permissions. Please try again.");
}
};
const handlePermissionChange = (section, permission, value) => {
setUserRoleFormData((prev) => ({
...prev,
permissions: {
...(prev.permissions ?? {}),
[section]: {
...(prev.permissions?.[section] ?? {}),
[permission]: value,
},
},
}));
};
const handleDelete = (role) => {
setDeletingRole(role);
setShowDeleteDialog(true);
};
const confirmDeleteRole = async () => {
if (!deletingRole || !currentUser?.user_id) return;
try {
const payload = { user_id: currentUser.user_id, delete_role_id: deletingRole.role_id };
const res = await apiService.deleteRole(payload);
if (res.status === 'success') {
toast.success(`Role "${deletingRole.role_name}" deleted.`);
setShowDeleteDialog(false);
setDeletingRole(null);
fetchRolesFromAPI(true);
} else {
toast.error(res.message || 'Failed to delete role');
}
} catch (e) {
console.error('Delete role failed', e);
toast.error('Error deleting role');
}
};
const handleSave = async () => {
if (!userRoleFormData.role_name.trim()) {
toast.error("Role name is required");
return;
}
if (!currentUser?.user_id) {
toast.error('User context missing');
return;
}
try {
const payloadBase = {
user_id: currentUser.user_id,
role_name: userRoleFormData.role_name,
role_description: userRoleFormData.description,
permissions: userRoleFormData.permissions
};
if (editingRole) {
const editPayload = { ...payloadBase, role_id: editingRole.role_id };
const res = await apiService.editSaveRole(editPayload);
if (res.status === 'success') {
toast.success('Role updated successfully');
handleCancel();
fetchRolesFromAPI(true);
} else {
toast.error(res.message || 'Failed to update role');
}
} else {
const createPayload = { ...payloadBase };
const res = await apiService.createRole(createPayload);
if (res.status === 'success') {
toast.success('Role created successfully');
handleCancel();
fetchRolesFromAPI(true);
} else {
toast.error(res.message || 'Failed to create role');
}
}
} catch (e) {
console.error('Save role failed', e);
toast.error('Error saving role');
}
};
const handleCancel = () => {
setUserRoleFormData({
role_name: "",
description: "",
permissions: {},
role_id: "",
});
setShowCreateForm(false);
setEditingRole(null);
setPermissionAreas([]);
};
if (showCreateForm) {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">
{editingRole ? "Edit User Role" : "Create New User Role"}
</h3>
<p className="text-sm text-gray-600 mt-1">
{editingRole
? "Update the role information and permissions below."
: "Fill in the role information and configure permissions below."}
</p>
</div>
<Button variant="outline" onClick={handleCancel}>
Back to Roles
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-6">
<Card>
<CardContent className="p-6">
<h4 className="font-medium mb-4">Role Information</h4>
<div className="space-y-4">
<div>
<label className="text-sm font-medium text-black mb-2 block">
Role Name *
</label>
<Input
value={userRoleFormData.role_name}
onChange={(e) =>
setUserRoleFormData({
...userRoleFormData,
role_name: e.target.value,
})
}
placeholder="Enter role name"
className="w-full"
required
/>
</div>
<div>
<label className="text-sm font-medium text-black mb-2 block">
Description
</label>
<Textarea
value={userRoleFormData.description}
onChange={(e) =>
setUserRoleFormData({
...userRoleFormData,
description: e.target.value,
})
}
placeholder="Enter role description"
className="w-full min-h-[80px]"
rows={3}
/>
</div>
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardContent className="p-6">
<h4 className="font-medium mb-4">Access Permissions</h4>
<div className="space-y-3">
{permissionAreas.map((area) => (
<PermissionSection
key={area.key}
title={area.label}
permissions={userRoleFormData.permissions ? userRoleFormData.permissions[area.key] || {} : {}}
permissionKeys={area.children.map((perm) => perm.value)}
onPermissionChange={(permission, value) =>
handlePermissionChange(area.key, permission, value)
}
/>
))}
{!permissionAreas.length && (
<p className="text-sm text-gray-500">
No permission areas available. Please try again.
</p>
)}
</div>
</CardContent>
</Card>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button
onClick={handleSave}
className="bg-[#00052e] text-white hover:bg-[#00052e]/90"
>
{editingRole ? "Update Role" : "Create Role"}
</Button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium">User Roles</h3>
<div className="flex gap-2">
<Button variant="outline" onClick={() => fetchRolesFromAPI(true)} disabled={loading} className="flex items-center gap-2">
<RefreshCcw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
<span className="hidden sm:inline">Refresh</span>
</Button>
<Button onClick={handleCreate} className="bg-[#00052e] text-white hover:bg-[#00052e]/90">Create Role</Button>
</div>
</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role Name</th>
<th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Description</th>
<th className="px-3 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{loading ? (
<tr>
<td colSpan={3} className="px-3 sm:px-6 py-8 text-center">
<div className="flex justify-center items-center">
<div className="text-gray-500">Loading...</div>
</div>
</td>
</tr>
) : userRoleRowData.length === 0 ? (
<tr>
<td colSpan={3} className="px-3 sm:px-6 py-8 text-center text-gray-500 text-sm">
No user roles found.
</td>
</tr>
) : (
userRoleRowData.map((role) => (
<tr key={role.id} className="hover:bg-gray-50">
<td className="px-3 sm:px-6 py-4 whitespace-nowrap text-xs sm:text-sm text-gray-900">
{role.role_name}
</td>
<td className="px-3 sm:px-6 py-4 text-xs sm:text-sm text-gray-900 max-w-48 sm:max-w-none truncate">
{role.description}
</td>
<td className="px-3 sm:px-6 py-4 whitespace-nowrap text-xs sm:text-sm font-medium">
<div className="flex gap-1 sm:gap-2">
<button
onClick={() => handleEdit(role)}
className="p-1 hover:bg-gray-100 rounded"
style={{ color: '#00052e' }}
title="Edit Role"
>
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button
onClick={() => handleDelete(role)}
className="p-1 hover:bg-gray-100 rounded"
style={{ color: '#00052e' }}
title="Delete Role"
>
<svg className="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-3 sm:p-4 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center space-x-6 text-xs sm:text-sm">
<div className="flex items-center gap-1">
<span className="font-normal">Total Roles</span>
<span>:</span>
<span className="font-medium">{userRoleRowData.length}</span>
</div>
</div>
</div>
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Delete Role</DialogTitle>
<DialogDescription>
Are you sure you want to delete the role "{deletingRole?.role_name}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<p className="text-sm text-gray-600">
This will permanently remove the role from the system. Users assigned to this role may lose their permissions.
</p>
</div>
<div className="flex justify-end gap-3">
<Button variant="outline" onClick={() => setShowDeleteDialog(false)}>
Cancel
</Button>
<Button
onClick={confirmDeleteRole}
className="bg-[#00052e] hover:bg-[#00052e]/90 text-white"
>
Delete Role
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
});
UserRoles.displayName = 'UserRoles';
export default UserRoles;
Comments
Post a Comment