From 178b8f9046322a769c3b9e8fe7cdb63a313d39bc Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 30 Apr 2026 18:05:03 +0530 Subject: [PATCH] feat: add imperative handle to UsersTable and implement theme-aware dynamic styling and permission-based access controls across core tables. --- src/components/layout/Sidebar.tsx | 55 +- .../shared/WorkflowDefinitionsTable.tsx | 150 ++-- .../superadmin/DepartmentsTable.tsx | 27 +- .../superadmin/DesignationsTable.tsx | 55 +- src/components/superadmin/RolesTable.tsx | 299 ++++---- src/components/superadmin/UsersTable.tsx | 83 ++- src/components/superadmin/index.ts | 6 +- src/pages/superadmin/CreateTenantWizard.tsx | 4 +- src/pages/superadmin/EditTenant.tsx | 4 +- src/pages/superadmin/TenantDetails.tsx | 10 +- src/pages/superadmin/Tenants.tsx | 89 +-- src/pages/tenant/Departments.tsx | 19 +- src/pages/tenant/FilesList.tsx | 383 ++++++----- src/pages/tenant/Roles.tsx | 475 +------------ src/pages/tenant/Users.tsx | 645 +----------------- src/types/tenant.ts | 2 + 16 files changed, 642 insertions(+), 1664 deletions(-) diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 770dfb3..d525177 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -270,25 +270,52 @@ const GroupMenuItem = ({ onClose: () => void; }) => { const isChildActive = (path: string) => { - // Special handling for Document Lists to NOT show as active when sub-actions are active - if (path === "/tenant/documents") { - const subActions = ["/create", "/categories", "/due-for-review", "/edit"]; - const isSubActionActive = subActions.some((sub) => - location.pathname.startsWith(path + sub), + // Basic exact match check + if (location.pathname === path) return true; + + // Special handling for paths that are prefixes of other siblings + // Example: /tenant/settings is a prefix of /tenant/settings/notifications + if (location.pathname.startsWith(`${path}/`)) { + // If there's another sibling that is a more specific match for the current path, + // then this prefix path should not be considered "active" + const hasMoreSpecificSibling = childrenItems.some( + (sibling) => + sibling.path !== path && + sibling.path.startsWith(path) && + (location.pathname === sibling.path || + location.pathname.startsWith(`${sibling.path}/`)), ); - if (isSubActionActive) return false; - } - // Special handling for Files List to NOT show as active when Storage Dashboard is active - if (path === "/tenant/files") { - if (location.pathname.startsWith("/tenant/files/storage-dashboard")) { - return false; + if (hasMoreSpecificSibling) return false; + + // Also keep existing special cases if they don't fit the generic rule above + // (Though the generic rule above should cover most of these) + + // Special handling for Document Lists to NOT show as active when sub-actions are active + if (path === "/tenant/documents") { + const subActions = [ + "/create", + "/categories", + "/due-for-review", + "/edit", + ]; + const isSubActionActive = subActions.some((sub) => + location.pathname.startsWith(path + sub), + ); + if (isSubActionActive) return false; } + + // Special handling for Files List to NOT show as active when Storage Dashboard is active + if (path === "/tenant/files") { + if (location.pathname.startsWith("/tenant/files/storage-dashboard")) { + return false; + } + } + + return true; } - return ( - location.pathname === path || location.pathname.startsWith(`${path}/`) - ); + return false; }; const isAnyChildActive = childrenItems.some((child) => isChildActive(child.path), diff --git a/src/components/shared/WorkflowDefinitionsTable.tsx b/src/components/shared/WorkflowDefinitionsTable.tsx index e8947bf..af3c03e 100644 --- a/src/components/shared/WorkflowDefinitionsTable.tsx +++ b/src/components/shared/WorkflowDefinitionsTable.tsx @@ -9,9 +9,11 @@ import { DeleteConfirmationModal, WorkflowDefinitionModal, WorkflowDefinitionViewModal, + SearchBox, type Column, + ActionDropdown, } from "@/components/shared"; -import { Plus, GitBranch, Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react"; +import { Plus, Play, Power, Trash2, Copy, Edit, Eye } from "lucide-react"; import { workflowService } from "@/services/workflow-service"; import type { WorkflowDefinition } from "@/types/workflow"; import { showToast } from "@/utils/toast"; @@ -206,9 +208,7 @@ const WorkflowDefinitionsTable = ({ { key: "entity_type", label: "Entity Type", - render: (wf) => ( - - ), + render: (wf) => , }, { key: "version", @@ -233,7 +233,9 @@ const WorkflowDefinitionsTable = ({ key: "source_module", label: "Module", render: (wf) => ( - {wf.source_module?.join(", ")} + + {wf.source_module?.join(", ")} + ), }, { @@ -250,83 +252,51 @@ const WorkflowDefinitionsTable = ({ label: "Actions", align: "right", render: (wf) => ( -
- - - - - - - {wf.status === "draft" && ( - - )} - - {wf.status === "active" && ( - - )} - {wf.status === "deprecated" && ( - - )} - - +
+ , + label: "Clone", + onClick: () => handleClone(wf.id, wf.name), + }, + { + icon: , + label: "View", + onClick: () => { + setViewDefinitionId(wf.id); + setIsViewModalOpen(true); + }, + }, + { + icon: , + label: "Edit", + onClick: () => { + setSelectedDefinition(wf); + setIsModalOpen(true); + }, + }, + (wf.status === "draft" || wf.status === "deprecated") ? { + icon: , + label: "Activate", + onClick: () => handleActivate(wf.id), + } : null, + wf.status === "active" ? { + icon: , + label: "Deprecate", + onClick: () => handleDeprecate(wf.id), + } : null, + { + icon: , + label: "Delete", + variant: "danger", + onClick: () => { + setSelectedDefinition(wf); + setIsDeleteModalOpen(true); + }, + }, + ].filter((a): a is any => a !== null)} + />
), }, @@ -339,20 +309,14 @@ const WorkflowDefinitionsTable = ({ {showHeader && (
-
- - setSearchQuery(e.target.value)} - /> -
+ { const { primaryColor } = useAppTheme(); + const { canCreate, canUpdate } = usePermissions(); const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); const effectiveTenantId = propsTenantId || reduxTenantId; @@ -265,10 +269,14 @@ export const DepartmentsTable = forwardRef { - setSelectedDepartment(dept); - setIsEditModalOpen(true); - }} + onEdit={ + canUpdate("departments") + ? () => { + setSelectedDepartment(dept); + setIsEditModalOpen(true); + } + : undefined + } />
), @@ -330,6 +338,17 @@ export const DepartmentsTable = forwardRef )}
+ + {canCreate("departments") && ( + setIsNewModalOpen(true)} + > + + New Department + + )}
)} diff --git a/src/components/superadmin/DesignationsTable.tsx b/src/components/superadmin/DesignationsTable.tsx index 334fc96..3252397 100644 --- a/src/components/superadmin/DesignationsTable.tsx +++ b/src/components/superadmin/DesignationsTable.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, type ReactElement } from "react"; +import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react"; import { useSelector } from "react-redux"; import { PrimaryButton, @@ -27,20 +27,27 @@ import type { } from "@/types/designation"; import { showToast } from "@/utils/toast"; import type { RootState } from "@/store/store"; +import { usePermissions } from "@/hooks/usePermissions"; // import { useAppTheme } from "@/hooks/useAppTheme"; import CodeBadge from "../shared/CodeBadge"; +export interface DesignationsTableRef { + openNewModal: () => void; + refresh: () => void; +} + interface DesignationsTableProps { tenantId?: string | null; // If provided, use this tenantId (Super Admin mode) compact?: boolean; // Compact mode for tabs showHeader?: boolean; } -const DesignationsTable = ({ +const DesignationsTable = forwardRef(({ tenantId: propsTenantId, compact = false, showHeader = true, -}: DesignationsTableProps): ReactElement => { +}, ref): ReactElement => { + const { canCreate, canUpdate } = usePermissions(); // const { primaryColor } = useAppTheme(); const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); const effectiveTenantId = propsTenantId || reduxTenantId; @@ -49,6 +56,12 @@ const DesignationsTable = ({ const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); + // Expose imperative methods + useImperativeHandle(ref, () => ({ + openNewModal: () => setIsNewModalOpen(true), + refresh: () => fetchDesignations(), + })); + // Pagination state const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(compact ? 10 : 5); @@ -226,14 +239,14 @@ const DesignationsTable = ({ setSelectedDesignation(desig); setIsViewModalOpen(true); }} - onEdit={() => { - setSelectedDesignation(desig); - setIsEditModalOpen(true); - }} - // onDelete={() => { - // setSelectedDesignation(desig); - // setIsDeleteModalOpen(true); - // }} + onEdit={ + canUpdate("designations") + ? () => { + setSelectedDesignation(desig); + setIsEditModalOpen(true); + } + : undefined + } /> ), @@ -257,14 +270,16 @@ const DesignationsTable = ({ onChange={(val) => setActiveOnly(val)} /> - setIsNewModalOpen(true)} - > - - New Designation - + {canCreate("designations") && ( + setIsNewModalOpen(true)} + > + + New Designation + + )} )} @@ -332,6 +347,6 @@ const DesignationsTable = ({ /> */} ); -}; +}); export default DesignationsTable; diff --git a/src/components/superadmin/RolesTable.tsx b/src/components/superadmin/RolesTable.tsx index 8ad9b29..13247c7 100644 --- a/src/components/superadmin/RolesTable.tsx +++ b/src/components/superadmin/RolesTable.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, type ReactElement } from 'react'; +import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react"; import { PrimaryButton, StatusBadge, @@ -10,42 +10,62 @@ import { DataTable, Pagination, FilterDropdown, + SearchBox, type Column, -} from '@/components/shared'; -import { Plus, Download, ArrowUpDown } from 'lucide-react'; -import { roleService } from '@/services/role-service'; -import type { Role, CreateRoleRequest, UpdateRoleRequest } from '@/types/role'; -import { showToast } from '@/utils/toast'; -import { formatDate } from '@/utils/format-date'; +} from "@/components/shared"; +import { Plus, ArrowUpDown } from "lucide-react"; +import { roleService } from "@/services/role-service"; +import type { Role, CreateRoleRequest, UpdateRoleRequest } from "@/types/role"; +import { showToast } from "@/utils/toast"; +import { formatDate } from "@/utils/format-date"; +// import { useAppTheme } from "@/hooks/useAppTheme"; +import { usePermissions } from "@/hooks/usePermissions"; +import CodeBadge from "@/components/shared/CodeBadge"; // Helper function to get scope badge variant -const getScopeVariant = (scope: string): 'success' | 'failure' | 'process' => { +const getScopeVariant = (scope: string): "success" | "failure" | "process" => { switch (scope.toLowerCase()) { - case 'platform': - return 'success'; - case 'tenant': - return 'process'; - case 'module': - return 'failure'; + case "platform": + return "success"; + case "tenant": + return "process"; + case "module": + return "failure"; default: - return 'success'; + return "success"; } }; +export interface RolesTableRef { + openNewModal: () => void; + refresh: () => void; +} + interface RolesTableProps { tenantId?: string | null; // If provided, fetch roles for this tenant only showHeader?: boolean; // Show header with title and actions (default: true) compact?: boolean; // Compact mode for tabs (default: false) } -export const RolesTable = ({ tenantId, showHeader = true, compact = false }: RolesTableProps): ReactElement => { - +export const RolesTable = forwardRef(({ + tenantId, + showHeader = true, + compact = false, +}, ref): ReactElement => { + // const { primaryColor } = useAppTheme(); + const { canCreate, canUpdate, canDelete } = usePermissions(); const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); 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); @@ -67,12 +87,16 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol // const [scopeFilter, setScopeFilter] = useState(null); const [orderBy, setOrderBy] = useState(null); + // Search state + const [search, setSearch] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + // View, Edit, Delete modals const [viewModalOpen, setViewModalOpen] = useState(false); const [editModalOpen, setEditModalOpen] = useState(false); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [selectedRoleId, setSelectedRoleId] = useState(null); - const [selectedRoleName, setSelectedRoleName] = useState(''); + const [selectedRoleName, setSelectedRoleName] = useState(""); const [isUpdating, setIsUpdating] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -80,30 +104,40 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol page: number, itemsPerPage: number, // scope: string | null = null, - sortBy: string[] | null = null + sortBy: string[] | null = null, + searchQuery: string | null = null, ): Promise => { try { setIsLoading(true); setError(null); const response = tenantId - ? await roleService.getByTenant(tenantId, page, itemsPerPage, sortBy) - : await roleService.getAll(page, itemsPerPage, sortBy); + ? await roleService.getByTenant(tenantId, page, itemsPerPage, sortBy, searchQuery) + : await roleService.getAll(page, itemsPerPage, sortBy, searchQuery); if (response.success) { setRoles(response.data); setPagination(response.pagination); } else { - setError('Failed to load roles'); + setError("Failed to load roles"); } } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load roles'); + setError(err?.response?.data?.error?.message || "Failed to load roles"); } finally { setIsLoading(false); } }; + // Handle search debouncing useEffect(() => { - fetchRoles(currentPage, limit, orderBy); - }, [currentPage, limit, orderBy, tenantId]); + const timer = setTimeout(() => { + setDebouncedSearch(search); + if (search) setCurrentPage(1); + }, 500); + return () => clearTimeout(timer); + }, [search]); + + useEffect(() => { + fetchRoles(currentPage, limit, orderBy, debouncedSearch); + }, [currentPage, limit, orderBy, debouncedSearch, tenantId]); const handleCreateRole = async (data: CreateRoleRequest): Promise => { try { @@ -187,61 +221,71 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol // Table columns const columns: Column[] = [ { - key: 'name', - label: 'Name', + key: "name", + label: "Name", render: (role) => ( {role.name} ), }, { - key: 'code', - label: 'Code', + key: "code", + label: "Code", + render: (role) => , + }, + { + key: "scope", + label: "Scope", render: (role) => ( - {role.code} + + {role.scope} + ), }, { - key: 'scope', - label: 'Scope', + key: "user_count", + label: "Users", render: (role) => ( - {role.scope} - ), - }, - { - key: 'description', - label: 'Description', - render: (role) => ( - - {role.description || 'N/A'} + + {role.user_count || 0} ), }, - // { - // key: 'is_system', - // label: 'System Role', - // render: (role) => ( - // - // {role.is_system ? 'Yes' : 'No'} - // - // ), - // }, { - key: 'created_at', - label: 'Created Date', + key: "description", + label: "Description", render: (role) => ( - {formatDate(role.created_at)} + + {role.description || "N/A"} + ), }, { - key: 'actions', - label: 'Actions', - align: 'right', + key: "created_at", + label: "Created Date", + render: (role) => ( + + {formatDate(role.created_at)} + + ), + }, + { + key: "actions", + label: "Actions", + align: "right", render: (role) => (
handleViewRole(role.id)} - onEdit={() => handleEditRole(role.id, role.name)} - onDelete={() => handleDeleteRole(role.id, role.name)} + onEdit={ + canUpdate("roles") + ? () => handleEditRole(role.id, role.name) + : undefined + } + onDelete={ + canDelete("roles") + ? () => handleDeleteRole(role.id, role.name) + : undefined + } />
), @@ -253,25 +297,45 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol
-

