diff --git a/src/components/shared/EditRoleModal.tsx b/src/components/shared/EditRoleModal.tsx index e185a1b..b27a37d 100644 --- a/src/components/shared/EditRoleModal.tsx +++ b/src/components/shared/EditRoleModal.tsx @@ -10,6 +10,7 @@ import { PrimaryButton, SecondaryButton, FormTextArea, + FormSelect, } from "@/components/shared"; import type { Role, UpdateRoleRequest } from "@/types/role"; import { useAppSelector } from "@/hooks/redux-hooks"; @@ -74,6 +75,7 @@ const editRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)", ), description: z.string().min(1, "Description is required"), + is_active: z.boolean(), permissions: z .array( z.object({ @@ -124,7 +126,7 @@ export const EditRoleModal = ({ register, handleSubmit, setValue, - // watch, + watch, reset, setError, clearErrors, @@ -132,10 +134,13 @@ export const EditRoleModal = ({ } = useForm({ resolver: zodResolver(editRoleSchema), defaultValues: { + is_active: true, permissions: [], }, }); + const statusValue = watch("is_active"); + // const nameValue = watch("name"); // Auto-generate code from name - Only during creation (handled in parent or different component) @@ -310,6 +315,7 @@ export const EditRoleModal = ({ name: role.name, code: role.code, description: role.description || "", + is_active: role.is_active ?? true, permissions: rolePermissions, }); } catch (err: any) { @@ -331,6 +337,7 @@ export const EditRoleModal = ({ name: "", code: "", description: "", + is_active: true, permissions: [], }); setLoadError(null); @@ -505,6 +512,20 @@ export const EditRoleModal = ({ rows={4} /> +
+ setValue("is_active", value === "true", { shouldValidate: true })} + error={errors.is_active?.message} + /> +
+ {/* Permissions Section */}
{ const response = defaultTenantId - ? await roleService.getByTenant(defaultTenantId, page, limit) - : await roleService.getAll(page, limit); + ? await roleService.getByTenant(defaultTenantId, page, limit, undefined, undefined, true) + : await roleService.getAll(page, limit, undefined, undefined, undefined, true); return { options: response.data.map((role) => ({ value: role.id, diff --git a/src/components/shared/NewRoleModal.tsx b/src/components/shared/NewRoleModal.tsx index e101d51..9afe913 100644 --- a/src/components/shared/NewRoleModal.tsx +++ b/src/components/shared/NewRoleModal.tsx @@ -10,6 +10,7 @@ import { PrimaryButton, SecondaryButton, FormTextArea, + FormSelect, } from "@/components/shared"; import type { CreateRoleRequest } from "@/types/role"; import { useAppSelector } from "@/hooks/redux-hooks"; @@ -74,6 +75,7 @@ const newRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)", ), description: z.string().min(1, "Description is required"), + is_active: z.boolean(), permissions: z .array( z.object({ @@ -125,10 +127,13 @@ export const NewRoleModal = ({ resolver: zodResolver(newRoleSchema), defaultValues: { code: undefined, + is_active: true, permissions: [], }, }); + const statusValue = watch("is_active"); + const nameValue = watch("name"); // Auto-generate code from name @@ -146,6 +151,7 @@ export const NewRoleModal = ({ name: "", code: undefined, description: "", + is_active: true, permissions: [], }); setSelectedPermissions([]); @@ -153,6 +159,11 @@ export const NewRoleModal = ({ } }, [isOpen, reset, clearErrors]); + const statusOptions = [ + { value: "true", label: "Active" }, + { value: "false", label: "Inactive" }, + ]; + // Build available resources and actions based on user permissions const availableResourcesAndActions = useMemo(() => { const resourceMap = new Map>(); @@ -395,6 +406,17 @@ export const NewRoleModal = ({ rows={4} /> +
+ setValue("is_active", value === "true", { shouldValidate: true })} + error={errors.is_active?.message} + /> +
+ {/* Permissions Section */}
{ const response = defaultTenantId - ? await roleService.getByTenant(defaultTenantId, page, limit) - : await roleService.getAll(page, limit); + ? await roleService.getByTenant(defaultTenantId, page, limit, undefined, undefined, true) + : await roleService.getAll(page, limit, undefined, undefined, undefined, true); return { options: response.data.map((role) => ({ value: role.id, label: role.name })), pagination: response.pagination, diff --git a/src/components/shared/ViewRoleModal.tsx b/src/components/shared/ViewRoleModal.tsx index f13c504..84087d2 100644 --- a/src/components/shared/ViewRoleModal.tsx +++ b/src/components/shared/ViewRoleModal.tsx @@ -116,6 +116,14 @@ export const ViewRoleModal = ({
+
+ +
+ + {role.is_active !== false ? 'Active' : 'Inactive'} + +
+

{role.id}

diff --git a/src/components/superadmin/PlatformRolesTable.tsx b/src/components/superadmin/PlatformRolesTable.tsx index 7339a3c..373ea3d 100644 --- a/src/components/superadmin/PlatformRolesTable.tsx +++ b/src/components/superadmin/PlatformRolesTable.tsx @@ -22,6 +22,8 @@ import { Modal, FormField, FormTextArea, + FormSelect, + StatusBadge, // DeleteConfirmationModal, type Column, } from "@/components/shared"; @@ -99,6 +101,7 @@ const newPlatformRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)", ), description: z.string().min(1, "Description is required"), + is_active: z.boolean(), permissions: z .array( z.object({ @@ -123,6 +126,7 @@ const editPlatformRoleSchema = z.object({ "Code must be lowercase and use '_' for separation (e.g. abc_def)", ), description: z.string().min(1, "Description is required"), + is_active: z.boolean(), permissions: z .array( z.object({ @@ -168,11 +172,13 @@ const NewPlatformRoleModal = ({ resolver: zodResolver(newPlatformRoleSchema), defaultValues: { code: undefined, + is_active: true, permissions: [], }, }); const nameValue = watch("name"); + const statusValue = watch("is_active"); // Auto-generate code from name useEffect(() => { @@ -189,6 +195,7 @@ const NewPlatformRoleModal = ({ name: "", code: undefined, description: "", + is_active: true, permissions: [], }); setSelectedPermissions([]); @@ -381,6 +388,20 @@ const NewPlatformRoleModal = ({ rows={4} /> +
+ setValue("is_active", value === "true", { shouldValidate: true })} + error={errors.is_active?.message} + /> +
+
@@ -510,6 +531,7 @@ const EditPlatformRoleModal = ({ register, handleSubmit, setValue, + watch, reset, setError, clearErrors, @@ -517,10 +539,13 @@ const EditPlatformRoleModal = ({ } = useForm({ resolver: zodResolver(editPlatformRoleSchema), defaultValues: { + is_active: true, permissions: [], }, }); + const statusValue = watch("is_active"); + // Build available resources and actions based on user permissions const availableResourcesAndActions = useMemo(() => { const resourceMap = new Map>(); @@ -608,6 +633,7 @@ const EditPlatformRoleModal = ({ name: role.name, code: role.code, description: role.description || "", + is_active: role.is_active ?? true, permissions: rolePermissions, }); } catch (err: any) { @@ -628,6 +654,7 @@ const EditPlatformRoleModal = ({ name: "", code: "", description: "", + is_active: true, permissions: [], }); setLoadError(null); @@ -769,6 +796,20 @@ const EditPlatformRoleModal = ({ rows={4} /> +
+ setValue("is_active", value === "true", { shouldValidate: true })} + error={errors.is_active?.message} + /> +
+
@@ -909,6 +950,7 @@ export const PlatformRolesTable = forwardRef( // Filter and Search state const [orderBy, setOrderBy] = useState(null); + const [statusFilter, setStatusFilter] = useState(null); const [search, setSearch] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(""); @@ -924,7 +966,7 @@ export const PlatformRolesTable = forwardRef( useImperativeHandle(ref, () => ({ openNewModal: () => setIsCreateOpen(true), refresh: () => - fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch), + fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter), })); const fetchPlatformRoles = async ( @@ -932,10 +974,12 @@ export const PlatformRolesTable = forwardRef( itemsPerPage: number, sortBy: string[] | null = null, searchQuery: string | null = null, + statusValStr: string | null = null, ) => { try { setIsLoading(true); setError(null); + const statusVal = statusValStr === "true" ? true : statusValStr === "false" ? false : null; // Load roles filtered by scope=platform const response = await roleService.getAll( page, @@ -943,6 +987,7 @@ export const PlatformRolesTable = forwardRef( sortBy, searchQuery, "platform", + statusVal, ); if (response.success) { setRoles(response.data); @@ -970,8 +1015,8 @@ export const PlatformRolesTable = forwardRef( }, [search]); useEffect(() => { - fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); - }, [currentPage, limit, orderBy, debouncedSearch]); + fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); + }, [currentPage, limit, orderBy, debouncedSearch, statusFilter]); const handleCreateRole = async (data: CreateRoleRequest) => { try { @@ -986,7 +1031,7 @@ export const PlatformRolesTable = forwardRef( response.message || "Platform role created successfully", ); setIsCreateOpen(false); - fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); + fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); } catch (err: any) { throw err; } finally { @@ -1008,7 +1053,7 @@ export const PlatformRolesTable = forwardRef( ); setEditModalOpen(false); setSelectedRoleId(null); - fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch); + fetchPlatformRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); } catch (err: any) { throw err; } finally { @@ -1064,6 +1109,15 @@ export const PlatformRolesTable = forwardRef( ), }, + { + key: "is_active", + label: "Status", + render: (role) => ( + + {role.is_active !== false ? "Active" : "Inactive"} + + ), + }, { key: "user_count", label: "Assigned Users", @@ -1120,6 +1174,20 @@ export const PlatformRolesTable = forwardRef( placeholder="Search platform roles..." /> + { + setStatusFilter(Array.isArray(value) ? null : value || null); + setCurrentPage(1); + }} + placeholder="All Statuses" + /> + ( null, null, "platform", + true, ); if (response.success) { setRoles(response.data); diff --git a/src/components/superadmin/RolesTable.tsx b/src/components/superadmin/RolesTable.tsx index 1361269..d0908f3 100644 --- a/src/components/superadmin/RolesTable.tsx +++ b/src/components/superadmin/RolesTable.tsx @@ -63,12 +63,6 @@ export const RolesTable = forwardRef( const [isModalOpen, setIsModalOpen] = useState(false); const [isCreating, setIsCreating] = useState(false); - // Expose imperative methods - useImperativeHandle(ref, () => ({ - openNewModal: () => setIsModalOpen(true), - refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch), - })); - // Pagination state const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(5); @@ -89,6 +83,13 @@ export const RolesTable = forwardRef( // Filter state // const [scopeFilter, setScopeFilter] = useState(null); const [orderBy, setOrderBy] = useState(null); + const [statusFilter, setStatusFilter] = useState(null); + + // Expose imperative methods + useImperativeHandle(ref, () => ({ + openNewModal: () => setIsModalOpen(true), + refresh: () => fetchRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter), + })); // Search state const [search, setSearch] = useState(""); @@ -106,13 +107,14 @@ export const RolesTable = forwardRef( const fetchRoles = async ( page: number, itemsPerPage: number, - // scope: string | null = null, sortBy: string[] | null = null, searchQuery: string | null = null, + statusValStr: string | null = null, ): Promise => { try { setIsLoading(true); setError(null); + const statusVal = statusValStr === "true" ? true : statusValStr === "false" ? false : null; const response = tenantId ? await roleService.getByTenant( tenantId, @@ -120,8 +122,16 @@ export const RolesTable = forwardRef( itemsPerPage, sortBy, searchQuery, + statusVal, ) - : await roleService.getAll(page, itemsPerPage, sortBy, searchQuery); + : await roleService.getAll( + page, + itemsPerPage, + sortBy, + searchQuery, + null, + statusVal, + ); if (response.success) { setRoles(response.data); setPagination(response.pagination); @@ -145,8 +155,8 @@ export const RolesTable = forwardRef( }, [search]); useEffect(() => { - fetchRoles(currentPage, limit, orderBy, debouncedSearch); - }, [currentPage, limit, orderBy, debouncedSearch, tenantId]); + fetchRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); + }, [currentPage, limit, orderBy, debouncedSearch, statusFilter, tenantId]); const handleCreateRole = async (data: CreateRoleRequest): Promise => { try { @@ -158,7 +168,7 @@ export const RolesTable = forwardRef( : `${data.name} has been added`; showToast.success(message, description); setIsModalOpen(false); - await fetchRoles(currentPage, limit, orderBy); + await fetchRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); } catch (err: any) { throw err; } finally { @@ -197,7 +207,7 @@ export const RolesTable = forwardRef( setEditModalOpen(false); setSelectedRoleId(null); // setSelectedRoleName(""); - await fetchRoles(currentPage, limit, orderBy); + await fetchRoles(currentPage, limit, orderBy, debouncedSearch, statusFilter); } catch (err: any) { throw err; } finally { @@ -282,6 +292,15 @@ export const RolesTable = forwardRef( ), }, + { + key: "is_active", + label: "Status", + render: (role) => ( + + {role.is_active !== false ? "Active" : "Inactive"} + + ), + }, { key: "user_count", label: "Users", @@ -357,6 +376,14 @@ export const RolesTable = forwardRef(
+
+ Status: +
+ + {role.is_active !== false ? "Active" : "Inactive"} + +
+
Users:

@@ -394,6 +421,19 @@ export const RolesTable = forwardRef( onChange={setSearch} placeholder="Search..." /> + { + setStatusFilter(Array.isArray(value) ? null : value || null); + setCurrentPage(1); + }} + placeholder="All Statuses" + /> {isTenantAdmin && ( ( placeholder="Search by name or code..." /> + {/* Status Filter */} + { + setStatusFilter(Array.isArray(value) ? null : value || null); + setCurrentPage(1); + }} + placeholder="All Statuses" + /> + {/* Sort Filter */} => { const params = new URLSearchParams(); params.append('page', String(page)); @@ -26,6 +27,9 @@ export const roleService = { if (scope) { params.append('scope', scope); } + if (isActive !== undefined && isActive !== null) { + params.append('is_active', String(isActive)); + } if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { params.append('orderBy[]', orderBy[0]); params.append('orderBy[]', orderBy[1]); @@ -38,7 +42,8 @@ export const roleService = { page: number = 1, limit: number = 20, orderBy?: string[] | null, - search?: string | null + search?: string | null, + isActive?: boolean | null ): Promise => { const params = new URLSearchParams(); params.append('page', String(page)); @@ -47,6 +52,9 @@ export const roleService = { if (search) { params.append('search', search); } + if (isActive !== undefined && isActive !== null) { + params.append('is_active', String(isActive)); + } if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) { params.append('orderBy[]', orderBy[0]); params.append('orderBy[]', orderBy[1]); diff --git a/src/types/role.ts b/src/types/role.ts index a8950de..dde2ccc 100644 --- a/src/types/role.ts +++ b/src/types/role.ts @@ -5,6 +5,7 @@ export interface Role { description?: string; scope: 'platform' | 'tenant' | 'module'; is_system?: boolean; + is_active?: boolean; tenant_id?: string | null; module_ids?: string[] | null; modules?: string[] | null; @@ -37,6 +38,7 @@ export interface CreateRoleRequest { name: string; code: string; description: string; + is_active?: boolean; tenant_id?: string | null; module_ids?: string[] | null; modules?: string[] | null; @@ -58,6 +60,7 @@ export interface UpdateRoleRequest { name?: string; code?: string; description?: string; + is_active?: boolean; tenant_id?: string | null; module_ids?: string[] | null; modules?: string[] | null;