From 93e38018dc75034e66c339771c70ee3fe7fee268 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Date: Fri, 27 Mar 2026 21:25:00 +0100 Subject: [PATCH] feat(admin): runtime-gated sidebar, wire roles/employees/departments/designations --- src/components/AdminShell.tsx | 113 ++++++++++++++++++++- src/components/AdminSidebar.tsx | 93 ++++++++++------- src/routes/admin/department.tsx | 23 ++++- src/routes/admin/designation.tsx | 34 ++++++- src/routes/admin/employees/index.tsx | 146 +++++++++++++++++++++++---- src/routes/admin/external-roles.tsx | 36 ++++++- src/routes/admin/index.tsx | 7 ++ src/routes/admin/roles/index.tsx | 63 ++++++++++-- 8 files changed, 440 insertions(+), 75 deletions(-) diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index b0aa055..c53380e 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -56,6 +56,47 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ ]; const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = []; +const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [ + { prefix: '/admin/department', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, + { prefix: '/admin/designation', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] }, + { prefix: '/admin/roles', keys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] }, + { prefix: '/admin/employees', keys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] }, + { prefix: '/admin/external-roles', keys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] }, + { prefix: '/admin/onboarding-management', keys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { prefix: '/admin/onboarding-schemas', keys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { prefix: '/admin/internal-dashboard-management', keys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS'] }, + { prefix: '/admin/external-dashboard-management', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] }, + { prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] }, + { prefix: '/admin/verification', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, + { prefix: '/admin/verification-status', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, + { prefix: '/admin/approval', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, + { prefix: '/admin/users', keys: ['USER_MANAGEMENT', 'USERS'] }, + { prefix: '/admin/company', keys: ['COMPANY_MANAGEMENT', 'COMPANIES'] }, + { prefix: '/admin/candidate', keys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] }, + { prefix: '/admin/customer', keys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] }, + { prefix: '/admin/photographer', keys: ['PHOTOGRAPHER_MANAGEMENT', 'PHOTOGRAPHERS'] }, + { prefix: '/admin/makeup-artist', keys: ['MAKEUP_ARTIST_MANAGEMENT', 'MAKEUP_ARTISTS'] }, + { prefix: '/admin/tutors', keys: ['TUTOR_MANAGEMENT', 'TUTORS'] }, + { prefix: '/admin/developers', keys: ['DEVELOPER_MANAGEMENT', 'DEVELOPERS'] }, + { prefix: '/admin/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] }, + { prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] }, + { prefix: '/admin/catering-services', keys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] }, + { prefix: '/admin/graphic-designers', keys: ['GRAPHIC_DESIGNER_MANAGEMENT', 'GRAPHIC_DESIGNERS'] }, + { prefix: '/admin/social-media-managers', keys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] }, + { prefix: '/admin/jobs', keys: ['JOBS_MANAGEMENT', 'JOBS'] }, + { prefix: '/admin/leads', keys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] }, + { prefix: '/admin/pricing', keys: ['PRICING_MANAGEMENT', 'PRICING'] }, + { prefix: '/admin/credit', keys: ['CREDIT_MANAGEMENT', 'CREDITS'] }, + { prefix: '/admin/coupon', keys: ['COUPON_MANAGEMENT', 'COUPONS'] }, + { prefix: '/admin/discount', keys: ['DISCOUNT_MANAGEMENT', 'DISCOUNTS'] }, + { prefix: '/admin/tax', keys: ['TAX_MANAGEMENT', 'TAXES'] }, + { prefix: '/admin/order', keys: ['ORDER_MANAGEMENT', 'ORDERS'] }, + { prefix: '/admin/invoice', keys: ['INVOICE_MANAGEMENT', 'INVOICES'] }, + { prefix: '/admin/review', keys: ['REVIEW_MANAGEMENT', 'REVIEWS'] }, + { prefix: '/admin/support', keys: ['SUPPORT_MANAGEMENT', 'SUPPORT'] }, + { prefix: '/admin/report', keys: ['REPORT_MANAGEMENT', 'REPORTS'] }, + { prefix: '/admin/ledger', keys: ['LEDGER', 'LEDGER_MANAGEMENT'] }, +]; const SEARCH_MODULES = [ { @@ -257,6 +298,8 @@ export default function AdminShell(props: { children: JSX.Element }) { const [checkedSession, setCheckedSession] = createSignal(false); const [adminName, setAdminName] = createSignal('Admin User'); + const [allowedModules, setAllowedModules] = createSignal(null); + const [isSuperAdmin, setIsSuperAdmin] = createSignal(false); const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); const [notifCount] = createSignal(0); @@ -318,7 +361,7 @@ export default function AdminShell(props: { children: JSX.Element }) { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; - const response = await fetch('/api/gateway/users/auth/me', { + const response = await fetch('/api/gateway/api/auth/session', { method: 'GET', headers: { Accept: 'application/json', @@ -330,6 +373,52 @@ export default function AdminShell(props: { children: JSX.Element }) { const payload = await response.json().catch(() => ({})); if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized'); if (payload?.full_name) setAdminName(payload.full_name); + + const roleKey = String( + payload?.active_role + || payload?.role + || payload?.user?.active_role + || payload?.user?.active_role_key + || payload?.user?.role + || payload?.user?.role_key + || '', + ).toUpperCase(); + setIsSuperAdmin(roleKey === 'SUPER_ADMIN'); + + try { + const res = await fetch('/api/gateway/api/runtime-config', { + method: 'GET', + headers: { + Accept: 'application/json', + 'x-portal-target': 'admin', + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + credentials: 'include', + }); + const runtime = await res.json().catch(() => ({})); + if (res.ok) { + const mods = ( + runtime?.enabled_modules + || runtime?.enabledModules + || runtime?.modules + || runtime?.config_json?.modules + || runtime?.configJson?.modules + || [] + ) as unknown; + if (Array.isArray(mods)) { + setAllowedModules(mods.map((m) => String(m || '').trim().toUpperCase()).filter(Boolean)); + } else { + setAllowedModules(null); + } + const activeRole = String(runtime?.active_role || runtime?.user?.active_role || roleKey || '').toUpperCase(); + if (activeRole) setIsSuperAdmin(activeRole === 'SUPER_ADMIN'); + } else { + setAllowedModules(null); + } + } catch { + setAllowedModules(null); + } + setCheckedSession(true); } catch { clearAdminSession(); @@ -358,6 +447,26 @@ export default function AdminShell(props: { children: JSX.Element }) { return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); }); + createEffect(() => { + if (!checkedSession()) return; + if (isSuperAdmin()) return; + + const modules = allowedModules(); + if (!modules || modules.length === 0) return; + + const path = location.pathname; + if (path === '/admin') return; + + const guard = ROUTE_MODULE_KEYS.find((entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`)); + if (!guard) return; + + const allowed = new Set(modules.map((m) => String(m || '').trim().toUpperCase()).filter(Boolean)); + const ok = guard.keys.some((k) => allowed.has(String(k).toUpperCase())); + if (ok) return; + + navigate(`/admin?denied=${encodeURIComponent(guard.keys[0])}&from=${encodeURIComponent(path)}`, { replace: true }); + }); + return (
setSidebarOpen(false)} adminName={adminName()} adminInitials={adminInitials()} + allowedModules={allowedModules()} + isSuperAdmin={isSuperAdmin()} />
diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index e61e24a..2a20da2 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -1,5 +1,5 @@ import { A, useLocation } from '@solidjs/router'; -import { For, Show } from 'solid-js'; +import { For, Show, createMemo } from 'solid-js'; import { LayoutGrid, Building2, Briefcase, Users, ShieldCheck, FileText, LayoutDashboard, ClipboardList, UserRoundSearch, UserCircle, @@ -15,63 +15,64 @@ type NavItem = { label: string; icon: any; aliasPrefix?: string; + moduleKeys?: string[]; }; const GROUPS: NavItem[][] = [ [ - { href: '/admin', label: 'Dashboard', icon: LayoutGrid }, + { href: '/admin', label: 'Dashboard', icon: LayoutGrid, moduleKeys: ['ADMIN_DASHBOARD'] }, ], [ - { href: '/admin/department', label: 'Department Management', icon: Building2 }, - { href: '/admin/designation', label: 'Designation Management', icon: Briefcase }, - { href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck }, - { href: '/admin/employees', label: 'Employee Management', icon: Users }, + { href: '/admin/department', label: 'Department Management', icon: Building2, moduleKeys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, + { href: '/admin/designation', label: 'Designation Management', icon: Briefcase, moduleKeys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] }, + { href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck, moduleKeys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] }, + { href: '/admin/employees', label: 'Employee Management', icon: Users, moduleKeys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] }, ], [ - { href: '/admin/external-roles', label: 'External Role Management', icon: ShieldCheck }, - { href: '/admin/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas' }, - { href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard }, - { href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs' }, + { href: '/admin/external-roles', label: 'External Role Management', icon: ShieldCheck, moduleKeys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] }, + { href: '/admin/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas', moduleKeys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard, moduleKeys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS'] }, + { href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs', moduleKeys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] }, ], [ - { href: '/admin/verification', label: 'Verification Management', icon: BadgeCheck }, - { href: '/admin/approval', label: 'Approval Management', icon: ClipboardList }, + { href: '/admin/verification', label: 'Verification Management', icon: BadgeCheck, moduleKeys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, + { href: '/admin/approval', label: 'Approval Management', icon: ClipboardList, moduleKeys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, ], [ - { href: '/admin/users', label: 'Users Management', icon: UserRoundSearch }, - { href: '/admin/company', label: 'Company Management', icon: Building2 }, - { href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle }, - { href: '/admin/customer', label: 'Customer Management', icon: UserCircle }, + { href: '/admin/users', label: 'Users Management', icon: UserRoundSearch, moduleKeys: ['USER_MANAGEMENT', 'USERS'] }, + { href: '/admin/company', label: 'Company Management', icon: Building2, moduleKeys: ['COMPANY_MANAGEMENT', 'COMPANIES'] }, + { href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle, moduleKeys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] }, + { href: '/admin/customer', label: 'Customer Management', icon: UserCircle, moduleKeys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] }, ], [ - { href: '/admin/photographer', label: 'Photographer Management', icon: Camera }, - { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette }, - { href: '/admin/tutors', label: 'Tutors Management', icon: BookOpen }, - { href: '/admin/developers', label: 'Developers Management', icon: Code2 }, - { href: '/admin/video-editors', label: 'Video Editor Management', icon: Film }, - { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity }, - { href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils }, - { href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: PenTool }, - { href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: Megaphone }, + { href: '/admin/photographer', label: 'Photographer Management', icon: Camera, moduleKeys: ['PHOTOGRAPHER_MANAGEMENT', 'PHOTOGRAPHERS'] }, + { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette, moduleKeys: ['MAKEUP_ARTIST_MANAGEMENT', 'MAKEUP_ARTISTS'] }, + { href: '/admin/tutors', label: 'Tutors Management', icon: BookOpen, moduleKeys: ['TUTOR_MANAGEMENT', 'TUTORS'] }, + { href: '/admin/developers', label: 'Developers Management', icon: Code2, moduleKeys: ['DEVELOPER_MANAGEMENT', 'DEVELOPERS'] }, + { href: '/admin/video-editors', label: 'Video Editor Management', icon: Film, moduleKeys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] }, + { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity, moduleKeys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] }, + { href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils, moduleKeys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] }, + { href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: PenTool, moduleKeys: ['GRAPHIC_DESIGNER_MANAGEMENT', 'GRAPHIC_DESIGNERS'] }, + { href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: Megaphone, moduleKeys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] }, ], [ - { href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness }, - { href: '/admin/leads', label: 'Leads Management', icon: HandHelping }, + { href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness, moduleKeys: ['JOBS_MANAGEMENT', 'JOBS'] }, + { href: '/admin/leads', label: 'Leads Management', icon: HandHelping, moduleKeys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] }, ], [ - { href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards }, - { href: '/admin/credit', label: 'Credit Management', icon: CreditCard }, - { href: '/admin/coupon', label: 'Coupon Management', icon: Tag }, - { href: '/admin/discount', label: 'Discount Management', icon: Percent }, - { href: '/admin/tax', label: 'Tax Management', icon: Receipt }, - { href: '/admin/order', label: 'Order Management', icon: ShoppingCart }, - { href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck }, + { href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, moduleKeys: ['PRICING_MANAGEMENT', 'PRICING'] }, + { href: '/admin/credit', label: 'Credit Management', icon: CreditCard, moduleKeys: ['CREDIT_MANAGEMENT', 'CREDITS'] }, + { href: '/admin/coupon', label: 'Coupon Management', icon: Tag, moduleKeys: ['COUPON_MANAGEMENT', 'COUPONS'] }, + { href: '/admin/discount', label: 'Discount Management', icon: Percent, moduleKeys: ['DISCOUNT_MANAGEMENT', 'DISCOUNTS'] }, + { href: '/admin/tax', label: 'Tax Management', icon: Receipt, moduleKeys: ['TAX_MANAGEMENT', 'TAXES'] }, + { href: '/admin/order', label: 'Order Management', icon: ShoppingCart, moduleKeys: ['ORDER_MANAGEMENT', 'ORDERS'] }, + { href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck, moduleKeys: ['INVOICE_MANAGEMENT', 'INVOICES'] }, ], [ - { href: '/admin/review', label: 'Review Management', icon: Star }, - { href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon }, - { href: '/admin/report', label: 'Report Management', icon: BarChart3 }, - { href: '/admin/ledger', label: 'Ledger Management', icon: Receipt }, + { href: '/admin/review', label: 'Review Management', icon: Star, moduleKeys: ['REVIEW_MANAGEMENT', 'REVIEWS'] }, + { href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon, moduleKeys: ['SUPPORT_MANAGEMENT', 'SUPPORT'] }, + { href: '/admin/report', label: 'Report Management', icon: BarChart3, moduleKeys: ['REPORT_MANAGEMENT', 'REPORTS'] }, + { href: '/admin/ledger', label: 'Ledger Management', icon: Receipt, moduleKeys: ['LEDGER', 'LEDGER_MANAGEMENT'] }, ], ]; @@ -81,9 +82,23 @@ export default function AdminSidebar(props: { onNavigate?: () => void; adminName: string; adminInitials: string; + allowedModules?: string[] | null; + isSuperAdmin?: boolean; }) { const location = useLocation(); + const allowed = createMemo(() => new Set((props.allowedModules || []).map((m) => String(m || '').trim().toUpperCase()).filter(Boolean))); + const canShowItem = (item: NavItem) => { + if (props.isSuperAdmin) return true; + if (!props.allowedModules || props.allowedModules.length === 0) return true; + if (item.href === '/admin') return true; + const keys = item.moduleKeys || []; + for (const k of keys) if (allowed().has(String(k).toUpperCase())) return true; + return false; + }; + + const visibleGroups = createMemo(() => GROUPS.map((group) => group.filter((item) => canShowItem(item))).filter((group) => group.length > 0)); + const isActive = (item: NavItem) => { if (location.pathname === '/admin') return item.href === '/admin'; if (item.href === '/admin') return false; @@ -129,7 +144,7 @@ export default function AdminSidebar(props: { {/* Navigation */}