{role.name}

+

+ {role.name} +

{role.code}

handleViewRole(role.id)} - onEdit={() => handleEditRole(role.id, role.name)} - onDelete={() => handleDeleteRole(role.id, role.name)} + onEdit={ + canUpdate("roles") + ? () => handleEditRole(role.id, role.name) + : undefined + } + onDelete={ + canDelete("roles") + ? () => handleDeleteRole(role.id, role.name) + : undefined + } />
Scope:
- {role.scope} + + {role.scope} +
+
+ Users: +

+ {role.user_count || 0} +

+
Created: -

{formatDate(role.created_at)}

+

+ {formatDate(role.created_at)} +

{role.description && (
@@ -288,32 +352,24 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol return ( <>
-
+

-
- {/* { - setScopeFilter(Array.isArray(value) ? null : value || null); - setCurrentPage(1); - }} - placeholder="Filter by scope" - /> */} - setIsModalOpen(true)} - > - - New Role - +
+ + {canCreate("roles") && ( + setIsModalOpen(true)} + > + + New Role + + )}
{ setEditModalOpen(false); setSelectedRoleId(null); - setSelectedRoleName(''); + setSelectedRoleName(""); }} roleId={selectedRoleId} onLoadRole={loadRole} @@ -380,7 +436,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol onClose={() => { setDeleteModalOpen(false); setSelectedRoleId(null); - setSelectedRoleName(''); + setSelectedRoleName(""); }} onConfirm={handleConfirmDelete} title="Delete Role" @@ -402,34 +458,25 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol
{/* Filters */}
- {/* Scope Filter */} - {/* { - setScopeFilter(value as string | null); - setCurrentPage(1); - }} - placeholder="All" - /> */} + {/* Global Search */} + {/* Sort Filter */} { @@ -445,23 +492,25 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol {/* Actions */}
{/* Export Button */} - + */} {/* New Role Button */} - setIsModalOpen(true)} - > - - New Role - + {canCreate("roles") && ( + setIsModalOpen(true)} + > + + New Role + + )}
)} @@ -489,7 +538,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol }} onLimitChange={(newLimit: number) => { setLimit(newLimit); - setCurrentPage(1); + setCurrentPage(1); // Reset to first page when limit changes }} /> )} @@ -543,4 +592,4 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol /> ); -}; +}); diff --git a/src/components/superadmin/UsersTable.tsx b/src/components/superadmin/UsersTable.tsx index 65003ee..5a5f64b 100644 --- a/src/components/superadmin/UsersTable.tsx +++ b/src/components/superadmin/UsersTable.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, type ReactElement } from "react"; +import { useState, useEffect, type ReactElement, forwardRef, useImperativeHandle } from "react"; import { PrimaryButton, StatusBadge, @@ -13,13 +13,15 @@ import { SearchBox, type Column, } from "@/components/shared"; -import { Plus, Download, ArrowUpDown } from "lucide-react"; +import { Plus, ArrowUpDown } from "lucide-react"; import { userService } from "@/services/user-service"; import { roleService } from "@/services/role-service"; import type { Role } from "@/types/role"; import type { User } from "@/types/user"; import { showToast } from "@/utils/toast"; import { formatDate } from "@/utils/format-date"; +import { useAppTheme } from "@/hooks/useAppTheme"; +import { usePermissions } from "@/hooks/usePermissions"; // Helper function to get user initials const getUserInitials = (firstName: string, lastName: string): string => { @@ -46,23 +48,36 @@ const getStatusVariant = ( } }; +export interface UsersTableRef { + openNewModal: () => void; + refresh: () => void; +} + interface UsersTableProps { tenantId?: string | null; // If provided, fetch users for this tenant only showHeader?: boolean; // Show header with title and actions (default: true) compact?: boolean; // Compact mode for tabs (default: false) } -export const UsersTable = ({ +export const UsersTable = forwardRef(({ tenantId, showHeader = true, compact = false, -}: UsersTableProps): ReactElement => { +}, ref): ReactElement => { + const { primaryColor } = useAppTheme(); + const { canCreate } = usePermissions(); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isCreating, setIsCreating] = useState(false); + // Expose imperative methods + useImperativeHandle(ref, () => ({ + openNewModal: () => setIsModalOpen(true), + refresh: () => fetchUsers(currentPage, limit, statusFilter, orderBy, debouncedSearch, roleFilter), + })); + // Pagination state const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(compact ? 10 : 5); @@ -322,7 +337,8 @@ export const UsersTable = ({ user.role_module_combinations.map((combo, idx) => ( {combo.role_name} {combo.module_name && `(${combo.module_name})`} @@ -332,7 +348,8 @@ export const UsersTable = ({ user.roles.map((role) => ( {role.name} @@ -502,7 +519,7 @@ export const UsersTable = ({ ({ value: role.id, label: role.name })) ]} value={roleFilter || ""} @@ -525,16 +542,18 @@ export const UsersTable = ({ setRoleFilter(Array.isArray(value) ? null : value || null); setCurrentPage(1); }} - placeholder="Filter by role" + placeholder="All" /> - setIsModalOpen(true)} - > - - New User - + {canCreate("users") && ( + setIsModalOpen(true)} + > + + New User + + )}
({ value: role.id, label: role.name })) ]} value={roleFilter || ""} @@ -663,7 +682,7 @@ export const UsersTable = ({ setRoleFilter(Array.isArray(value) ? null : value || null); setCurrentPage(1); }} - placeholder="Filter by role" + placeholder="All" /> {/* Sort Filter */} @@ -695,23 +714,25 @@ export const UsersTable = ({ {/* Actions */}
{/* Export Button */} - + */} {/* New User Button */} - setIsModalOpen(true)} - > - - New User - + {canCreate("users") && ( + setIsModalOpen(true)} + > + + New User + + )}
)} @@ -792,4 +813,4 @@ export const UsersTable = ({ /> */} ); -}; +}); diff --git a/src/components/superadmin/index.ts b/src/components/superadmin/index.ts index a8962d6..5a15101 100644 --- a/src/components/superadmin/index.ts +++ b/src/components/superadmin/index.ts @@ -6,7 +6,7 @@ export { ViewModuleModal } from './ViewModuleModal'; export { EditModuleModal } from './EditModuleModal'; export { WebhookSyncModal } from './WebhookSyncModal'; export { ApikeyReissueModal } from './ApikeyReissueModal'; -export { UsersTable } from './UsersTable'; -export { RolesTable } from './RolesTable'; +export { UsersTable, type UsersTableRef } from './UsersTable'; +export { RolesTable, type RolesTableRef } from './RolesTable'; export { DepartmentsTable, type DepartmentsTableRef } from './DepartmentsTable'; -export { default as DesignationsTable } from './DesignationsTable'; +export { default as DesignationsTable, type DesignationsTableRef } from './DesignationsTable'; diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index 5652d79..9a01766 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -772,7 +772,7 @@ const CreateTenantWizard = (): ReactElement => {
{/* Max Users and Max Modules Row */} -
+ {/*
{ })} />
-
+
*/} {/* Modules Multiselect */} { />
-
+ {/*
{ })} />
-
+
*/} { )} {activeTab === "users" && id && ( - + )} {activeTab === "roles" && id && ( - + )} {activeTab === "departments" && id && ( )} {activeTab === "designations" && id && ( - + )} {/* {activeTab === "user-categories" && id && ( @@ -437,7 +437,7 @@ const OverviewTab = ({ tenant, stats }: OverviewTabProps): ReactElement => { {tenant.subscription_tier || "N/A"} -
+ {/*
Max Users
@@ -452,7 +452,7 @@ const OverviewTab = ({ tenant, stats }: OverviewTabProps): ReactElement => {
{tenant.max_modules || "Unlimited"}
-
+
*/}
Created At diff --git a/src/pages/superadmin/Tenants.tsx b/src/pages/superadmin/Tenants.tsx index 5c1d14f..ffc54b7 100644 --- a/src/pages/superadmin/Tenants.tsx +++ b/src/pages/superadmin/Tenants.tsx @@ -5,7 +5,7 @@ import { PrimaryButton, StatusBadge, ActionDropdown, - DeleteConfirmationModal, + // DeleteConfirmationModal, DataTable, Pagination, FilterDropdown, @@ -94,10 +94,10 @@ const Tenants = (): ReactElement => { // View, Edit, Delete modals // const [viewModalOpen, setViewModalOpen] = useState(false); // Commented out - using details page instead // const [editModalOpen, setEditModalOpen] = useState(false); // Commented out - using edit page instead - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [selectedTenantId, setSelectedTenantId] = useState(null); - const [selectedTenantName, setSelectedTenantName] = useState(""); - const [isDeleting, setIsDeleting] = useState(false); + // const [deleteModalOpen, setDeleteModalOpen] = useState(false); + // const [selectedTenantId, setSelectedTenantId] = useState(null); + // const [selectedTenantName, setSelectedTenantName] = useState(""); + // const [isDeleting, setIsDeleting] = useState(false); const fetchTenants = async ( page: number, @@ -181,29 +181,29 @@ const Tenants = (): ReactElement => { // Update tenant handler - removed, now handled in EditTenant page // Delete tenant handler - const handleDeleteTenant = (tenantId: string, tenantName: string): void => { - setSelectedTenantId(tenantId); - setSelectedTenantName(tenantName); - setDeleteModalOpen(true); - }; + // const handleDeleteTenant = (tenantId: string, tenantName: string): void => { + // setSelectedTenantId(tenantId); + // setSelectedTenantName(tenantName); + // setDeleteModalOpen(true); + // }; // Confirm delete handler - const handleConfirmDelete = async (): Promise => { - if (!selectedTenantId) return; + // const handleConfirmDelete = async (): Promise => { + // if (!selectedTenantId) return; - try { - setIsDeleting(true); - await tenantService.delete(selectedTenantId); - setDeleteModalOpen(false); - setSelectedTenantId(null); - setSelectedTenantName(""); - await fetchTenants(currentPage, limit, statusFilter, orderBy); - } catch (err: any) { - throw err; // Let the modal handle the error display - } finally { - setIsDeleting(false); - } - }; + // try { + // setIsDeleting(true); + // await tenantService.delete(selectedTenantId); + // setDeleteModalOpen(false); + // setSelectedTenantId(null); + // setSelectedTenantName(""); + // await fetchTenants(currentPage, limit, statusFilter, orderBy); + // } catch (err: any) { + // throw err; // Let the modal handle the error display + // } finally { + // setIsDeleting(false); + // } + // }; // Define table columns const columns: Column[] = [ @@ -252,17 +252,17 @@ const Tenants = (): ReactElement => { ), }, { - key: "max_users", + key: "user_count", label: "Users", render: (tenant) => ( - {tenant.max_users ?? "N/A"} + {tenant.user_count ?? 0} ), }, { key: "subscription_tier", - label: "Plan", + label: "Subscription Tier", render: (tenant) => ( {formatSubscriptionTier(tenant.subscription_tier)} @@ -270,11 +270,11 @@ const Tenants = (): ReactElement => { ), }, { - key: "max_modules", + key: "module_count", label: "Modules", render: (tenant) => ( - {tenant.max_modules ?? "N/A"} + {tenant.module_count ?? 0} ), }, @@ -297,7 +297,7 @@ const Tenants = (): ReactElement => { handleViewTenant(tenant.id)} onEdit={() => handleEditTenant(tenant.id)} - onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} + // onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} />
), @@ -329,7 +329,7 @@ const Tenants = (): ReactElement => { handleViewTenant(tenant.id)} onEdit={() => handleEditTenant(tenant.id)} - onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} + // onDelete={() => handleDeleteTenant(tenant.id, tenant.name)} />
@@ -485,29 +485,8 @@ const Tenants = (): ReactElement => { )}
- {/* New Tenant Modal - Commented out, using wizard instead */} - {/* setIsModalOpen(false)} - onSubmit={handleCreateTenant} - isLoading={isCreating} - /> */} - - {/* View Tenant Modal - Commented out, using details page instead */} - {/* { - setViewModalOpen(false); - setSelectedTenantId(null); - }} - tenantId={selectedTenantId} - onLoadTenant={loadTenant} - /> */} - - {/* Edit Tenant Modal - Removed, using edit page instead */} - {/* Delete Confirmation Modal */} - { setDeleteModalOpen(false); @@ -519,7 +498,7 @@ const Tenants = (): ReactElement => { message="Are you sure you want to delete this tenant" itemName={selectedTenantName} isLoading={isDeleting} - /> + /> */} ); }; diff --git a/src/pages/tenant/Departments.tsx b/src/pages/tenant/Departments.tsx index d58a5dc..6498e85 100644 --- a/src/pages/tenant/Departments.tsx +++ b/src/pages/tenant/Departments.tsx @@ -1,30 +1,17 @@ -import { type ReactElement, useRef } from 'react'; +import { type ReactElement } from 'react'; import { Layout } from '@/components/layout/Layout'; -import { DepartmentsTable, type DepartmentsTableRef } from '@/components/superadmin'; -import { PrimaryButton } from '@/components/shared'; -import { Plus } from 'lucide-react'; +import { DepartmentsTable } from '@/components/superadmin'; const Departments = (): ReactElement => { - const tableRef = useRef(null); - return ( tableRef.current?.openNewModal()} - className="flex items-center gap-2" - > - - New Department - - ) }} > - + ); }; diff --git a/src/pages/tenant/FilesList.tsx b/src/pages/tenant/FilesList.tsx index 54a41b3..0b2a460 100644 --- a/src/pages/tenant/FilesList.tsx +++ b/src/pages/tenant/FilesList.tsx @@ -13,7 +13,6 @@ import { useCallback, useEffect, useMemo, useState, type ReactElement } from "re import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { - Search, Upload, FileText, Image, @@ -22,7 +21,12 @@ import { ChevronDown, } from "lucide-react"; import { Layout } from "@/components/layout/Layout"; -import { Pagination } from "@/components/shared"; +import { + Pagination, + DataTable, + SearchBox, + type Column, +} from "@/components/shared"; import { DeleteConfirmationModal } from "@/components/shared/DeleteConfirmationModal"; import { cn } from "@/lib/utils"; import fileAttachmentService, { @@ -212,6 +216,126 @@ const FilesList = (): ReactElement => { (p) => (p.resource === "files" || p.resource === "*") && (p.action === "delete" || p.action === "*") ); + // Table columns + const columns = useMemo[]>(() => [ + { + key: "original_name", + label: "File Name", + render: (file) => ( + + ), + }, + { + key: "file_size", + label: "Size", + render: (file) => ( + + {file.file_size_formatted || formatBytes(file.file_size)} + + ), + }, + { + key: "category", + label: "Category", + render: (file) => ( + file.category ? ( + + {file.category} + + ) : ( + + ) + ), + }, + { + key: "source_module", + label: "Source Module", + render: (file) => ( + file.source_module ? ( + + {file.source_module} + + ) : ( + + ) + ), + }, + { + key: "uploaded_by_email", + label: "Uploaded By", + render: (file) => ( + file.uploaded_by_email ? ( +
+
+ {getInitials(file.uploaded_by_email)} +
+ + {file.uploaded_by_email.split("@")[0]} + +
+ ) : ( + Unknown + ) + ), + }, + { + key: "created_at", + label: "Upload Date", + render: (file) => ( + {formatDate(file.created_at)} + ), + }, + { + key: "version", + label: "Version", + render: (file) => ( + v{file.version} + ), + }, + { + key: "actions", + label: "Actions", + align: "right", + render: (file) => ( + navigate(`/tenant/files/${file.id}`)} + onDownload={() => handleDownload(file)} + onEdit={canUpdate ? () => navigate(`/tenant/files/${file.id}`) : undefined} + onDelete={canDelete ? () => openDeleteConfirm(file) : undefined} + /> + ), + }, + ], [canUpdate, canDelete, navigate, primaryColor]); + // ── State ── const [files, setFiles] = useState([]); const [total, setTotal] = useState(0); @@ -315,6 +439,64 @@ const FilesList = (): ReactElement => { setCurrentPage(1); }; + // Mobile card renderer + const mobileCardRenderer = (file: FileAttachment) => ( +
+
+
+
+
+ {getFileIcon(file.mime_type, file.original_name, primaryColor)} +
+
+

