Nn
import { useState, useRef, useEffect, type JSX } from "react";
import { type ColDef } from "ag-grid-community";
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 } from "lucide-react";
import AGGrid, {
type AGGridRef,
} from "../../../Shared/Components/Ag-grid/Ag-grid";
import ActionsRenderer from "../../../Shared/Components/CellRenderer/CellRenderer";
import { apiService } from "../../../Services/apiService";
// Use environment variable for API base URL
const BASE_URL =
import.meta.env.VITE_API_BASE_URL || "http://localhost:5000/api";
interface Permission {
label: string;
value: string;
field: string;
}
interface PermissionArea {
label: string;
key: string;
children: Permission[];
}
interface UserRole {
id: string;
role_id: string;
role_name: string;
description: string;
last_updated_time?: string;
last_updated_by?: string;
active?: boolean;
permissions?: Record<string, Record<string, boolean>>;
}
function PermissionToggle({
label,
description,
checked,
onCheckedChange,
}: {
label: string;
description: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
}) {
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,
}: {
title: string;
permissions: Record<string, boolean>;
permissionKeys: string[];
onPermissionChange: (permission: string, value: boolean) => void;
}) {
const [isOpen, setIsOpen] = useState(true);
const permissionDescriptions = (key: string) => {
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>
);
}
type UserRolesGridProps = {
onEdit: (role: UserRole) => void;
onDelete: (role: UserRole) => void;
refreshTrigger: number;
};
function UserRolesGrid({
onEdit,
onDelete,
refreshTrigger,
}: UserRolesGridProps) {
const gridRef = useRef<AGGridRef>(null);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);
const [userRoleRowData, setUserRoleRowData] = useState<UserRole[]>([]);
const [loading, setLoading] = useState(true);
const handleActionClick = (actionType: string, row: UserRole) => {
switch (actionType) {
case "edit":
onEdit(row);
break;
case "delete":
onDelete(row);
break;
default:
console.warn("Unhandled action:", actionType);
}
};
const payload = {
filterModel: [
{
search_text: "",
type: "contains",
column_name: "",
},
],
sortModel: {
sort: "",
column_name: "",
},
startRow: 0,
endRow: 100,
page: 1,
records: 50,
};
const fetchData = async () => {
try {
setLoading(true);
const response = await apiService.fetchUserRoles(payload);
if (!response) throw new Error("Failed to fetch roles");
const roles: UserRole[] = await response.json();
const columns: ColDef[] = [
{
headerName: "Role Name",
field: "role_name",
sortable: true,
resizable: true,
},
{
headerName: "Description",
field: "description",
sortable: true,
resizable: true,
},
{
headerName: "Actions",
field: "actions",
width: 100,
cellRendererFramework: ActionsRenderer,
cellRendererParams: {
actions: [
{
action: "edit",
tooltip: "Edit",
type: "edit",
class: "fa fa-pencil",
},
{
action: "delete",
tooltip: "Delete",
type: "delete",
class: "fa fa-trash",
},
],
onActionClick: handleActionClick,
},
suppressMenu: true,
sortable: false,
filter: false,
resizable: false,
},
];
setColumnDefs(columns);
setUserRoleRowData(roles);
} catch (error) {
console.error("Error fetching roles:", error);
toast.error("Failed to fetch roles from server");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [refreshTrigger]);
return (
<div className="space-y-4">
<div className="hidden md:block">
<div className="ag-theme-alpine" style={{ height: 400, width: "100%" }}>
{loading ? (
<div className="flex justify-center items-center h-full">
<div className="text-gray-500">Loading...</div>
</div>
) : userRoleRowData.length > 0 ? (
<AGGrid
ref={gridRef}
columnDefs={columnDefs}
cacheBlockSize={100}
maxBlocksInCache={5}
rowData={userRoleRowData}
height={400}
enableSorting={false}
enableFiltering={false}
enableResizing={true}
animateRows={true}
headerHeight={40}
rowHeight={35}
domLayout="normal"
/>
) : (
<div className="flex justify-center items-center h-full text-gray-500">
No user roles found.
</div>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-4 bg-white border border-[#b8bcbf]">
<div className="flex items-center space-x-6 text-sm font-['Roboto',sans-serif]">
<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>
</div>
);
}
export default function UserRoles() {
const [showCreateForm, setShowCreateForm] = useState(false);
const [editingRole, setEditingRole] = useState<UserRole | null>(null);
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [deletingRole, setDeletingRole] = useState<UserRole | null>(null);
const [permissionAreas, setPermissionAreas] = useState<PermissionArea[]>([]);
const [userRoleFormData, setUserRoleFormData] = useState<
Omit<UserRole, "id">
>({
role_id: "",
description: "",
active: true,
role_name: "",
permissions: {},
});
const [refreshTrigger, setRefreshTrigger] = useState(0);
const handleEdit = async (role: UserRole) => {
try {
const response = await fetch(`${BASE_URL}/permissions`);
if (!response.ok) throw new Error("Failed to fetch permissions");
const permissionAreas: PermissionArea[] = await response.json();
setPermissionAreas(permissionAreas);
const initializedPermissions = permissionAreas.reduce(
(acc: any, area) => ({
...acc,
[area.key]: area.children.reduce(
(permAcc, perm) => ({
...permAcc,
[perm.value]: role.permissions?.[area.key]?.[perm.value] || false,
}),
{} as Record<string, boolean>
),
}),
{} as Record<string, Record<string, boolean>>
);
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 role:", error);
toast.error("Failed to load role details. Please try again.");
}
};
const handleCreate = async () => {
try {
const response = await fetch(`${BASE_URL}/permissions`);
if (!response.ok) throw new Error("Failed to fetch permissions");
const permissionAreas: PermissionArea[] = await response.json();
setPermissionAreas(permissionAreas);
const initializedPermissions = permissionAreas.reduce(
(acc: any, area) => ({
...acc,
[area.key]: area.children.reduce(
(permAcc: any, perm) => ({
...permAcc,
[perm.value]: false,
}),
{} as Record<string, boolean>
),
}),
{} as Record<string, Record<string, boolean>>
);
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: string,
permission: string,
value: boolean
) => {
setUserRoleFormData((prev) => ({
...prev,
permissions: {
...(prev.permissions ?? {}),
[section]: {
...(prev.permissions?.[section] ?? {}),
[permission]: value,
},
},
}));
};
const handleDelete = (role: UserRole) => {
setDeletingRole(role);
setShowDeleteDialog(true);
};
const confirmDeleteRole = async () => {
if (!deletingRole) return;
try {
const response = await fetch(`${BASE_URL}/roles/${deletingRole.id}`, {
method: "DELETE",
});
if (!response.ok) throw new Error("Failed to delete role");
toast.success(`Role "${deletingRole.role_name}" deleted successfully.`);
setShowDeleteDialog(false);
setDeletingRole(null);
setRefreshTrigger((prev) => prev + 1);
} catch (error) {
console.error("Error deleting role:", error);
toast.error("Failed to delete role.");
}
};
const handleSave = async () => {
if (!userRoleFormData.role_name.trim()) {
toast.error("Role name is required");
return;
}
try {
if (editingRole) {
const response = await fetch(`${BASE_URL}/roles/${editingRole.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userRoleFormData),
});
if (!response.ok) throw new Error("Failed to update role");
toast.success("User role updated successfully");
} else {
const response = await fetch(`${BASE_URL}/roles`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(userRoleFormData),
});
if (!response.ok) throw new Error("Failed to create role");
toast.success("User role created successfully");
}
handleCancel();
setRefreshTrigger((prev) => prev + 1);
} catch (error) {
console.error("Error saving user role:", error);
toast.error("Failed to save user role. Please try again.");
}
};
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-[#000000] text-white hover:bg-gray-800"
>
{editingRole ? "Update Role" : "Create Role"}
</Button>
</div>
</div>
</div>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-end">
<Button
onClick={handleCreate}
className="bg-[#000000] text-white hover:bg-gray-800 h-[34px] px-4 rounded-[5px]"
>
Add Role +
</Button>
</div>
<UserRolesGrid
onEdit={handleEdit}
onDelete={handleDelete}
refreshTrigger={refreshTrigger}
/>
<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-red-600 hover:bg-red-700 text-white"
>
Delete Role
</Button>
</div>
</DialogContent>
</Dialog>
</div>
);
}
Comments
Post a Comment