diff --git a/frontend/src/components/roles/PermissionsBody.tsx b/frontend/src/components/roles/PermissionsBody.tsx
new file mode 100644
index 00000000..abf70ebb
--- /dev/null
+++ b/frontend/src/components/roles/PermissionsBody.tsx
@@ -0,0 +1,251 @@
+/*
+ * Copyright © 2024 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import AddIcon from "@mui/icons-material/Add";
+import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
+import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Divider,
+ Grid,
+ MenuItem,
+ Paper,
+ Typography,
+} from "@mui/material";
+import { FC, Fragment, useEffect, useState } from "react";
+
+import { IconButton } from "@/app-components/buttons/IconButton";
+import { Input } from "@/app-components/inputs/Input";
+import { useCreate } from "@/hooks/crud/useCreate";
+import { useDelete } from "@/hooks/crud/useDelete";
+import { useFind } from "@/hooks/crud/useFind";
+import { useGetFromCache } from "@/hooks/crud/useGet";
+import { useToast } from "@/hooks/useToast";
+import { useTranslate } from "@/hooks/useTranslate";
+import { EntityType, Format } from "@/services/types";
+import { ComponentFormProps } from "@/types/common/dialogs.types";
+import { IPermission, IPermissionAttributes } from "@/types/permission.types";
+import { IRole } from "@/types/role.types";
+
+const DEFAULT_PAYLOAD: IPermissionAttributes = {
+ action: "",
+ model: "",
+ relation: "",
+ role: "",
+};
+const AccordionModelHead = () => (
+
+
+
+
+ Action
+
+
+
+
+ Relation
+
+
+
+);
+
+export const PermissionsBody: FC> = ({
+ data: role,
+ Wrapper = Fragment,
+ WrapperProps,
+ ...rest
+}) => {
+ const { t } = useTranslate();
+ const { toast } = useToast();
+ const { data: models, refetch: modelRefetch } = useFind(
+ { entity: EntityType.MODEL, format: Format.FULL },
+ {
+ hasCount: false,
+ },
+ );
+ const getPermissionFromCache = useGetFromCache(EntityType.PERMISSION);
+ const options = {
+ onError: (error: Error) => {
+ toast.error(error.message || t("message.internal_server_error"));
+ },
+ onSuccess: () => {
+ modelRefetch();
+ toast.success(t("message.item_delete_success"));
+ },
+ };
+ const { mutate: createPermission } = useCreate(EntityType.PERMISSION, {
+ ...options,
+ onError: (error: Error & { statusCode?: number }) => {
+ rest.onError?.();
+ if (error.statusCode === 409) {
+ toast.error(t("message.permission_already_exists"));
+ } else {
+ toast.error(t("message.internal_server_error"));
+ }
+ },
+ });
+ const { mutate: deletePermission } = useDelete(
+ EntityType.PERMISSION,
+ options,
+ );
+ const [expanded, setExpanded] = useState(false);
+ const [payload, setPayload] =
+ useState(DEFAULT_PAYLOAD);
+ const reset = () => setPayload(DEFAULT_PAYLOAD);
+ const handleChange =
+ (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
+ setExpanded(isExpanded ? panel : false);
+ };
+
+ useEffect(() => {
+ if (expanded === false && models?.[0]?.id) setExpanded(models[0].id);
+ }, [models]);
+
+ return (
+ {}} {...WrapperProps}>
+
+ {role?.name}
+
+ {models?.map((model) => (
+
+ }
+ sx={{
+ backgroundColor: "background.default",
+ borderRadius: 1,
+ fontFamily: "inherit",
+ }}
+ >
+ {model.name}
+
+
+
+
+ {model.permissions
+ ?.map((p) => getPermissionFromCache(p))
+ ?.filter(
+ (permission) => permission && permission.role === role?.id,
+ )
+ .map((p) => p as IPermission)
+ .map(({ id, action, relation }, index) => {
+ return (
+ <>
+ {index > 0 && }
+
+
+ deletePermission(id)}
+ size="small"
+ >
+
+
+
+
+ {action}
+
+
+ {relation}
+
+
+ >
+ );
+ })}
+
+
+ {
+ if (role?.id)
+ createPermission({
+ ...payload,
+ role: role.id,
+ model: model.id,
+ });
+ reset();
+ }}
+ >
+
+
+
+
+ {
+ if (e.target.value)
+ setPayload((currentPayload) => ({
+ ...currentPayload,
+ action: e.target.value,
+ }));
+ }}
+ >
+
+
+
+
+
+
+
+ {
+ if (e.target.value)
+ setPayload((currentPayload) => ({
+ ...currentPayload,
+ relation: e.target.value,
+ }));
+ }}
+ >
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/components/roles/PermissionsBodyDialog.tsx b/frontend/src/components/roles/PermissionsBodyDialog.tsx
new file mode 100644
index 00000000..3ee11651
--- /dev/null
+++ b/frontend/src/components/roles/PermissionsBodyDialog.tsx
@@ -0,0 +1,23 @@
+/*
+ * Copyright © 2025 Hexastack. All rights reserved.
+ *
+ * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
+ * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
+ * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
+ */
+
+import { GenericFormDialog } from "@/app-components/dialogs";
+import { ComponentFormDialogProps } from "@/types/common/dialogs.types";
+import { IRole } from "@/types/role.types";
+
+import { PermissionsBody } from "./PermissionsBody";
+
+export const PermissionBodyDialog = (
+ props: ComponentFormDialogProps,
+) => (
+
+ Form={PermissionsBody}
+ editText="title.manage_permissions"
+ {...props}
+ />
+);
diff --git a/frontend/src/components/roles/PermissionsDialog.tsx b/frontend/src/components/roles/PermissionsDialog.tsx
deleted file mode 100644
index dca38329..00000000
--- a/frontend/src/components/roles/PermissionsDialog.tsx
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Copyright © 2024 Hexastack. All rights reserved.
- *
- * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
- * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
- * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
- */
-
-import AddIcon from "@mui/icons-material/Add";
-import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined";
-import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
-import {
- Accordion,
- AccordionDetails,
- AccordionSummary,
- Dialog,
- Grid,
- MenuItem,
- Paper,
- Typography,
- DialogContent,
- DialogActions,
- Button,
- Divider,
-} from "@mui/material";
-import { useState, FC, useEffect } from "react";
-
-import { IconButton } from "@/app-components/buttons/IconButton";
-import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
-import { Input } from "@/app-components/inputs/Input";
-import { useCreate } from "@/hooks/crud/useCreate";
-import { useDelete } from "@/hooks/crud/useDelete";
-import { useFind } from "@/hooks/crud/useFind";
-import { useGetFromCache } from "@/hooks/crud/useGet";
-import { DialogControlProps } from "@/hooks/useDialog";
-import { useToast } from "@/hooks/useToast";
-import { useTranslate } from "@/hooks/useTranslate";
-import { EntityType, Format } from "@/services/types";
-import { IPermission, IPermissionAttributes } from "@/types/permission.types";
-import { IRole } from "@/types/role.types";
-
-export type PermissionsDialogProps = DialogControlProps<{
- role: IRole;
-}>;
-
-const DEFAULT_PAYLOAD: IPermissionAttributes = {
- action: "",
- model: "",
- relation: "",
- role: "",
-};
-const AccordionModelHead = () => (
-
-
-
-
- Action
-
-
-
-
- Relation
-
-
-
-);
-
-export const PermissionsDialog: FC = ({
- open,
- data,
- closeDialog: closeFunction,
-}) => {
- const { t } = useTranslate();
- const { toast } = useToast();
- const { data: models, refetch: modelRefetch } = useFind(
- { entity: EntityType.MODEL, format: Format.FULL },
- {
- hasCount: false,
- },
- );
- const getPermisionFromCache = useGetFromCache(EntityType.PERMISSION);
- const { mutateAsync: createPermission } = useCreate(EntityType.PERMISSION, {
- onError: (error: Error & { statusCode?: number }) => {
- if (error.statusCode === 409) {
- toast.error(t("message.permission_already_exists"));
- } else {
- toast.error(t("message.internal_server_error"));
- }
- },
- onSuccess: () => {
- modelRefetch();
- toast.success(t("message.success_save"));
- },
- });
- const { mutateAsync: deletePermission } = useDelete(EntityType.PERMISSION, {
- onError: () => {
- toast.error(t("message.internal_server_error"));
- },
- onSuccess: () => {
- modelRefetch();
- toast.success(t("message.item_delete_success"));
- },
- });
- const [expanded, setExpanded] = useState(false);
- const [payload, setPayload] =
- useState(DEFAULT_PAYLOAD);
- const reset = () => setPayload(DEFAULT_PAYLOAD);
- const handleChange =
- (panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
- setExpanded(isExpanded ? panel : false);
- };
-
- useEffect(() => {
- if (expanded === false && models?.[0]?.id) setExpanded(models[0].id);
- }, [models]);
-
- return (
-
- );
-};