navigate(`/tenant/files/${file.id}`)} + > + {file.original_name} +

+

+ {file.file_size_formatted || formatBytes(file.file_size)} • v{file.version} +

+
+
+
+ navigate(`/tenant/files/${file.id}`)} + onDownload={() => handleDownload(file)} + onEdit={canUpdate ? () => navigate(`/tenant/files/${file.id}`) : undefined} + onDelete={canDelete ? () => openDeleteConfirm(file) : undefined} + /> +
+
+ {file.category && ( + + {file.category} + + )} + {file.source_module && ( + + {file.source_module} + + )} + + {formatDate(file.created_at)} + +
+
+ ); + // ───────────────────────────────────────────────────────────────────────── // Render // ───────────────────────────────────────────────────────────────────────── @@ -345,30 +527,11 @@ const FilesList = (): ReactElement => {
{/* Search */} -
- - { setSearch(e.target.value); setCurrentPage(1); }} - placeholder="Search by name, ID..." - className="h-9 w-[240px] pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.1)] rounded-lg text-sm text-[#0e1b2a] placeholder:text-[#c4cbd6] focus:outline-none focus:ring-2" - style={{ - // @ts-ignore - '--tw-ring-color': `${primaryColor}33`, - borderColor: 'rgba(0,0,0,0.1)' - }} - onFocus={(e) => { - e.currentTarget.style.borderColor = primaryColor; - e.currentTarget.style.boxShadow = `0 0 0 2px ${primaryColor}33`; - }} - onBlur={(e) => { - e.currentTarget.style.borderColor = 'rgba(0,0,0,0.1)'; - e.currentTarget.style.boxShadow = 'none'; - }} - /> -
+ { setSearch(v); setCurrentPage(1); }} + placeholder="Search by name, ID..." + /> {/* Category filter */} {
{/* Table */} -
- - - - {["File Name", "Size", "Category", "Source Module", "Uploaded By", "Upload Date", "Version", "Actions"].map((h) => ( - - ))} - - - - {isLoading ? ( - Array.from({ length: 5 }).map((_, i) => ( - - {Array.from({ length: 8 }).map((__, j) => ( - - ))} - - )) - ) : error ? ( - - - - ) : files.length === 0 ? ( - - - - ) : ( - files.map((file) => ( - - {/* File Name */} - - - {/* Size */} - - - {/* Category */} - - - {/* Source Module */} - - - {/* Uploaded By */} - - - {/* Upload Date */} - - - {/* Version */} - - - {/* Actions */} - - - )) - )} - -
- {h} -
-
-
- {error} -
-
- -

No files found

- {canCreate && ( - - )} -
-
- - - - {file.file_size_formatted || formatBytes(file.file_size)} - - - {file.category ? ( - - {file.category} - - ) : ( - - )} - - {file.source_module ? ( - - {file.source_module} - - ) : ( - - )} - - {file.uploaded_by_email ? ( -
-
- {getInitials(file.uploaded_by_email)} -
- - {file.uploaded_by_email.split("@")[0]} - -
- ) : ( - Unknown - )} -
- {formatDate(file.created_at)} - - v{file.version} - - navigate(`/tenant/files/${file.id}`)} - onDownload={() => handleDownload(file)} - onEdit={canUpdate ? () => navigate(`/tenant/files/${file.id}`) : undefined} - onDelete={canDelete ? () => openDeleteConfirm(file) : undefined} - /> -
-
+ file.id} + isLoading={isLoading} + error={error} + emptyMessage="No files found" + mobileCardRenderer={mobileCardRenderer} + /> {/* Pagination */} {total > 0 && ( diff --git a/src/pages/tenant/Roles.tsx b/src/pages/tenant/Roles.tsx index bc09d3f..17716b0 100644 --- a/src/pages/tenant/Roles.tsx +++ b/src/pages/tenant/Roles.tsx @@ -1,480 +1,17 @@ -import { useState, useEffect } from 'react'; -import type { ReactElement } from 'react'; -import { Layout } from '@/components/layout/Layout'; -import { - PrimaryButton, - StatusBadge, - ActionDropdown, - ViewRoleModal, - EditRoleModal, - DeleteConfirmationModal, - DataTable, - Pagination, - FilterDropdown, - SearchBox, - type Column, -} from '@/components/shared'; -import { Plus, ArrowUpDown } from 'lucide-react'; -import { roleService } from '@/services/role-service'; -import type { Role, CreateRoleRequest, UpdateRoleRequest } from '@/types/role'; -import { showToast } from '@/utils/toast'; -import { NewRoleModal } from '@/components/shared/NewRoleModal'; -import { usePermissions } from '@/hooks/usePermissions'; -import CodeBadge from '@/components/shared/CodeBadge'; - -// Helper function to format date -const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); -}; - -// Helper function to get scope badge variant -const getScopeVariant = (scope: string): 'success' | 'failure' | 'process' => { - switch (scope.toLowerCase()) { - case 'platform': - return 'success'; - case 'tenant': - return 'process'; - case 'module': - return 'failure'; - default: - return 'success'; - } -}; +import { type ReactElement } from "react"; +import { Layout } from "@/components/layout/Layout"; +import { RolesTable } from "@/components/superadmin"; const Roles = (): ReactElement => { - const { canCreate, canUpdate, canDelete } = usePermissions(); - const [roles, setRoles] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isCreating, setIsCreating] = useState(false); - - // Pagination state - const [currentPage, setCurrentPage] = useState(1); - const [limit, setLimit] = useState(5); - const [pagination, setPagination] = useState<{ - page: number; - limit: number; - total: number; - totalPages: number; - hasMore: boolean; - }>({ - page: 1, - limit: 5, - total: 0, - totalPages: 1, - hasMore: false, - }); - - // Filter state - // const [scopeFilter, setScopeFilter] = useState(null); - const [orderBy, setOrderBy] = useState(null); - - // Search state - const [search, setSearch] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - - // View, Edit, Delete modals - const [viewModalOpen, setViewModalOpen] = useState(false); - const [editModalOpen, setEditModalOpen] = useState(false); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [selectedRoleId, setSelectedRoleId] = useState(null); - const [selectedRoleName, setSelectedRoleName] = useState(''); - const [isUpdating, setIsUpdating] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - - const fetchRoles = async ( - page: number, - itemsPerPage: number, - // scope: string | null = null, - sortBy: string[] | null = null, - searchQuery: string | null = null - ): Promise => { - try { - setIsLoading(true); - setError(null); - const response = await roleService.getAll(page, itemsPerPage, sortBy, searchQuery); - if (response.success) { - setRoles(response.data); - setPagination(response.pagination); - } else { - setError('Failed to load roles'); - } - } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load roles'); - } finally { - setIsLoading(false); - } - }; - - // Handle search debouncing - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearch(search); - if (search) setCurrentPage(1); - }, 500); - return () => clearTimeout(timer); - }, [search]); - - useEffect(() => { - fetchRoles(currentPage, limit, orderBy, debouncedSearch); - }, [currentPage, limit, orderBy, debouncedSearch]); - - const handleCreateRole = async (data: CreateRoleRequest): Promise => { - try { - setIsCreating(true); - const response = await roleService.create(data); - const message = response.message || `Role created successfully`; - const description = response.message ? undefined : `${data.name} has been added`; - showToast.success(message, description); - setIsModalOpen(false); - await fetchRoles(currentPage, limit, orderBy); - } catch (err: any) { - throw err; - } finally { - setIsCreating(false); - } - }; - - // View role handler - const handleViewRole = (roleId: string): void => { - setSelectedRoleId(roleId); - setViewModalOpen(true); - }; - - // Edit role handler - const handleEditRole = (roleId: string, roleName: string): void => { - setSelectedRoleId(roleId); - setSelectedRoleName(roleName); - setEditModalOpen(true); - }; - - // Update role handler - const handleUpdateRole = async ( - id: string, - data: UpdateRoleRequest - ): Promise => { - try { - setIsUpdating(true); - const response = await roleService.update(id, data); - const message = response.message || `Role updated successfully`; - const description = response.message ? undefined : `${data.name} has been updated`; - showToast.success(message, description); - setEditModalOpen(false); - setSelectedRoleId(null); - await fetchRoles(currentPage, limit, orderBy); - } catch (err: any) { - throw err; - } finally { - setIsUpdating(false); - } - }; - - // Delete role handler - const handleDeleteRole = (roleId: string, roleName: string): void => { - setSelectedRoleId(roleId); - setSelectedRoleName(roleName); - setDeleteModalOpen(true); - }; - - // Confirm delete handler - const handleConfirmDelete = async (): Promise => { - if (!selectedRoleId) return; - - try { - setIsDeleting(true); - await roleService.delete(selectedRoleId); - setDeleteModalOpen(false); - setSelectedRoleId(null); - setSelectedRoleName(''); - await fetchRoles(currentPage, limit, orderBy); - } catch (err: any) { - throw err; - } finally { - setIsDeleting(false); - } - }; - - // Load role for view/edit - const loadRole = async (id: string): Promise => { - const response = await roleService.getById(id); - return response.data; - }; - - // Table columns - const columns: Column[] = [ - { - key: 'name', - label: 'Name', - render: (role) => ( - {role.name} - ), - }, - { - key: 'code', - label: 'Code', - render: (role) => ( - - ), - }, - { - key: 'scope', - label: 'Scope', - render: (role) => ( - {role.scope} - ), - }, - { - key: 'user_count', - label: 'Users', - render: (role) => ( - - {role.user_count || 0} - - ), - }, - { - key: 'description', - label: 'Description', - render: (role) => ( - - {role.description || 'N/A'} - - ), - }, - // { - // key: 'is_system', - // label: 'System Role', - // render: (role) => ( - // - // {role.is_system ? 'Yes' : 'No'} - // - // ), - // }, - { - key: 'created_at', - label: 'Created Date', - render: (role) => ( - {formatDate(role.created_at)} - ), - }, - { - key: 'actions', - label: 'Actions', - align: 'right', - render: (role) => ( -
- handleViewRole(role.id)} - onEdit={canUpdate('roles') ? () => handleEditRole(role.id, role.name) : undefined} - onDelete={canDelete('roles') ? () => handleDeleteRole(role.id, role.name) : undefined} - /> -
- ), - }, - ]; - - // Mobile card renderer - const mobileCardRenderer = (role: Role) => ( -
-
-
-

