refactor: centralize platform resources and improve role-based access control with standardized parsing and updated permission hooks

This commit is contained in:
Yashwin 2026-06-19 16:46:01 +05:30
parent ae72eebcea
commit edd8fe8089
23 changed files with 2783 additions and 1215 deletions

View File

@ -17,7 +17,7 @@ interface HeaderProps {
export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps): ReactElement => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { user, isLoading,roles } = useAppSelector((state) => state.auth);
const { user, isLoading, roles, tenantId } = useAppSelector((state) => state.auth);
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const dropdownRef = useRef<HTMLDivElement>(null);
@ -61,6 +61,8 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
};
}, [isDropdownOpen]);
const isPlatformUser = roles.includes('super_admin') || tenantId === '00000000-0000-0000-0000-000000000001';
// Handle logout
const handleLogout = async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
e.preventDefault();
@ -72,10 +74,9 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
// Check if user is on a tenant route to determine redirect path
// Note: use /tenant/ instead of /tenant to avoid matching /tenants
const isTenantRoute = window.location.pathname.startsWith('/tenant/') || window.location.pathname === '/tenant';
const isSuperAdmin = roles.includes('super_admin');
// Super admins always go to root login, tenant users go to /tenant/login if on a tenant route
const redirectPath = isSuperAdmin ? '/' : (isTenantRoute ? '/tenant/login' : '/');
// Platform users always go to root login, tenant users go to /tenant/login if on a tenant route
const redirectPath = isPlatformUser ? '/' : (isTenantRoute ? '/tenant/login' : '/');
try {
// Call logout API with Bearer token
@ -118,7 +119,7 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
{breadcrumbs[0].label !== 'QAssure' && (
<>
<button
onClick={() => navigate(roles.includes('super_admin') ? '/dashboard' : '/tenant')}
onClick={() => navigate(isPlatformUser ? '/dashboard' : '/tenant')}
className="text-xs md:text-[13px] font-normal text-[#6b7280] hover:text-[#0f1724] transition-colors cursor-pointer"
>
QAssure
@ -149,7 +150,7 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
) : (
<>
<button
onClick={() => navigate(roles.includes('super_admin') ? '/dashboard' : '/tenant')}
onClick={() => navigate(isPlatformUser ? '/dashboard' : '/tenant')}
className="text-xs md:text-[13px] font-normal text-[#6b7280] hover:text-[#0f1724] transition-colors cursor-pointer"
>
QAssure

View File

@ -52,10 +52,29 @@ interface SidebarProps {
// Super Admin menu items
const superAdminPlatformMenu: MenuItem[] = [
{ icon: LayoutDashboard, label: "Dashboard", path: "/dashboard" },
{ icon: Building2, label: "Tenants", path: "/tenants" },
// { icon: Users, label: 'User Management', path: '/users' },
// { icon: Shield, label: 'Roles', path: '/roles' },
{ icon: Package, label: "Modules", path: "/modules" },
{
icon: Building2,
label: "Tenants",
path: "/tenants",
requiredPermission: { resource: "tenants", action: "read" },
},
// Platform Users & Roles — no requiredPermission: super_admin always sees it;
// platform users do NOT see it (cannot manage other platform users)
{
icon: Users,
label: "Platform Users",
isGroup: true,
children: [
{ label: "Users", path: "/platform-users" },
{ label: "Roles & Permissions", path: "/platform-roles" },
],
},
{
icon: Package,
label: "Modules",
path: "/modules",
requiredPermission: { resource: "modules", action: "read" },
},
];
const superAdminSystemMenu: MenuItem[] = [
@ -65,21 +84,33 @@ const superAdminSystemMenu: MenuItem[] = [
isGroup: true,
children: [
{ label: "Notifications List", path: "/notifications" },
{ label: "Master Management", path: "/notification-master" },
{ label: "Global Templates", path: "/notification-templates" },
{ label: "Master Management", path: "/notification-master" },
{ label: "Global Templates", path: "/notification-templates" },
],
requiredPermission: { resource: "notifications", action: "read" },
},
{
icon: FileText,
label: "Audit Logs",
path: "/audit-logs",
requiredPermission: { resource: "audit_logs", action: "read" },
},
{
icon: Shield,
label: "Audit Resources",
path: "/audit-resource-types",
requiredPermission: { resource: "audit_resources", action: "read" },
},
{ icon: FileText, label: "Audit Logs", path: "/audit-logs" },
{ icon: Shield, label: "Audit Resources", path: "/audit-resource-types" },
{
icon: Settings,
label: "Settings",
isGroup: true,
children: [
{ label: "SMTP Config", path: "/settings/smtp" },
{ label: "Failed Emails", path: "/settings/failed-emails" },
{ label: "AI Fallback Monitoring", path: "/settings/ai-fallbacks" },
{ label: "SMTP Config", path: "/settings/smtp" },
{ label: "Failed Emails", path: "/settings/failed-emails" },
{ label: "AI Fallback Monitoring",path: "/settings/ai-fallbacks" },
],
requiredPermission: { resource: "settings", action: "read" },
},
];
@ -426,7 +457,7 @@ const GroupMenuItem = ({
export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const { primaryColor, secondaryColor, accentColor, logoUrl } = useAppTheme();
const location = useLocation();
const { roles, permissions } = useAppSelector((state) => state.auth);
const { roles, permissions, tenantId } = useAppSelector((state) => state.auth);
// Fetch theme for tenant admin
const isSuperAdminCheck = () => {
@ -445,11 +476,50 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const isSuperAdmin = isSuperAdminCheck();
/**
* Is this user a platform user (non-super-admin with a platform-scoped role)?
* Platform users see a filtered super-admin sidebar only the tabs they have
* permission for. The sidebar check re-uses the existing hasPermission() +
* filterMenuItems() logic that already works for tenant users.
*/
const isPlatformUser = !isSuperAdmin && (
tenantId === "00000000-0000-0000-0000-000000000001" ||
(() => {
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
rolesArray = roles;
} else if (typeof roles === "string") {
try { rolesArray = JSON.parse(roles); } catch { rolesArray = []; }
}
// A platform user is NOT super_admin and NOT a tenant_admin / viewer / etc.
// The server already embeds platform-scoped roles in the JWT.
// We detect it by: the user has no tenantId but has roles (or has any role
// that is not one of the known tenant roles). Simplest: if not super_admin
// and the user ended up on the super-admin layout, treat them as platform user.
const knownTenantRoles = ["tenant_admin", "quality_manager", "viewer"];
return rolesArray.some(r => !knownTenantRoles.includes(r) && r !== "super_admin");
})()
);
// Get role name for display
const getRoleName = (): string => {
if (isSuperAdmin) {
return "Super Admin";
}
if (isPlatformUser) {
// Format the platform role name nicely
let rolesArray: string[] = [];
if (Array.isArray(roles)) rolesArray = roles;
else if (typeof roles === "string") {
try { rolesArray = JSON.parse(roles); } catch { rolesArray = []; }
}
if (rolesArray.length > 0) {
return rolesArray[0]
.split("_")
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
.join(" ");
}
}
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
rolesArray = roles;
@ -546,14 +616,16 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
};
// Select and filter menu items based on role and permissions
// Platform users reuse the super-admin menu arrays but filterMenuItems() hides
// tabs they don't have permission for (same as tenant sidebar logic).
const platformMenu = filterMenuItems(
isSuperAdmin ? superAdminPlatformMenu : tenantAdminPlatformMenu,
isSuperAdmin || isPlatformUser ? superAdminPlatformMenu : tenantAdminPlatformMenu,
);
const platformServiceMenu = filterMenuItems(
isSuperAdmin ? [] : tenantAdminPlatformServiceMenu,
isSuperAdmin || isPlatformUser ? [] : tenantAdminPlatformServiceMenu,
);
const systemMenu = filterMenuItems(
isSuperAdmin ? superAdminSystemMenu : tenantAdminSystemMenu,
isSuperAdmin || isPlatformUser ? superAdminSystemMenu : tenantAdminSystemMenu,
);
const MenuSection = ({

View File

@ -13,12 +13,14 @@ import { toast } from "sonner";
import { Pagination } from "./Pagination";
import { ActionDropdown } from "./ActionDropdown";
import { PrimaryButton } from "./PrimaryButton";
import { usePermissions } from "@/hooks/usePermissions";
interface FailedEmailsTableProps {
onRegisterResendAll?: (node: React.ReactNode) => void;
}
export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegisterResendAll }) => {
const { canUpdate, canDelete } = usePermissions();
const [emails, setEmails] = useState<FailedEmail[]>([]);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
@ -86,22 +88,24 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
useEffect(() => {
if (onRegisterResendAll) {
onRegisterResendAll(
<PrimaryButton
onClick={handleResendAll}
disabled={
isResendingAll ||
emails.filter((e) => e.status === "failed").length === 0
}
>
{isResendingAll ? (
<>
<Loader2 className="mr-2 w-3.5 h-3.5 animate-spin" />
Resending...
</>
) : (
"Resend All Failed"
)}
</PrimaryButton>
canUpdate("settings") ? (
<PrimaryButton
onClick={handleResendAll}
disabled={
isResendingAll ||
emails.filter((e) => e.status === "failed").length === 0
}
>
{isResendingAll ? (
<>
<Loader2 className="mr-2 w-3.5 h-3.5 animate-spin" />
Resending...
</>
) : (
"Resend All Failed"
)}
</PrimaryButton>
) : null
);
}
return () => {
@ -109,7 +113,7 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
onRegisterResendAll(null);
}
};
}, [isResendingAll, emails, onRegisterResendAll]);
}, [isResendingAll, emails, onRegisterResendAll, canUpdate]);
const handleDelete = async (id: string) => {
try {
@ -167,7 +171,7 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
onClick: () => showEmailDetails(record),
icon: <Eye className="w-3.5 h-3.5 text-gray-500" />,
},
...(record.status === "failed"
...(record.status === "failed" && canUpdate("settings")
? [
{
label:
@ -184,12 +188,16 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
},
]
: []),
{
label: "Delete Email",
onClick: () => handleDelete(record.id),
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
variant: "danger",
},
...(canDelete("settings")
? [
{
label: "Delete Email",
onClick: () => handleDelete(record.id),
icon: <Trash2 className="w-3.5 h-3.5 text-red-600" />,
variant: "danger" as const,
},
]
: []),
]}
/>
</div>
@ -200,13 +208,8 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
return (
<div className="overflow-hidden">
{/* Toolbar / Actions Header */}
{!onRegisterResendAll && (
{!onRegisterResendAll && canUpdate("settings") && (
<div className="pb-2 flex justify-end">
{/* <div className="flex items-center gap-2 w-full sm:w-auto justify-end"> */}
{/* <Button variant="outline" onClick={() => fetchEmails(currentPage)}>
<RefreshCw className="mr-2 w-3.5 h-3.5" />
Refresh
</Button> */}
<PrimaryButton
onClick={handleResendAll}
disabled={
@ -223,7 +226,6 @@ export const FailedEmailsTable: React.FC<FailedEmailsTableProps> = ({ onRegister
"Resend All Failed"
)}
</PrimaryButton>
{/* </div> */}
</div>
)}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ import {
NewRoleModal,
ViewRoleModal,
EditRoleModal,
DeleteConfirmationModal,
// DeleteConfirmationModal,
DataTable,
Pagination,
FilterDropdown,
@ -97,11 +97,11 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
// View, Edit, Delete modals
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
const [selectedRoleName, setSelectedRoleName] = useState<string>("");
// const [selectedRoleName, setSelectedRoleName] = useState<string>("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
const fetchRoles = async (
page: number,
@ -173,9 +173,11 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
};
// Edit role handler
const handleEditRole = (roleId: string, roleName: string): void => {
const handleEditRole = (roleId: string
// , roleName: string
): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
// setSelectedRoleName(roleName);
setEditModalOpen(true);
};
@ -194,7 +196,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
showToast.success(message, description);
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
// setSelectedRoleName("");
await fetchRoles(currentPage, limit, orderBy);
} catch (err: any) {
throw err;
@ -204,29 +206,29 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
};
// Delete role handler
const handleDeleteRole = (roleId: string, roleName: string): void => {
setSelectedRoleId(roleId);
setSelectedRoleName(roleName);
setDeleteModalOpen(true);
};
// const handleDeleteRole = (roleId: string, roleName: string): void => {
// setSelectedRoleId(roleId);
// setSelectedRoleName(roleName);
// setDeleteModalOpen(true);
// };
// Confirm delete handler
const handleConfirmDelete = async (): Promise<void> => {
if (!selectedRoleId) return;
// const handleConfirmDelete = async (): Promise<void> => {
// 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; // Let the modal handle the error display
} finally {
setIsDeleting(false);
}
};
// try {
// setIsDeleting(true);
// await roleService.delete(selectedRoleId);
// setDeleteModalOpen(false);
// setSelectedRoleId(null);
// setSelectedRoleName("");
// await fetchRoles(currentPage, limit, orderBy);
// } catch (err: any) {
// throw err; // Let the modal handle the error display
// } finally {
// setIsDeleting(false);
// }
// };
// Load role for view/edit
const loadRole = async (id: string): Promise<Role> => {
@ -302,14 +304,16 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
onView={() => handleViewRole(role.id)}
onEdit={
isTenantAdmin
? () => handleEditRole(role.id, role.name)
: undefined
}
onDelete={
isTenantAdmin
? () => handleDeleteRole(role.id, role.name)
? () => handleEditRole(role.id
// , role.name
)
: undefined
}
// onDelete={
// isTenantAdmin
// ? () => handleDeleteRole(role.id, role.name)
// : undefined
// }
/>
</div>
),
@ -332,14 +336,16 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
onView={() => handleViewRole(role.id)}
onEdit={
isTenantAdmin
? () => handleEditRole(role.id, role.name)
: undefined
}
onDelete={
isTenantAdmin
? () => handleDeleteRole(role.id, role.name)
? () => handleEditRole(role.id
// , role.name
)
: undefined
}
// onDelete={
// isTenantAdmin
// ? () => handleDeleteRole(role.id, role.name)
// : undefined
// }
/>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
@ -450,7 +456,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
onClose={() => {
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
// setSelectedRoleName("");
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
@ -459,7 +465,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
defaultTenantId={tenantId || undefined}
/>
<DeleteConfirmationModal
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
@ -471,7 +477,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
message={`Are you sure you want to delete this role`}
itemName={selectedRoleName}
isLoading={isDeleting}
/>
/> */}
</>
);
}
@ -593,7 +599,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
onClose={() => {
setEditModalOpen(false);
setSelectedRoleId(null);
setSelectedRoleName("");
// setSelectedRoleName("");
}}
roleId={selectedRoleId}
onLoadRole={loadRole}
@ -602,7 +608,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
defaultTenantId={tenantId || undefined}
/>
<DeleteConfirmationModal
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
@ -614,7 +620,7 @@ export const RolesTable = forwardRef<RolesTableRef, RolesTableProps>(
message={`Are you sure you want to delete this role`}
itemName={selectedRoleName}
isLoading={isDeleting}
/>
/> */}
</>
);
},

View File

@ -0,0 +1,34 @@
/**
* Platform Resources QAssure Platform
* Canonical list of resources that platform roles can be granted permissions on.
* Derived from seed.js permissions list (lines 4881).
*
* Used by:
* - Platform Role create/edit permission matrix (checklist)
* - Sidebar menu item filtering for platform users
*/
export interface PlatformResource {
resource: string;
label: string;
actions: string[];
}
export const PLATFORM_RESOURCES: PlatformResource[] = [
{ resource: 'tenants', label: 'Tenants', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'users', label: 'Users', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'roles', label: 'Roles', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'permissions', label: 'Permissions', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'modules', label: 'Modules', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'audit_logs', label: 'Audit Logs', actions: ['read'] },
{ resource: 'audit_resources', label: 'Audit Resources', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'notifications', label: 'Notifications', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'smtp_config', label: 'SMTP Config', actions: ['read', 'update'] },
{ resource: 'api_keys', label: 'API Keys', actions: ['read', 'create', 'update', 'delete'] },
{ resource: 'settings', label: 'Settings', actions: ['read', 'update'] },
{ resource: 'security', label: 'Security', actions: ['read', 'update'] },
{ resource: 'ai_completions', label: 'AI Completions', actions: ['read'] },
];
/** Convenience: all distinct action names */
export const ALL_ACTIONS = ['read', 'create', 'update', 'delete'];

View File

@ -8,11 +8,28 @@ import { useAppSelector } from './redux-hooks';
export const usePermissions = () => {
const { roles, permissions } = useAppSelector((state) => state.auth);
const isSuperAdmin = useMemo(() => {
const rolesArray = Array.isArray(roles) ? roles : [];
return rolesArray.includes('super_admin');
const parsedRoles = useMemo((): string[] => {
if (Array.isArray(roles)) {
return roles;
}
if (typeof roles === 'string') {
try {
const parsed = JSON.parse(roles);
if (Array.isArray(parsed)) {
return parsed;
}
return [roles];
} catch {
return [roles];
}
}
return [];
}, [roles]);
const isSuperAdmin = useMemo(() => {
return parsedRoles.includes('super_admin');
}, [parsedRoles]);
/**
* Check if user has permission for a specific resource and action
* @param resource - The resource name (e.g., 'roles', 'users', 'audit_logs')
@ -81,9 +98,8 @@ export const usePermissions = () => {
);
const isTenantAdmin = useMemo(() => {
const rolesArray = Array.isArray(roles) ? roles : [];
return rolesArray.includes('tenant_admin') || rolesArray.includes('super_admin');
}, [roles]);
return parsedRoles.includes('tenant_admin') || parsedRoles.includes('super_admin');
}, [parsedRoles]);
return {
hasPermission,

View File

@ -29,7 +29,7 @@ type LoginFormData = z.infer<typeof loginSchema>;
const Login = (): ReactElement => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const { isLoading, error, isAuthenticated, roles } = useAppSelector(
const { isLoading, error, isAuthenticated, roles, tenantId } = useAppSelector(
(state) => state.auth,
);
@ -49,7 +49,7 @@ const Login = (): ReactElement => {
// Redirect if already authenticated
useEffect(() => {
if (isAuthenticated) {
// Check if user is super_admin, redirect to super admin dashboard
// Check if user is super_admin or a platform user, redirect to super admin dashboard
// Handle both array and JSON string formats
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
@ -61,14 +61,17 @@ const Login = (): ReactElement => {
rolesArray = [];
}
}
if (rolesArray.includes("super_admin")) {
const isPlatformUser =
rolesArray.includes("super_admin") ||
tenantId === "00000000-0000-0000-0000-000000000001";
if (isPlatformUser) {
navigate("/dashboard");
} else {
// Tenant admin - redirect to tenant landing page (workspace selector)
navigate("/tenant/landing");
}
}
}, [isAuthenticated, roles, navigate]);
}, [isAuthenticated, roles, tenantId, navigate]);
// Clear errors only on component mount, not on every auth state change
useEffect(() => {
@ -104,7 +107,11 @@ const Login = (): ReactElement => {
// Check roles after login to redirect appropriately
const userRoles = result.data.roles || [];
if (userRoles.includes("super_admin")) {
const userTenantId = result.data.tenant_id;
const isPlatformUser =
userRoles.includes("super_admin") ||
userTenantId === "00000000-0000-0000-0000-000000000001";
if (isPlatformUser) {
navigate("/dashboard");
} else {
navigate("/tenant/landing");

View File

@ -7,13 +7,13 @@ interface ProtectedRouteProps {
}
const ProtectedRoute = ({ children }: ProtectedRouteProps): ReactElement => {
const { isAuthenticated, roles } = useAppSelector((state) => state.auth);
const { isAuthenticated, roles, tenantId } = useAppSelector((state) => state.auth);
if (!isAuthenticated) {
return <Navigate to="/" replace />;
}
// Check if user has super_admin role
// Check if user has super_admin role or is a platform user
// Handle both array and JSON string formats
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
@ -26,9 +26,10 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps): ReactElement => {
}
}
const hasSuperAdminRole = rolesArray && rolesArray.length > 0 && rolesArray.includes('super_admin');
const isPlatformUser = hasSuperAdminRole || tenantId === '00000000-0000-0000-0000-000000000001';
if (!hasSuperAdminRole) {
// If not super_admin, redirect to tenant login
if (!isPlatformUser) {
// If not a platform user, redirect to tenant login
return <Navigate to="/tenant/login" replace />;
}

View File

@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from "react";
import { Layout } from "@/components/layout/Layout";
import { usePermissions } from "@/hooks/usePermissions";
import {
DataTable,
Pagination,
@ -49,6 +50,7 @@ const BUILT_IN_PROVIDERS = [
const NONE_OPTION = { value: "__none__", label: "— No fallback —" };
export const AIFallbackHistory = () => {
const { canUpdate } = usePermissions();
const [fallbacks, setFallbacks] = useState<FallbackEvent[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedEvent, setSelectedEvent] = useState<FallbackEvent | null>(null);
@ -432,7 +434,8 @@ export const AIFallbackHistory = () => {
<select
value={currentFallback}
onChange={(e) => handleMappingChange(provider.name, e.target.value)}
className="w-full h-9 px-3 text-sm border border-slate-200 rounded-lg bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-[#112868]/20 focus:border-[#112868] transition-all cursor-pointer"
disabled={!canUpdate("settings")}
className="w-full h-9 px-3 text-sm border border-slate-200 rounded-lg bg-white text-slate-800 focus:outline-none focus:ring-2 focus:ring-[#112868]/20 focus:border-[#112868] transition-all cursor-pointer disabled:bg-slate-100 disabled:text-slate-400 disabled:cursor-not-allowed"
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
@ -464,23 +467,25 @@ export const AIFallbackHistory = () => {
{availableProviders.length} provider{availableProviders.length !== 1 ? "s" : ""} configured.
Add more via the <span className="font-semibold text-slate-500">Dynamic Fallback Models & API Keys</span> tab.
</p>
<PrimaryButton
onClick={handleSaveMapping}
disabled={isSavingMapping}
className="h-9 px-5 text-xs flex items-center gap-1.5 rounded-lg"
>
{isSavingMapping ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-3.5 h-3.5" />
Save Fallback Mapping
</>
)}
</PrimaryButton>
{canUpdate("settings") && (
<PrimaryButton
onClick={handleSaveMapping}
disabled={isSavingMapping}
className="h-9 px-5 text-xs flex items-center gap-1.5 rounded-lg"
>
{isSavingMapping ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-3.5 h-3.5" />
Save Fallback Mapping
</>
)}
</PrimaryButton>
)}
</div>
</>
)}

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import { usePermissions } from "@/hooks/usePermissions";
import {
StatusBadge,
PrimaryButton,
@ -53,6 +54,7 @@ const getStatusVariant = (
};
const Modules = (): ReactElement => {
const { canCreate, canUpdate } = usePermissions();
const [modules, setModules] = useState<Module[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -291,21 +293,25 @@ const Modules = (): ReactElement => {
label: "View",
onClick: () => handleViewModule(module.id),
},
{
icon: <Edit className="w-3.5 h-3.5 shrink-0" />,
label: "Edit",
onClick: () => handleEditModule(module),
},
{
icon: <Key className="w-3.5 h-3.5 shrink-0" />,
label: "Reissue API Key",
onClick: () => handleOpenReissueApiKey(module.id),
},
{
icon: <CloudSync className="w-3.5 h-3.5 shrink-0" />,
label: "Webhook Sync",
onClick: () => handleOpenWebhookSync(module.id),
},
...(canUpdate("modules")
? [
{
icon: <Edit className="w-3.5 h-3.5 shrink-0" />,
label: "Edit",
onClick: () => handleEditModule(module),
},
{
icon: <Key className="w-3.5 h-3.5 shrink-0" />,
label: "Reissue API Key",
onClick: () => handleOpenReissueApiKey(module.id),
},
{
icon: <CloudSync className="w-3.5 h-3.5 shrink-0" />,
label: "Webhook Sync",
onClick: () => handleOpenWebhookSync(module.id),
},
]
: []),
]}
/>
</div>
@ -482,14 +488,16 @@ const Modules = (): ReactElement => {
</button> */}
{/* New Module Button */}
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Module</span>
</PrimaryButton>
{canCreate("modules") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => setIsModalOpen(true)}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">New Module</span>
</PrimaryButton>
)}
</div>
</div>

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useRef, type KeyboardEvent } from "react";
import type { ReactElement } from "react";
import { useLocation } from "react-router-dom";
import { Layout } from "@/components/layout/Layout";
import { usePermissions } from "@/hooks/usePermissions";
import {
PrimaryButton,
DataTable,
@ -132,6 +133,7 @@ const VariableTagInput = ({
};
const NotificationMaster = (): ReactElement => {
const { canCreate, canUpdate, canDelete } = usePermissions();
const location = useLocation();
const [categories, setCategories] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
@ -408,20 +410,28 @@ const NotificationMaster = (): ReactElement => {
Codes ({c.code_count || 0})
</button>
<ActionDropdown
onEdit={() => {
setEditingCategory(c);
resetCategory({
name: c.name,
code: c.code,
description: c.description || "",
module_id: c.module_id || "",
});
setCategoryModalOpen(true);
}}
onDelete={() => {
setDeleteTarget({ id: c.id, name: c.name, type: "category" });
setDeleteModalOpen(true);
}}
onEdit={
canUpdate("notifications")
? () => {
setEditingCategory(c);
resetCategory({
name: c.name,
code: c.code,
description: c.description || "",
module_id: c.module_id || "",
});
setCategoryModalOpen(true);
}
: undefined
}
onDelete={
canDelete("notifications")
? () => {
setDeleteTarget({ id: c.id, name: c.name, type: "category" });
setDeleteModalOpen(true);
}
: undefined
}
/>
</div>
),
@ -471,36 +481,40 @@ const NotificationMaster = (): ReactElement => {
align: "right",
render: (c) => (
<div className="flex justify-end gap-3 whitespace-nowrap">
<button
onClick={() => {
setEditingCode(c);
setCodeVariables(Array.isArray(c.variables) ? c.variables : []);
resetCode({
code: c.code,
name: c.name,
description: c.description || "",
default_channels: c.default_channels,
default_priority: c.default_priority,
});
setCodeFormModalOpen(true);
}}
className="text-blue-600 hover:text-blue-800 font-bold text-[11px] uppercase"
>
Edit
</button>
<button
onClick={() => {
setDeleteTarget({
id: c.id,
name: c.code,
type: "code",
});
setDeleteModalOpen(true);
}}
className="text-red-500 hover:text-red-700 font-bold text-[11px] uppercase"
>
Delete
</button>
{canUpdate("notifications") && (
<button
onClick={() => {
setEditingCode(c);
setCodeVariables(Array.isArray(c.variables) ? c.variables : []);
resetCode({
code: c.code,
name: c.name,
description: c.description || "",
default_channels: c.default_channels,
default_priority: c.default_priority,
});
setCodeFormModalOpen(true);
}}
className="text-blue-600 hover:text-blue-800 font-bold text-[11px] uppercase"
>
Edit
</button>
)}
{canDelete("notifications") && (
<button
onClick={() => {
setDeleteTarget({
id: c.id,
name: c.code,
type: "code",
});
setDeleteModalOpen(true);
}}
className="text-red-500 hover:text-red-700 font-bold text-[11px] uppercase"
>
Delete
</button>
)}
</div>
),
},
@ -536,23 +550,25 @@ const NotificationMaster = (): ReactElement => {
>
Back to Categories
</button>
<PrimaryButton
onClick={() => {
setEditingCode(null);
setCodeVariables([]);
resetCode({
code: "",
name: "",
description: "",
default_channels: ["in_app", "email"],
default_priority: "normal",
});
setCodeFormModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Code
</PrimaryButton>
{canCreate("notifications") && (
<PrimaryButton
onClick={() => {
setEditingCode(null);
setCodeVariables([]);
resetCode({
code: "",
name: "",
description: "",
default_channels: ["in_app", "email"],
default_priority: "normal",
});
setCodeFormModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Code
</PrimaryButton>
)}
</div>
) : undefined,
}}
@ -583,21 +599,23 @@ const NotificationMaster = (): ReactElement => {
isSearchable
/>
</div>
<PrimaryButton
onClick={() => {
setEditingCategory(null);
resetCategory({
name: "",
code: "",
description: "",
module_id: "",
});
setCategoryModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Category
</PrimaryButton>
{canCreate("notifications") && (
<PrimaryButton
onClick={() => {
setEditingCategory(null);
resetCategory({
name: "",
code: "",
description: "",
module_id: "",
});
setCategoryModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Category
</PrimaryButton>
)}
</div>
{/* <div className="flex-1"> */}

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import { usePermissions } from "@/hooks/usePermissions";
import {
PrimaryButton,
DataTable,
@ -91,6 +92,7 @@ const VariableChips = ({
};
const NotificationTemplateMaster = (): ReactElement => {
const { canCreate, canUpdate } = usePermissions();
const [templates, setTemplates] = useState<any[]>([]);
const [modules, setModules] = useState<any[]>([]);
const [selectedModule, setSelectedModule] = useState<string | null>(null);
@ -330,12 +332,14 @@ const NotificationTemplateMaster = (): ReactElement => {
label: "Actions",
align: "right",
render: (t) => (
<button
onClick={() => openEditModal(t)}
className="text-xs text-blue-600 hover:underline font-semibold"
>
Edit
</button>
canUpdate("notifications") ? (
<button
onClick={() => openEditModal(t)}
className="text-xs text-blue-600 hover:underline font-semibold"
>
Edit
</button>
) : null
),
},
];
@ -377,30 +381,32 @@ const NotificationTemplateMaster = (): ReactElement => {
</div>
</div>
</div>
<PrimaryButton
onClick={() => {
setEditingId(null);
setTemplateVariables([]);
setEmailBodyHtml("");
reset({
code: "",
name: "",
description: "",
category: "",
title_template: "",
message_template: "",
email_subject_template: "",
email_body_template: "",
default_priority: "normal",
channels: ["in_app", "email"],
is_active: true,
});
setModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Template
</PrimaryButton>
{canCreate("notifications") && (
<PrimaryButton
onClick={() => {
setEditingId(null);
setTemplateVariables([]);
setEmailBodyHtml("");
reset({
code: "",
name: "",
description: "",
category: "",
title_template: "",
message_template: "",
email_subject_template: "",
email_body_template: "",
default_priority: "normal",
channels: ["in_app", "email"],
is_active: true,
});
setModalOpen(true);
}}
className="flex gap-2"
>
<Plus className="w-4 h-4" /> New Template
</PrimaryButton>
)}
</div>
<div className="flex-1">

View File

@ -1,443 +1,19 @@
// import { useState, useEffect } from 'react';
// import type { ReactElement } from 'react';
// import { Layout } from '@/components/layout/Layout';
// import {
// PrimaryButton,
// StatusBadge,
// ActionDropdown,
// NewRoleModal,
// ViewRoleModal,
// EditRoleModal,
// DeleteConfirmationModal,
// DataTable,
// Pagination,
// FilterDropdown,
// 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 type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import { PlatformRolesTable } from "@/components/superadmin/PlatformRolesTable";
// // 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' });
// };
const Roles = (): ReactElement => {
return (
<Layout
currentPage="Roles"
pageHeader={{
title: "Platform Roles",
description: "Manage system-wide administrative roles and permissions.",
}}
>
<PlatformRolesTable />
</Layout>
);
};
// // 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';
// }
// };
// const Roles = (): ReactElement => {
// const [roles, setRoles] = useState<Role[]>([]);
// const [isLoading, setIsLoading] = useState<boolean>(true);
// const [error, setError] = useState<string | null>(null);
// const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
// const [isCreating, setIsCreating] = useState<boolean>(false);
// // Pagination state
// const [currentPage, setCurrentPage] = useState<number>(1);
// const [limit, setLimit] = useState<number>(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<string | null>(null);
// const [orderBy, setOrderBy] = useState<string[] | null>(null);
// // View, Edit, Delete modals
// const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
// const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
// const [selectedRoleId, setSelectedRoleId] = useState<string | null>(null);
// const [selectedRoleName, setSelectedRoleName] = useState<string>('');
// const [isUpdating, setIsUpdating] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
// const fetchRoles = async (
// page: number,
// itemsPerPage: number,
// scope: string | null = null,
// sortBy: string[] | null = null
// ): Promise<void> => {
// try {
// setIsLoading(true);
// setError(null);
// const response = await roleService.getAll(page, itemsPerPage, scope, sortBy);
// 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);
// }
// };
// useEffect(() => {
// fetchRoles(currentPage, limit, scopeFilter, orderBy);
// }, [currentPage, limit, scopeFilter, orderBy]);
// const handleCreateRole = async (data: CreateRoleRequest): Promise<void> => {
// 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, scopeFilter, 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<void> => {
// 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, scopeFilter, 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<void> => {
// if (!selectedRoleId) return;
// try {
// setIsDeleting(true);
// await roleService.delete(selectedRoleId);
// setDeleteModalOpen(false);
// setSelectedRoleId(null);
// setSelectedRoleName('');
// await fetchRoles(currentPage, limit, scopeFilter, orderBy);
// } catch (err: any) {
// throw err;
// } finally {
// setIsDeleting(false);
// }
// };
// // Load role for view/edit
// const loadRole = async (id: string): Promise<Role> => {
// const response = await roleService.getById(id);
// return response.data;
// };
// // Table columns
// const columns: Column<Role>[] = [
// {
// key: 'name',
// label: 'Name',
// render: (role) => (
// <span className="text-sm font-normal text-[#0f1724]">{role.name}</span>
// ),
// },
// {
// key: 'code',
// label: 'Code',
// render: (role) => (
// <span className="text-sm font-normal text-[#0f1724] font-mono">{role.code}</span>
// ),
// },
// {
// key: 'scope',
// label: 'Scope',
// render: (role) => (
// <StatusBadge variant={getScopeVariant(role.scope)}>{role.scope}</StatusBadge>
// ),
// },
// {
// key: 'description',
// label: 'Description',
// render: (role) => (
// <span className="text-sm font-normal text-[#6b7280]">
// {role.description || 'N/A'}
// </span>
// ),
// },
// {
// key: 'is_system',
// label: 'System Role',
// render: (role) => (
// <span className="text-sm font-normal text-[#0f1724]">
// {role.is_system ? 'Yes' : 'No'}
// </span>
// ),
// },
// {
// key: 'created_at',
// label: 'Created Date',
// render: (role) => (
// <span className="text-sm font-normal text-[#6b7280]">{formatDate(role.created_at)}</span>
// ),
// },
// {
// key: 'actions',
// label: 'Actions',
// align: 'right',
// render: (role) => (
// <div className="flex justify-end">
// <ActionDropdown
// onView={() => handleViewRole(role.id)}
// onEdit={() => handleEditRole(role.id, role.name)}
// onDelete={() => handleDeleteRole(role.id, role.name)}
// />
// </div>
// ),
// },
// ];
// // Mobile card renderer
// const mobileCardRenderer = (role: Role) => (
// <div className="p-4">
// <div className="flex items-start justify-between gap-3 mb-3">
// <div className="flex-1 min-w-0">
// <h3 className="text-sm font-medium text-[#0f1724] truncate">{role.name}</h3>
// <p className="text-xs text-[#6b7280] mt-0.5 font-mono">{role.code}</p>
// </div>
// <ActionDropdown
// onView={() => handleViewRole(role.id)}
// onEdit={() => handleEditRole(role.id, role.name)}
// onDelete={() => handleDeleteRole(role.id, role.name)}
// />
// </div>
// <div className="grid grid-cols-2 gap-3 text-xs">
// <div>
// <span className="text-[#9aa6b2]">Scope:</span>
// <div className="mt-1">
// <StatusBadge variant={getScopeVariant(role.scope)}>{role.scope}</StatusBadge>
// </div>
// </div>
// <div>
// <span className="text-[#9aa6b2]">Created:</span>
// <p className="text-[#0f1724] font-normal mt-1">{formatDate(role.created_at)}</p>
// </div>
// {role.description && (
// <div className="col-span-2">
// <span className="text-[#9aa6b2]">Description:</span>
// <p className="text-[#0f1724] font-normal mt-1">{role.description}</p>
// </div>
// )}
// </div>
// </div>
// );
// return (
// <Layout
// currentPage="Roles"
// pageHeader={{
// title: 'Role List',
// description: 'Define and manage roles to control user access based on job responsibilities',
// }}
// >
// {/* Table Container */}
// <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
// {/* Table Header with Filters */}
// <div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
// {/* Filters */}
// <div className="flex flex-wrap items-center gap-3">
// {/* Scope Filter */}
// <FilterDropdown
// label="Scope"
// options={[
// { value: 'platform', label: 'Platform' },
// { value: 'tenant', label: 'Tenant' },
// { value: 'module', label: 'Module' },
// ]}
// value={scopeFilter}
// onChange={(value) => {
// setScopeFilter(value as string | null);
// setCurrentPage(1);
// }}
// placeholder="All"
// />
// {/* Sort Filter */}
// <FilterDropdown
// label="Sort by"
// options={[
// { value: ['name', 'asc'], label: 'Name (A-Z)' },
// { value: ['name', 'desc'], label: 'Name (Z-A)' },
// { value: ['code', 'asc'], label: 'Code (A-Z)' },
// { value: ['code', 'desc'], label: 'Code (Z-A)' },
// { value: ['created_at', 'asc'], label: 'Created (Oldest)' },
// { value: ['created_at', 'desc'], label: 'Created (Newest)' },
// { value: ['updated_at', 'asc'], label: 'Updated (Oldest)' },
// { value: ['updated_at', 'desc'], label: 'Updated (Newest)' },
// ]}
// value={orderBy}
// onChange={(value) => {
// setOrderBy(value as string[] | null);
// setCurrentPage(1);
// }}
// placeholder="Default"
// showIcon
// icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
// />
// </div>
// {/* Actions */}
// <div className="flex items-center gap-2">
// {/* Export Button */}
// <button
// type="button"
// className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
// >
// <Download className="w-3.5 h-3.5" />
// <span>Export</span>
// </button>
// {/* New Role Button */}
// <PrimaryButton
// size="default"
// className="flex items-center gap-2"
// onClick={() => setIsModalOpen(true)}
// >
// <Plus className="w-3.5 h-3.5" />
// <span className="text-xs">New Role</span>
// </PrimaryButton>
// </div>
// </div>
// {/* Table */}
// <DataTable
// data={roles}
// columns={columns}
// keyExtractor={(role) => role.id}
// mobileCardRenderer={mobileCardRenderer}
// emptyMessage="No roles found"
// isLoading={isLoading}
// error={error}
// />
// {/* Table Footer with Pagination */}
// {pagination.total > 0 && (
// <Pagination
// currentPage={currentPage}
// totalPages={pagination.totalPages}
// totalItems={pagination.total}
// limit={limit}
// onPageChange={(page: number) => {
// setCurrentPage(page);
// }}
// onLimitChange={(newLimit: number) => {
// setLimit(newLimit);
// setCurrentPage(1);
// }}
// />
// )}
// </div>
// {/* New Role Modal */}
// <NewRoleModal
// isOpen={isModalOpen}
// onClose={() => setIsModalOpen(false)}
// onSubmit={handleCreateRole}
// isLoading={isCreating}
// />
// {/* View Role Modal */}
// <ViewRoleModal
// isOpen={viewModalOpen}
// onClose={() => {
// setViewModalOpen(false);
// setSelectedRoleId(null);
// }}
// roleId={selectedRoleId}
// onLoadRole={loadRole}
// />
// {/* Edit Role Modal */}
// <EditRoleModal
// isOpen={editModalOpen}
// onClose={() => {
// setEditModalOpen(false);
// setSelectedRoleId(null);
// setSelectedRoleName('');
// }}
// roleId={selectedRoleId}
// onLoadRole={loadRole}
// onSubmit={handleUpdateRole}
// isLoading={isUpdating}
// />
// {/* Delete Confirmation Modal */}
// <DeleteConfirmationModal
// isOpen={deleteModalOpen}
// onClose={() => {
// setDeleteModalOpen(false);
// setSelectedRoleId(null);
// setSelectedRoleName('');
// }}
// onConfirm={handleConfirmDelete}
// title="Delete Role"
// message={`Are you sure you want to delete this role`}
// itemName={selectedRoleName}
// isLoading={isDeleting}
// />
// </Layout>
// );
// };
// export default Roles;
export default Roles;

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { Layout } from "@/components/layout/Layout";
import { usePermissions } from "@/hooks/usePermissions";
import {
DataTable,
Pagination,
@ -18,6 +19,7 @@ import { SmtpConfigModal } from "@/components/superadmin/SmtpConfigModal";
import { showToast } from "@/utils/toast";
const SmtpConfigPage = () => {
const { canCreate, canUpdate, canDelete } = usePermissions();
const [configs, setConfigs] = useState<SmtpConfig[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
@ -139,8 +141,8 @@ const SmtpConfigPage = () => {
align: "right",
render: (config) => (
<ActionDropdown
onEdit={() => handleEdit(config)}
onDelete={() => handleDelete(config)}
onEdit={canUpdate("settings") ? () => handleEdit(config) : undefined}
onDelete={canDelete("settings") ? () => handleDelete(config) : undefined}
/>
),
},
@ -153,7 +155,7 @@ const SmtpConfigPage = () => {
title: "SMTP Configurations",
description:
"Manage email delivery settings for the entire platform and individual tenants.",
action: (
action: canCreate("settings") ? (
<PrimaryButton
onClick={() => {
setSelectedConfig(null);
@ -163,7 +165,7 @@ const SmtpConfigPage = () => {
<Plus className="w-4 h-4 mr-2" />
Add Configuration
</PrimaryButton>
),
) : undefined,
}}
>
<div className="overflow-hidden">

View File

@ -17,6 +17,7 @@ import { Plus, ArrowUpDown } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { tenantService } from "@/services/tenant-service";
import type { Tenant } from "@/types/tenant";
import { usePermissions } from "@/hooks/usePermissions";
// Helper function to get tenant initials
const getTenantInitials = (name: string): string => {
const words = name.trim().split(/\s+/);
@ -60,6 +61,7 @@ const formatSubscriptionTier = (tier: string | null): string => {
const Tenants = (): ReactElement => {
const navigate = useNavigate();
const { canCreate, canUpdate } = usePermissions();
const [tenants, setTenants] = useState<Tenant[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -254,11 +256,7 @@ const Tenants = (): ReactElement => {
{
key: "user_count",
label: "Users",
render: (tenant) => (
<span className="">
{tenant.user_count ?? 0}
</span>
),
render: (tenant) => <span className="">{tenant.user_count ?? 0}</span>,
},
{
key: "subscription_tier",
@ -272,19 +270,13 @@ const Tenants = (): ReactElement => {
{
key: "module_count",
label: "Modules",
render: (tenant) => (
<span className="">
{tenant.module_count ?? 0}
</span>
),
render: (tenant) => <span className="">{tenant.module_count ?? 0}</span>,
},
{
key: "created_at",
label: "Joined Date",
render: (tenant) => (
<span className="">
{formatDate(tenant.created_at)}
</span>
<span className="">{formatDate(tenant.created_at)}</span>
),
mobileLabel: "Joined",
},
@ -296,7 +288,9 @@ const Tenants = (): ReactElement => {
<div className="flex justify-end">
<ActionDropdown
onView={() => handleViewTenant(tenant.id)}
onEdit={() => handleEditTenant(tenant.id)}
onEdit={
canUpdate("users") ? () => handleEditTenant(tenant.id) : undefined
}
// onDelete={() => handleDeleteTenant(tenant.id, tenant.name)}
/>
</div>
@ -445,14 +439,16 @@ const Tenants = (): ReactElement => {
</PrimaryButton> */}
{/* Add Tenant Button (New Wizard) */}
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => navigate("/tenants/create-wizard")}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">Add Tenant</span>
</PrimaryButton>
{canCreate("tenants") && (
<PrimaryButton
size="default"
className="flex items-center gap-2"
onClick={() => navigate("/tenants/create-wizard")}
>
<Plus className="w-3.5 h-3.5" />
<span className="text-xs">Add Tenant</span>
</PrimaryButton>
)}
</div>
</div>

View File

@ -1,476 +1,19 @@
// import { useState, useEffect } from 'react';
// import type { ReactElement } from 'react';
// import { Layout } from '@/components/layout/Layout';
// import {
// PrimaryButton,
// StatusBadge,
// ActionDropdown,
// NewUserModal,
// ViewUserModal,
// EditUserModal,
// DeleteConfirmationModal,
// DataTable,
// Pagination,
// FilterDropdown,
// type Column,
// } from '@/components/shared';
// import { Plus, Download, ArrowUpDown } from 'lucide-react';
// import { userService } from '@/services/user-service';
// import type { User } from '@/types/user';
// import { showToast } from '@/utils/toast';
import type { ReactElement } from "react";
import { Layout } from "@/components/layout/Layout";
import { PlatformUsersTable } from "@/components/superadmin/PlatformUsersTable";
// // Helper function to get user initials
// const getUserInitials = (firstName: string, lastName: string): string => {
// return `${firstName[0]}${lastName[0]}`.toUpperCase();
// };
const Users = (): ReactElement => {
return (
<Layout
currentPage="Users"
pageHeader={{
title: "Platform Users",
description: "Manage system-wide administrative personnel and platform users.",
}}
>
<PlatformUsersTable />
</Layout>
);
};
// // 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';
// }
// };
// const Users = (): ReactElement => {
// const [users, setUsers] = useState<User[]>([]);
// const [isLoading, setIsLoading] = useState<boolean>(true);
// const [error, setError] = useState<string | null>(null);
// const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
// const [isCreating, setIsCreating] = useState<boolean>(false);
// // Pagination state
// const [currentPage, setCurrentPage] = useState<number>(1);
// const [limit, setLimit] = useState<number>(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<string | null>(null);
// const [orderBy, setOrderBy] = useState<string[] | null>(null);
// // View, Edit, Delete modals
// const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
// const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
// const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
// const [selectedUserName, setSelectedUserName] = useState<string>('');
// const [isUpdating, setIsUpdating] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
// const fetchUsers = async (
// page: number,
// itemsPerPage: number,
// status: string | null = null,
// sortBy: string[] | null = null
// ): Promise<void> => {
// try {
// setIsLoading(true);
// setError(null);
// const response = await userService.getAll(page, itemsPerPage, status, sortBy);
// 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);
// }
// };
// useEffect(() => {
// fetchUsers(currentPage, limit, statusFilter, orderBy);
// }, [currentPage, limit, statusFilter, orderBy]);
// const handleCreateUser = async (data: {
// email: string;
// password: string;
// first_name: string;
// last_name: string;
// status: 'active' | 'suspended' | 'deleted';
// auth_provider: 'local';
// role_id: string;
// }): Promise<void> => {
// 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_id: string;
// }
// ): Promise<void> => {
// 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<void> => {
// 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<User> => {
// const response = await userService.getById(id);
// return response.data;
// };
// // Define table columns
// const columns: Column<User>[] = [
// {
// key: 'name',
// label: 'User Name',
// render: (user) => (
// <div className="flex items-center gap-3">
// <div className="w-8 h-8 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
// <span className="text-xs font-normal text-[#9aa6b2]">
// {getUserInitials(user.first_name, user.last_name)}
// </span>
// </div>
// <span className="text-sm font-normal text-[#0f1724]">
// {user.first_name} {user.last_name}
// </span>
// </div>
// ),
// mobileLabel: 'Name',
// },
// {
// key: 'email',
// label: 'Email',
// render: (user) => <span className="text-sm font-normal text-[#0f1724]">{user.email}</span>,
// },
// {
// key: 'status',
// label: 'Status',
// render: (user) => (
// <StatusBadge variant={getStatusVariant(user.status)}>{user.status}</StatusBadge>
// ),
// },
// {
// key: 'auth_provider',
// label: 'Auth Provider',
// render: (user) => (
// <span className="text-sm font-normal text-[#0f1724]">{user.auth_provider}</span>
// ),
// },
// {
// key: 'created_at',
// label: 'Joined Date',
// render: (user) => (
// <span className="text-sm font-normal text-[#6b7280]">{formatDate(user.created_at)}</span>
// ),
// mobileLabel: 'Joined',
// },
// {
// key: 'actions',
// label: 'Actions',
// align: 'right',
// render: (user) => (
// <div className="flex justify-end">
// <ActionDropdown
// onView={() => handleViewUser(user.id)}
// onEdit={() => handleEditUser(user.id, `${user.first_name} ${user.last_name}`)}
// onDelete={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)}
// />
// </div>
// ),
// },
// ];
// // Mobile card renderer
// const mobileCardRenderer = (user: User) => (
// <div className="p-4">
// <div className="flex items-start justify-between gap-3 mb-3">
// <div className="flex items-center gap-3 flex-1 min-w-0">
// <div className="w-8 h-8 bg-[#f5f7fa] border border-[rgba(0,0,0,0.08)] rounded-lg flex items-center justify-center shrink-0">
// <span className="text-xs font-normal text-[#9aa6b2]">
// {getUserInitials(user.first_name, user.last_name)}
// </span>
// </div>
// <div className="flex-1 min-w-0">
// <h3 className="text-sm font-medium text-[#0f1724] truncate">
// {user.first_name} {user.last_name}
// </h3>
// <p className="text-xs text-[#6b7280] mt-0.5 truncate">{user.email}</p>
// </div>
// </div>
// <ActionDropdown
// onView={() => handleViewUser(user.id)}
// onEdit={() => handleEditUser(user.id, `${user.first_name} ${user.last_name}`)}
// onDelete={() => handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)}
// />
// </div>
// <div className="grid grid-cols-2 gap-3 text-xs">
// <div>
// <span className="text-[#9aa6b2]">Status:</span>
// <div className="mt-1">
// <StatusBadge variant={getStatusVariant(user.status)}>{user.status}</StatusBadge>
// </div>
// </div>
// <div>
// <span className="text-[#9aa6b2]">Auth Provider:</span>
// <p className="text-[#0f1724] font-normal mt-1">{user.auth_provider}</p>
// </div>
// <div>
// <span className="text-[#9aa6b2]">Joined:</span>
// <p className="text-[#6b7280] font-normal mt-1">{formatDate(user.created_at)}</p>
// </div>
// </div>
// </div>
// );
// return (
// <Layout
// currentPage="Users"
// pageHeader={{
// title: 'User List',
// description: 'View and manage all users in your QAssure platform from a single place.',
// }}
// >
// {/* Table Container */}
// <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] overflow-hidden">
// {/* Table Header with Filters */}
// <div className="border-b border-[rgba(0,0,0,0.08)] px-4 md:px-5 py-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
// {/* Filters */}
// <div className="flex flex-wrap items-center gap-3">
// {/* Status Filter */}
// <FilterDropdown
// label="Status"
// options={[
// { value: 'active', label: 'Active' },
// { value: 'pending_verification', label: 'Pending Verification' },
// { value: 'inactive', label: 'Inactive' },
// { value: 'suspended', label: 'Suspended' },
// { value: 'deleted', label: 'Deleted' },
// ]}
// value={statusFilter}
// onChange={(value) => {
// setStatusFilter(value as string | null);
// setCurrentPage(1); // Reset to first page when filter changes
// }}
// placeholder="All"
// />
// {/* Sort Filter */}
// <FilterDropdown
// label="Sort by"
// options={[
// { value: ['first_name', 'asc'], label: 'First Name (A-Z)' },
// { value: ['first_name', 'desc'], label: 'First Name (Z-A)' },
// { value: ['last_name', 'asc'], label: 'Last Name (A-Z)' },
// { value: ['last_name', 'desc'], label: 'Last Name (Z-A)' },
// { value: ['email', 'asc'], label: 'Email (A-Z)' },
// { value: ['email', 'desc'], label: 'Email (Z-A)' },
// { value: ['created_at', 'asc'], label: 'Created (Oldest)' },
// { value: ['created_at', 'desc'], label: 'Created (Newest)' },
// { value: ['updated_at', 'asc'], label: 'Updated (Oldest)' },
// { value: ['updated_at', 'desc'], label: 'Updated (Newest)' },
// ]}
// value={orderBy}
// onChange={(value) => {
// setOrderBy(value as string[] | null);
// setCurrentPage(1); // Reset to first page when sort changes
// }}
// placeholder="Default"
// showIcon
// icon={<ArrowUpDown className="w-3.5 h-3.5 text-[#475569]" />}
// />
// </div>
// {/* Actions */}
// <div className="flex items-center gap-2">
// {/* Export Button */}
// <button
// type="button"
// className="flex items-center gap-1.5 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50 transition-colors cursor-pointer"
// >
// <Download className="w-3.5 h-3.5" />
// <span>Export</span>
// </button>
// {/* New User Button */}
// <PrimaryButton
// size="default"
// className="flex items-center gap-2"
// onClick={() => setIsModalOpen(true)}
// >
// <Plus className="w-3.5 h-3.5" />
// <span className="text-xs">New User</span>
// </PrimaryButton>
// </div>
// </div>
// {/* Data Table */}
// <DataTable
// data={users}
// columns={columns}
// keyExtractor={(user) => user.id}
// mobileCardRenderer={mobileCardRenderer}
// emptyMessage="No users found"
// isLoading={isLoading}
// error={error}
// />
// {/* Table Footer with Pagination */}
// {pagination.total > 0 && (
// <Pagination
// currentPage={currentPage}
// totalPages={pagination.totalPages}
// totalItems={pagination.total}
// limit={limit}
// onPageChange={(page: number) => {
// setCurrentPage(page);
// }}
// onLimitChange={(newLimit: number) => {
// setLimit(newLimit);
// setCurrentPage(1); // Reset to first page when limit changes
// }}
// />
// )}
// </div>
// {/* New User Modal */}
// <NewUserModal
// isOpen={isModalOpen}
// onClose={() => setIsModalOpen(false)}
// onSubmit={handleCreateUser}
// isLoading={isCreating}
// />
// {/* View User Modal */}
// <ViewUserModal
// isOpen={viewModalOpen}
// onClose={() => {
// setViewModalOpen(false);
// setSelectedUserId(null);
// }}
// userId={selectedUserId}
// onLoadUser={loadUser}
// />
// {/* Edit User Modal */}
// <EditUserModal
// isOpen={editModalOpen}
// onClose={() => {
// setEditModalOpen(false);
// setSelectedUserId(null);
// setSelectedUserName('');
// }}
// userId={selectedUserId}
// onLoadUser={loadUser}
// onSubmit={handleUpdateUser}
// isLoading={isUpdating}
// />
// {/* Delete Confirmation Modal */}
// <DeleteConfirmationModal
// isOpen={deleteModalOpen}
// onClose={() => {
// setDeleteModalOpen(false);
// setSelectedUserId(null);
// setSelectedUserName('');
// }}
// onConfirm={handleConfirmDelete}
// title="Delete User"
// message="Are you sure you want to delete this user"
// itemName={selectedUserName}
// isLoading={isDeleting}
// />
// </Layout>
// );
// };
// export default Users;
export default Users;

View File

@ -8,7 +8,7 @@ interface TenantProtectedRouteProps {
}
const TenantProtectedRoute = ({ children }: TenantProtectedRouteProps): ReactElement => {
const { isAuthenticated, roles } = useAppSelector((state) => state.auth);
const { isAuthenticated, roles, tenantId } = useAppSelector((state) => state.auth);
// Fetch and apply tenant theme
useTenantTheme();
@ -17,7 +17,7 @@ const TenantProtectedRoute = ({ children }: TenantProtectedRouteProps): ReactEle
return <Navigate to="/tenant/login" replace />;
}
// Check if user has super_admin role - if yes, redirect to super admin dashboard
// Check if user has super_admin role or is a platform user - if yes, redirect to super admin dashboard
// Handle both array and JSON string formats
let rolesArray: string[] = [];
if (Array.isArray(roles)) {
@ -30,9 +30,10 @@ const TenantProtectedRoute = ({ children }: TenantProtectedRouteProps): ReactEle
}
}
const hasSuperAdminRole = rolesArray.includes('super_admin');
const isPlatformUser = hasSuperAdminRole || tenantId === '00000000-0000-0000-0000-000000000001';
if (hasSuperAdminRole) {
// If super_admin, redirect to super admin dashboard
if (isPlatformUser) {
// If platform user or super_admin, redirect to super admin dashboard
return <Navigate to="/dashboard" replace />;
}

View File

@ -20,6 +20,8 @@ const NotificationTemplateMaster = lazy(() => import("@/pages/superadmin/Notific
const SmtpConfig = lazy(() => import("@/pages/superadmin/SmtpConfig"));
const FailedEmails = lazy(() => import("@/pages/superadmin/FailedEmails"));
const AIFallbackHistory = lazy(() => import("@/pages/superadmin/AIFallbackHistory"));
const PlatformUsers = lazy(() => import("@/pages/superadmin/Users"));
const PlatformRoles = lazy(() => import("@/pages/superadmin/Roles"));
// Loading fallback component
const RouteLoader = (): ReactElement => (
@ -50,6 +52,14 @@ export const superAdminRoutes: RouteConfig[] = [
path: "/dashboard",
element: <LazyRoute component={Dashboard} />,
},
{
path: "/platform-users",
element: <LazyRoute component={PlatformUsers} />,
},
{
path: "/platform-roles",
element: <LazyRoute component={PlatformRoles} />,
},
{
path: "/tenants",
element: <LazyRoute component={Tenants} />,

View File

@ -14,7 +14,8 @@ export const roleService = {
page: number = 1,
limit: number = 20,
orderBy?: string[] | null,
search?: string | null
search?: string | null,
scope?: string | null
): Promise<RolesResponse> => {
const params = new URLSearchParams();
params.append('page', String(page));
@ -22,6 +23,9 @@ export const roleService = {
if (search) {
params.append('search', search);
}
if (scope) {
params.append('scope', scope);
}
if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) {
params.append('orderBy[]', orderBy[0]);
params.append('orderBy[]', orderBy[1]);

View File

@ -16,7 +16,8 @@ const getAllUsers = async (
orderBy?: string[] | null,
tenantId?: string | null,
search?: string | null,
roleId?: string | null
roleId?: string | null,
scope?: string | null
): Promise<UsersResponse> => {
const params = new URLSearchParams();
params.append('page', String(page));
@ -33,6 +34,9 @@ const getAllUsers = async (
if (roleId) {
params.append('role_id', roleId);
}
if (scope) {
params.append('scope', scope);
}
if (orderBy && Array.isArray(orderBy) && orderBy.length === 2) {
// Send array as orderBy[]=field&orderBy[]=direction
params.append('orderBy[]', orderBy[0]);
@ -49,8 +53,9 @@ export const userService = {
status?: string | null,
orderBy?: string[] | null,
search?: string | null,
roleId?: string | null
) => getAllUsers(page, limit, status, orderBy, null, search, roleId),
roleId?: string | null,
scope?: string | null
) => getAllUsers(page, limit, status, orderBy, null, search, roleId, scope),
create: async (data: CreateUserRequest): Promise<CreateUserResponse> => {
const response = await apiClient.post<CreateUserResponse>('/users', data);

View File

@ -77,8 +77,8 @@ export interface CreateUserRequest {
role_id?: string;
role_ids?: string[];
role_module_combinations?: RoleModuleCombination[];
department_id?: string;
designation_id?: string;
department_id?: string | null;
designation_id?: string | null;
module_ids?: string[];
category?: 'tenant_user' | 'supplier_user';
supplier_id?: string | null;
@ -105,8 +105,8 @@ export interface UpdateUserRequest {
role_id?: string;
role_ids?: string[];
role_module_combinations?: RoleModuleCombination[];
department_id?: string;
designation_id?: string;
department_id?: string | null;
designation_id?: string | null;
module_ids?: string[];
category?: 'tenant_user' | 'supplier_user';
supplier_id?: string | null;