import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router'; import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX, } from 'solid-js'; import { Bell, Moon, Search, Settings, Sun, User } from 'lucide-solid'; import AdminSidebar from './AdminSidebar'; import { isExternalIdentity } from '~/lib/admin-auth'; import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; import { normalizeAllowedModules } from '~/lib/admin/module-access'; type Tab = { href: string; label: string; exact?: boolean }; type SearchResult = { id: string; title: string; subtitle: string; href: string }; type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] }; const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ { prefix: '/admin', label: 'Dashboard', exact: true }, { prefix: '/admin/department', label: 'Department Management' }, { prefix: '/admin/designation', label: 'Designation Management' }, { prefix: '/admin/roles', label: 'Internal Role Management' }, { prefix: '/admin/employees', label: 'Employee Management' }, { prefix: '/admin/external-roles', label: 'External Role Management' }, { prefix: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management' }, { prefix: '/admin/external-dashboard-management', label: 'External Dashboard Management' }, { prefix: '/admin/role-ui-configs', label: 'External Dashboard Management' }, { prefix: '/admin/verification', label: 'Verification Management' }, { prefix: '/admin/verification-status', label: 'Verification Management' }, { prefix: '/admin/approval', label: 'Approval Management' }, { prefix: '/admin/approvals', label: 'Approval Management' }, { prefix: '/admin/approval-management', label: 'Approval Management' }, { prefix: '/admin/users', label: 'Users Management' }, { prefix: '/admin/company', label: 'Company Management' }, { prefix: '/admin/candidate', label: 'Candidate Management' }, { prefix: '/admin/customer', label: 'Customer Management' }, { prefix: '/admin/photographer', label: 'Photographer Management' }, { prefix: '/admin/makeup-artist', label: 'Makeup Artist Management' }, { prefix: '/admin/tutors', label: 'Tutors Management' }, { prefix: '/admin/developers', label: 'Developers Management' }, { prefix: '/admin/video-editors', label: 'Video Editor Management' }, { prefix: '/admin/fitness-trainers', label: 'Fitness Trainer Management' }, { prefix: '/admin/catering-services', label: 'Catering Services Management' }, { prefix: '/admin/ugc-content-creators', label: 'UGC Content Creator Management' }, { prefix: '/admin/graphic-designers', label: 'Graphic Designer Management' }, { prefix: '/admin/social-media-managers', label: 'Social Media Manager Management' }, { prefix: '/admin/jobs', label: 'Jobs Management' }, { prefix: '/admin/leads', label: 'Leads Management' }, { prefix: '/admin/applications', label: 'Applications Management' }, { prefix: '/admin/responses', label: 'Responses Management' }, { prefix: '/admin/pricing', label: 'Pricing Management' }, { prefix: '/admin/credit', label: 'Credit Management' }, { prefix: '/admin/coupon', label: 'Coupon Management' }, { prefix: '/admin/discount', label: 'Discount Management' }, { prefix: '/admin/tax', label: 'Tax Management' }, { prefix: '/admin/order', label: 'Order Management' }, { prefix: '/admin/invoice', label: 'Invoice Management' }, { prefix: '/admin/payment-gateway', label: 'Payment Gateway Management' }, { prefix: '/admin/smtp', label: 'SMTP Management' }, { prefix: '/admin/kb', label: 'Knowledge Base Management' }, { prefix: '/admin/notifications', label: 'Notifications' }, { prefix: '/admin/review', label: 'Review Management' }, { prefix: '/admin/support', label: 'Support Management' }, { prefix: '/admin/report', label: 'Report Management' }, { prefix: '/admin/ledger', label: 'Ledger Management' }, ]; const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = []; const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [ { prefix: '/admin', keys: ['ADMIN_DASHBOARD', 'DASHBOARD'] }, { prefix: '/admin/department', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, { prefix: '/admin/department-management', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, { prefix: '/admin/designation', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] }, { prefix: '/admin/designation-management', 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/internal-dashboard-management', keys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS', 'INTERNAL_DASHBOARD_CONFIG'] }, { prefix: '/admin/external-dashboard-management', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] }, { prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', '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/approvals', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, { prefix: '/admin/approval-management', 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/ugc-content-creator', keys: ['UGC_CONTENT_CREATOR_MANAGEMENT', 'UGC_CONTENT_CREATOR'] }, { 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/applications', keys: ['APPLICATIONS_MANAGEMENT', 'APPLICATIONS'] }, { prefix: '/admin/responses', keys: ['RESPONSES_MANAGEMENT', 'RESPONSES'] }, { 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/payment-gateway', keys: ['PAYMENT_GATEWAY_MANAGEMENT', 'PAYMENT_GATEWAY'] }, { prefix: '/admin/smtp', keys: ['SMTP_MANAGEMENT', 'SMTP'] }, { prefix: '/admin/kb', keys: ['KNOWLEDGE_BASE_MANAGEMENT', 'KNOWLEDGE_BASE', 'KB'] }, { prefix: '/admin/notifications', keys: ['NOTIFICATIONS_MANAGEMENT', 'NOTIFICATIONS'] }, { 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 = [ { label: 'Users', viewAllHref: '/admin/users', api: '/api/admin/users', listKeys: ['users', 'items'], titleKeys: ['full_name', 'name'], subtitleKeys: ['email', 'phone'], detailBase: '/admin/users', }, { label: 'Companies', viewAllHref: '/admin/company', api: '/api/admin/companies', listKeys: ['companies', 'items'], titleKeys: ['name', 'companyName'], subtitleKeys: ['email', 'phone'], detailBase: '/admin/company', }, { label: 'Employees', viewAllHref: '/admin/employees', api: '/api/admin/employees', listKeys: ['employees', 'items'], titleKeys: ['full_name', 'name'], subtitleKeys: ['email', 'department_name'], detailBase: '/admin/employees', }, { label: 'Jobs', viewAllHref: '/admin/jobs', api: '/api/admin/jobs', listKeys: ['jobs', 'items'], titleKeys: ['title', 'name'], subtitleKeys: ['status', 'company_name'], detailBase: '/admin/jobs', }, { label: 'Leads', viewAllHref: '/admin/leads', api: '/api/admin/leads', listKeys: ['leads', 'items'], titleKeys: ['name', 'full_name'], subtitleKeys: ['email', 'status'], detailBase: '/admin/leads', }, ]; function pickStr(obj: Record, keys: string[]): string { for (const k of keys) if (obj[k]) return String(obj[k]); return '—'; } function extractList(data: any, keys: string[]): any[] { if (Array.isArray(data)) return data; for (const k of keys) if (Array.isArray(data[k])) return data[k]; return []; } function GlobalSearch() { const [query, setQuery] = createSignal(''); const [open, setOpen] = createSignal(false); const [groups, setGroups] = createSignal([]); const [searching, setSearching] = createSignal(false); let wrapRef!: HTMLDivElement; let timer: ReturnType; const doSearch = async (q: string) => { const trimmed = q.trim(); if (trimmed.length < 2) { setGroups([]); setOpen(false); return; } setSearching(true); const settled = await Promise.allSettled( SEARCH_MODULES.map(async (mod) => { const res = await fetch(`${mod.api}?search=${encodeURIComponent(trimmed)}&limit=4`).catch(() => null); if (!res?.ok) return null; const data = await res.json().catch(() => null); if (!data) return null; const items = extractList(data, mod.listKeys).slice(0, 4); if (!items.length) return null; return { label: mod.label, viewAllHref: mod.viewAllHref, results: items.map((item: any) => ({ id: item.id, title: pickStr(item, mod.titleKeys), subtitle: pickStr(item, mod.subtitleKeys), href: `${mod.detailBase}/${item.id}`, })), } satisfies SearchGroup; }), ); setGroups(settled.flatMap((r) => (r.status === 'fulfilled' && r.value ? [r.value] : []))); setOpen(true); setSearching(false); }; const handleInput = (val: string) => { setQuery(val); clearTimeout(timer); if (val.trim().length < 2) { setGroups([]); setOpen(false); return; } timer = setTimeout(() => doSearch(val), 350); }; const close = () => { setOpen(false); setQuery(''); setGroups([]); }; const onOutside = (e: MouseEvent) => { if (!wrapRef.contains(e.target as Node)) setOpen(false); }; onMount(() => document.addEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener('mousedown', onOutside)); return (
handleInput(e.currentTarget.value)} onFocus={() => groups().length > 0 && setOpen(true)} onKeyDown={(e) => e.key === 'Escape' && close()} class="h-[68px] w-full rounded-[24px] border-2 border-transparent bg-[#f4f5f8] pl-[60px] pr-6 text-[16px] text-[#0D0D2A] placeholder:text-[rgba(13,13,42,0.4)] outline-none transition-all focus:border-[#e5e7eb] focus:bg-white" /> 0}> = 2 && groups().length === 0}>
No results for "{query()}"
); } function ShowTabs(props: { tabs: Tab[]; isTabActive: (tab: Tab) => boolean; setTabsTrackEl: (el: HTMLDivElement) => void; setTabRefs: (fn: (prev: Record) => Record) => void; tabIndicator: () => { left: number; width: number; ready: boolean }; }) { if (props.tabs.length === 0) return null; return (
{(tab) => ( props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))} aria-current={props.isTabActive(tab) ? 'page' : undefined} class={`px-4 pb-3 pt-3 text-[14px] font-semibold transition-colors ${ props.isTabActive(tab) ? 'text-[#FF5E13]' : 'text-[rgba(13,13,42,0.6)] hover:text-[#0D0D2A]' }`} > {tab.label} )}
); } export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); 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 [unreadCount, setUnreadCount] = createSignal(0); const [theme, setTheme] = createSignal<'light' | 'dark'>('light'); const [routeTransitioning, setRouteTransitioning] = createSignal(false); const [tabsTrackEl, setTabsTrackEl] = createSignal(); const [tabRefs, setTabRefs] = createSignal>({}); const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false }); let contentScrollRef: HTMLDivElement | undefined; const logout = async () => { try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; await fetch('/api/auth/logout', { method: 'POST', headers: { Accept: 'application/json', 'x-portal-target': 'admin', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }).catch(() => null); } finally { if (typeof sessionStorage !== 'undefined') { sessionStorage.removeItem('nxtgauge_admin_access_token'); sessionStorage.removeItem('nxtgauge_admin_preview'); } clearAdminSession(); navigate('/login', { replace: true }); } }; const tabs = createMemo(() => { const path = location.pathname; for (const set of TAB_SETS) { if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return set.tabs; } return []; }); const isTabActive = (tab: Tab) => tab.exact ? location.pathname === tab.href : location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); const refreshTabIndicator = () => { const activeTab = tabs().find((tab) => isTabActive(tab)); const track = tabsTrackEl(); if (!activeTab || !track) { setTabIndicator((p) => ({ ...p, ready: false })); return; } const el = tabRefs()[activeTab.href]; if (!el) return; setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true }); }; createEffect(() => { tabs(); location.pathname; requestAnimationFrame(refreshTabIndicator); }); createEffect(() => { location.pathname; setRouteTransitioning(true); requestAnimationFrame(() => { requestAnimationFrame(() => setRouteTransitioning(false)); }); if (!contentScrollRef) return; const prefersReducedMotion = typeof window !== 'undefined' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false; contentScrollRef.scrollTo({ top: 0, behavior: prefersReducedMotion ? 'auto' : 'smooth', }); }); onMount(() => { const savedTheme = (typeof localStorage !== 'undefined' ? localStorage.getItem('nxtgauge_admin_theme') : null) as 'light' | 'dark' | null; const nextTheme = savedTheme === 'dark' ? 'dark' : 'light'; setTheme(nextTheme); if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-theme', nextTheme); } window.addEventListener('resize', refreshTabIndicator); onCleanup(() => window.removeEventListener('resize', refreshTabIndicator)); // Fetch unread notification count and poll every 30 seconds const fetchUnreadCount = async () => { try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; if (!accessToken) return; const res = await fetch('/api/me/notifications/unread-count', { method: 'GET', headers: { Accept: 'application/json', 'x-portal-target': 'admin', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); if (res.ok) { const data = await res.json(); setUnreadCount(data.unread_count || 0); } } catch (e) { console.error('Failed to fetch unread count:', e); } }; fetchUnreadCount(); const interval = setInterval(fetchUnreadCount, 30000); onCleanup(() => clearInterval(interval)); const isPreview = searchParams._preview === '1' || (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); if (isPreview) { if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); setAdminSession(); setCheckedSession(true); return; } const verify = async () => { if (!hasAdminSession()) { navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); return; } try { const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const response = await fetch('/api/auth/session', { method: 'GET', headers: { Accept: 'application/json', 'x-portal-target': 'admin', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), }, credentials: 'include', }); 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/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) { setAllowedModules(normalizeAllowedModules(runtime)); 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(); navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); } }; void verify(); }); const pageTitle = createMemo(() => { const path = location.pathname; for (const entry of PAGE_TITLES) { if (entry.exact ? path === entry.prefix : (path === entry.prefix || path.startsWith(`${entry.prefix}/`))) { return entry.label; } } return 'Admin'; }); const adminInitials = createMemo(() => { if (adminName().trim().toLowerCase() === 'admin user') return 'AD'; const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); if (parts.length === 0) return 'U'; if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); }); createEffect(() => { const t = theme(); if (typeof localStorage !== 'undefined') localStorage.setItem('nxtgauge_admin_theme', t); if (typeof document !== 'undefined') document.documentElement.setAttribute('data-theme', t); }); const toggleTheme = () => setTheme((v) => (v === 'dark' ? 'light' : 'dark')); const isDark = () => theme() === 'dark'; 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 matches = ROUTE_MODULE_KEYS.filter( (entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`), ); const guard = matches.sort((a, b) => b.prefix.length - a.prefix.length)[0]; 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 (
Checking session…
} >
setSidebarOpen(false)} />
setSidebarCollapsed((v) => !v)} onNavigate={() => setSidebarOpen(false)} adminName={adminName()} adminInitials={adminInitials()} theme={theme()} allowedModules={allowedModules()} isSuperAdmin={isSuperAdmin()} />
{ contentScrollRef = el; }} class="min-h-0 flex-1 overflow-y-scroll" style={{ background: isDark() ? '#0B1220' : '#F9FAFB', 'scrollbar-gutter': 'stable' }} >
{props.children}
); }