{role.name}

-

{role.code}

-
- handleViewRole(role.id)} - onEdit={canUpdate('roles') ? () => handleEditRole(role.id, role.name) : undefined} - onDelete={canDelete('roles') ? () => handleDeleteRole(role.id, role.name) : undefined} - /> -
-
-
- Scope: -
- {role.scope} -
-
-
- Users: -

{role.user_count || 0}

-
-
- Created: -

{formatDate(role.created_at)}

-
- {role.description && ( -
- Description: -

{role.description}

-
- )} -
-
- ); - return ( - {/* Table Container */} -
- {/* Table Header with Filters */} -
- {/* Filters */} -
- {/* Global Search */} - - {/* Scope Filter */} - {/* { - setScopeFilter(value as string | null); - setCurrentPage(1); - }} - placeholder="All" - /> */} - - {/* Sort Filter */} - { - setOrderBy(value as string[] | null); - setCurrentPage(1); - }} - placeholder="Default" - showIcon - icon={} - /> -
- - {/* Actions */} -
- {/* Export Button */} - {/* */} - - {/* New Role Button */} - {canCreate('roles') && ( - setIsModalOpen(true)} - > - - New Role - - )} -
-
- - {/* Table */} - role.id} - mobileCardRenderer={mobileCardRenderer} - emptyMessage="No roles found" - isLoading={isLoading} - error={error} - /> - - {/* Table Footer with Pagination */} - {pagination.total > 0 && ( - { - setCurrentPage(page); - }} - onLimitChange={(newLimit: number) => { - setLimit(newLimit); - setCurrentPage(1); - }} - /> - )} -
- - {/* New Role Modal */} - setIsModalOpen(false)} - onSubmit={handleCreateRole} - isLoading={isCreating} - /> - - {/* View Role Modal */} - { - setViewModalOpen(false); - setSelectedRoleId(null); - }} - roleId={selectedRoleId} - onLoadRole={loadRole} - /> - - {/* Edit Role Modal */} - { - setEditModalOpen(false); - setSelectedRoleId(null); - setSelectedRoleName(''); - }} - roleId={selectedRoleId} - onLoadRole={loadRole} - onSubmit={handleUpdateRole} - isLoading={isUpdating} - /> - - {/* Delete Confirmation Modal */} - { - setDeleteModalOpen(false); - setSelectedRoleId(null); - setSelectedRoleName(''); - }} - onConfirm={handleConfirmDelete} - title="Delete Role" - message={`Are you sure you want to delete this role`} - itemName={selectedRoleName} - isLoading={isDeleting} - /> +
); }; diff --git a/src/pages/tenant/Users.tsx b/src/pages/tenant/Users.tsx index d46feb2..266bcff 100644 --- a/src/pages/tenant/Users.tsx +++ b/src/pages/tenant/Users.tsx @@ -1,652 +1,17 @@ -import { useState, useEffect } from "react"; -import type { ReactElement } from "react"; +import { type ReactElement } from "react"; import { Layout } from "@/components/layout/Layout"; -import { - PrimaryButton, - StatusBadge, - ActionDropdown, - NewUserModal, - ViewUserModal, - EditUserModal, - // DeleteConfirmationModal, - DataTable, - Pagination, - FilterDropdown, - SearchBox, - type Column, -} from "@/components/shared"; -import { Plus, ArrowUpDown } from "lucide-react"; -import { userService } from "@/services/user-service"; -import { roleService } from "@/services/role-service"; -import type { User } from "@/types/user"; -import type { Role } from "@/types/role"; -import { showToast } from "@/utils/toast"; -import { usePermissions } from "@/hooks/usePermissions"; -import { useAppTheme } from "@/hooks/useAppTheme"; - -// Helper function to get user initials -const getUserInitials = (firstName: string, lastName: string): string => { - return `${firstName[0]}${lastName[0]}`.toUpperCase(); -}; - -// Helper function to format date -const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }); -}; - -// Helper function to get status badge variant -const getStatusVariant = ( - status: string, -): "success" | "failure" | "process" => { - switch (status.toLowerCase()) { - case "active": - return "success"; - case "pending_verification": - return "process"; - case "inactive": - return "failure"; - case "deleted": - return "failure"; - case "suspended": - return "process"; - default: - return "success"; - } -}; +import { UsersTable } from "@/components/superadmin"; const Users = (): ReactElement => { - const { primaryColor } = useAppTheme(); - const { canCreate, canUpdate - // , canDelete - } = usePermissions(); - const [users, setUsers] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [isCreating, setIsCreating] = useState(false); - - // Pagination state - const [currentPage, setCurrentPage] = useState(1); - const [limit, setLimit] = useState(5); - const [pagination, setPagination] = useState<{ - page: number; - limit: number; - total: number; - totalPages: number; - hasMore: boolean; - }>({ - page: 1, - limit: 5, - total: 0, - totalPages: 1, - hasMore: false, - }); - - // Filter state - const [statusFilter, setStatusFilter] = useState(null); - const [orderBy, setOrderBy] = useState(null); - const [roleFilter, setRoleFilter] = useState(null); - - // Search state - const [search, setSearch] = useState(""); - const [debouncedSearch, setDebouncedSearch] = useState(""); - - // Roles list for filter - const [roles, setRoles] = useState([]); - - // View, Edit, Delete modals - const [viewModalOpen, setViewModalOpen] = useState(false); - const [editModalOpen, setEditModalOpen] = useState(false); - // const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [selectedUserId, setSelectedUserId] = useState(null); - // const [selectedUserName, setSelectedUserName] = useState(""); - const [isUpdating, setIsUpdating] = useState(false); - // const [isDeleting, setIsDeleting] = useState(false); - - const fetchUsers = async ( - page: number, - itemsPerPage: number, - status: string | null = null, - sortBy: string[] | null = null, - searchQuery: string | null = null, - roleId: string | null = null, - ): Promise => { - try { - setIsLoading(true); - setError(null); - const response = await userService.getAll( - page, - itemsPerPage, - status, - sortBy, - searchQuery, - roleId, - ); - if (response.success) { - setUsers(response.data); - setPagination(response.pagination); - } else { - setError("Failed to load users"); - } - } catch (err: any) { - setError(err?.response?.data?.error?.message || "Failed to load users"); - } finally { - setIsLoading(false); - } - }; - - // Handle search debouncing - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedSearch(search); - // We only reset to first page if we are actively searching. - if (search) setCurrentPage(1); - }, 500); - return () => clearTimeout(timer); - }, [search]); - - // Fetch roles for filter - useEffect(() => { - const fetchRoles = async () => { - try { - const response = await roleService.getAll(1, 100); - if (response.success) { - setRoles(response.data); - } - } catch (err) { - console.error("Failed to fetch roles:", err); - } - }; - fetchRoles(); - }, []); - - // Fetch users on mount and when pagination/filters change - useEffect(() => { - fetchUsers(currentPage, limit, statusFilter, orderBy, debouncedSearch, roleFilter); - }, [currentPage, limit, statusFilter, orderBy, debouncedSearch, roleFilter]); - - const handleCreateUser = async (data: { - email: string; - password: string; - first_name: string; - last_name: string; - status: "active" | "suspended" | "deleted"; - auth_provider: "local"; - role_module_combinations: { role_id: string; module_id?: string | null }[]; - department_id?: string; - designation_id?: string; - }): Promise => { - try { - setIsCreating(true); - const response = await userService.create(data); - const message = response.message || `User created successfully`; - const description = response.message - ? undefined - : `${data.first_name} ${data.last_name} has been added`; - showToast.success(message, description); - setIsModalOpen(false); - await fetchUsers(currentPage, limit, statusFilter, orderBy); - } catch (err: any) { - throw err; - } finally { - setIsCreating(false); - } - }; - - // View user handler - const handleViewUser = (userId: string): void => { - setSelectedUserId(userId); - setViewModalOpen(true); - }; - - // Edit user handler - const handleEditUser = (userId: string - // , userName: string - ): void => { - setSelectedUserId(userId); - // setSelectedUserName(userName); - setEditModalOpen(true); - }; - - // Update user handler - const handleUpdateUser = async ( - id: string, - data: { - email: string; - first_name: string; - last_name: string; - status: "active" | "suspended" | "deleted"; - tenant_id: string; - role_module_combinations: { role_id: string; module_id?: string | null }[]; - department_id?: string; - designation_id?: string; - }, - ): Promise => { - try { - setIsUpdating(true); - const response = await userService.update(id, data); - const message = response.message || `User updated successfully`; - const description = response.message - ? undefined - : `${data.first_name} ${data.last_name} has been updated`; - showToast.success(message, description); - setEditModalOpen(false); - setSelectedUserId(null); - await fetchUsers(currentPage, limit, statusFilter, orderBy); - } catch (err: any) { - throw err; - } finally { - setIsUpdating(false); - } - }; - - // Delete user handler - // const handleDeleteUser = (userId: string, userName: string): void => { - // setSelectedUserId(userId); - // setSelectedUserName(userName); - // setDeleteModalOpen(true); - // }; - - // Confirm delete handler - // const handleConfirmDelete = async (): Promise => { - // if (!selectedUserId) return; - - // try { - // setIsDeleting(true); - // await userService.delete(selectedUserId); - // setDeleteModalOpen(false); - // setSelectedUserId(null); - // setSelectedUserName(""); - // await fetchUsers(currentPage, limit, statusFilter, orderBy); - // } catch (err: any) { - // throw err; - // } finally { - // setIsDeleting(false); - // } - // }; - - // Load user for view/edit - const loadUser = async (id: string): Promise => { - const response = await userService.getById(id); - return response.data; - }; - - // Define table columns - const columns: Column[] = [ - { - key: "name", - label: "User Name", - render: (user) => ( -
-
- - {getUserInitials(user.first_name, user.last_name)} - -
- - {user.first_name} {user.last_name} - -
- ), - mobileLabel: "Name", - }, - { - key: "email", - label: "Email", - render: (user) => ( - {user.email} - ), - }, - { - key: "role", - label: "Role", - render: (user) => ( -
- {user.role_module_combinations && user.role_module_combinations.length > 0 ? ( - user.role_module_combinations.map((combo, idx) => ( - - {combo.role_name} {combo.module_name && `(${combo.module_name})`} - - )) - ) : user.roles && user.roles.length > 0 ? ( - user.roles.map((role) => ( - - {role.name} - - )) - ) : ( - - {user.role?.name || "-"} - - )} -
- ), - }, - { - key: "status", - label: "Status", - render: (user) => ( - - {user.status} - - ), - }, - { - key: "auth_provider", - label: "Auth Provider", - render: (user) => ( - - {user.auth_provider} - - ), - }, - { - key: "created_at", - label: "Joined Date", - render: (user) => ( - - {formatDate(user.created_at)} - - ), - mobileLabel: "Joined", - }, - { - key: "actions", - label: "Actions", - align: "right", - render: (user) => ( -
- handleViewUser(user.id)} - onEdit={ - canUpdate("users") - ? () => - handleEditUser( - user.id, - // `${user.first_name} ${user.last_name}`, - ) - : undefined - } - // onDelete={ - // canDelete("users") - // ? () => - // handleDeleteUser( - // user.id, - // `${user.first_name} ${user.last_name}`, - // ) - // : undefined - // } - /> -
- ), - }, - ]; - - // Mobile card renderer - const mobileCardRenderer = (user: User) => ( -
-
-
-
- - {getUserInitials(user.first_name, user.last_name)} - -
-
-

