refactor: centralize platform resources and improve role-based access control with standardized parsing and updated permission hooks
This commit is contained in:
parent
ae72eebcea
commit
edd8fe8089
@ -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
|
||||
@ -110,7 +111,7 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps):
|
||||
>
|
||||
<Menu className="w-5 h-5 text-[#0f1724]" />
|
||||
</button>
|
||||
|
||||
|
||||
{/* Breadcrumbs */}
|
||||
<nav className="flex items-center gap-1.5 md:gap-2">
|
||||
{breadcrumbs && breadcrumbs.length > 0 ? (
|
||||
@ -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
|
||||
|
||||
@ -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 = ({
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
|
||||
1220
src/components/superadmin/PlatformRolesTable.tsx
Normal file
1220
src/components/superadmin/PlatformRolesTable.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1035
src/components/superadmin/PlatformUsersTable.tsx
Normal file
1035
src/components/superadmin/PlatformUsersTable.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -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}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
||||
34
src/constants/platformResources.ts
Normal file
34
src/constants/platformResources.ts
Normal 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 48–81).
|
||||
*
|
||||
* 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'];
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"> */}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
@ -211,7 +213,7 @@ const Tenants = (): ReactElement => {
|
||||
key: "name",
|
||||
label: "Tenant Name",
|
||||
render: (tenant) => (
|
||||
<div
|
||||
<div
|
||||
className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
||||
>
|
||||
@ -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>
|
||||
@ -308,7 +302,7 @@ const Tenants = (): ReactElement => {
|
||||
const mobileCardRenderer = (tenant: Tenant) => (
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between gap-3 mb-3">
|
||||
<div
|
||||
<div
|
||||
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => navigate(`/tenants/${tenant.id}`)}
|
||||
>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@ -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} />,
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user