- {user.first_name} {user.last_name} -

-

- {user.email} -

-
-
- handleViewUser(user.id)} - onEdit={ - canUpdate("users") - ? () => - handleEditUser( - user.id, - // `${user.first_name} ${user.last_name}`, - ) - : undefined - } - // onDelete={ - // canDelete("users") - // ? () => - // handleDeleteUser( - // user.id, - // `${user.first_name} ${user.last_name}`, - // ) - // : undefined - // } - /> -
-
-
- Status: -
- - {user.status} - -
-
-
- Auth Provider: -

- {user.auth_provider} -

-
-
- Joined: -

- {formatDate(user.created_at)} -

-
-
-
- ); - return ( - {/* Table Container */} -
- {/* Table Header with Filters */} -
- {/* Filters */} -
- {/* Global Search */} - - - {/* Status Filter */} - { - setStatusFilter(value as string | null); - setCurrentPage(1); // Reset to first page when filter changes - }} - placeholder="All" - /> - - {/* Role Filter */} - ({ value: role.id, label: role.name })) - ]} - value={roleFilter || ""} - onChange={(value) => { - setRoleFilter(Array.isArray(value) ? null : value || null); - setCurrentPage(1); - }} - placeholder="All" - /> - - {/* Sort Filter */} - { - setOrderBy(value as string[] | null); - setCurrentPage(1); // Reset to first page when sort changes - }} - placeholder="Default" - showIcon - icon={} - /> -
- - {/* Actions */} -
- {/* Export Button */} - {/* */} - - {/* New User Button */} - {canCreate("users") && ( - setIsModalOpen(true)} - > - - New User - - )} -
-
- - {/* Data Table */} - user.id} - mobileCardRenderer={mobileCardRenderer} - emptyMessage="No users found" - isLoading={isLoading} - error={error} - /> - - {/* Table Footer with Pagination */} - {pagination.total > 0 && ( - { - setCurrentPage(page); - }} - onLimitChange={(newLimit: number) => { - setLimit(newLimit); - setCurrentPage(1); // Reset to first page when limit changes - }} - /> - )} -
- - {/* New User Modal */} - setIsModalOpen(false)} - onSubmit={handleCreateUser} - isLoading={isCreating} - /> - - {/* View User Modal */} - { - setViewModalOpen(false); - setSelectedUserId(null); - }} - userId={selectedUserId} - onLoadUser={loadUser} - /> - - {/* Edit User Modal */} - { - setEditModalOpen(false); - setSelectedUserId(null); - // setSelectedUserName(""); - }} - userId={selectedUserId} - onLoadUser={loadUser} - onSubmit={handleUpdateUser} - isLoading={isUpdating} - /> - - {/* Delete Confirmation Modal */} - {/* { - setDeleteModalOpen(false); - setSelectedUserId(null); - setSelectedUserName(""); - }} - onConfirm={handleConfirmDelete} - title="Delete User" - message="Are you sure you want to delete this user" - itemName={selectedUserName} - isLoading={isDeleting} - /> */} +
); }; diff --git a/src/types/tenant.ts b/src/types/tenant.ts index 5af5cfb..10b5449 100644 --- a/src/types/tenant.ts +++ b/src/types/tenant.ts @@ -66,6 +66,8 @@ export interface Tenant { assignedModules?: AssignedModule[]; // Array of assigned modules with full details users?: TenantUser[]; // Array of tenant users tenant_admin?: TenantAdmin; // Tenant admin user details + user_count?: number; + module_count?: number; created_at: string; updated_at: string; }