diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index 0b09af3..b082db1 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -1,5 +1,5 @@ import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router'; -import { createMemo, createSignal, onMount, type JSX } from 'solid-js'; +import { For, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX } from 'solid-js'; import AdminSidebar from './AdminSidebar'; import { isExternalIdentity } from '~/lib/admin-auth'; import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; @@ -10,88 +10,57 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ { prefixes: ['/admin/roles'], tabs: [ - { href: '/admin/roles', label: 'Internal Roles', exact: true }, + { href: '/admin/roles', label: 'Roles', exact: true }, { href: '/admin/roles/create', label: 'Create Role' }, - { href: '/admin/roles/templates', label: 'Role Templates' }, + { href: '/admin/roles/templates', label: 'View Roles' }, ], }, { prefixes: ['/admin/runtime-roles'], tabs: [ - { href: '/admin/runtime-roles', label: 'External Roles', exact: true }, - { href: '/admin/runtime-roles/new', label: 'Create External Role' }, - ], - }, - { - prefixes: ['/admin/onboarding-schemas'], - tabs: [ - { href: '/admin/onboarding-schemas', label: 'Onboarding Flows', exact: true }, - { href: '/admin/onboarding-schemas/new', label: 'Create Flow' }, - ], - }, - { - prefixes: ['/admin/internal-dashboard-management'], - tabs: [ - { href: '/admin/internal-dashboard-management', label: 'Internal Dashboards' }, - ], - }, - { - prefixes: ['/admin/external-dashboard-management'], - tabs: [ - { href: '/admin/external-dashboard-management', label: 'External Dashboards' }, - ], - }, - { - prefixes: ['/admin/role-ui-configs'], - tabs: [ - { href: '/admin/role-ui-configs', label: 'Config Inspector', exact: true }, - { href: '/admin/role-ui-configs/new', label: 'Create Config' }, + { href: '/admin/runtime-roles', label: 'Roles', exact: true }, + { href: '/admin/runtime-roles/new', label: 'Create Role' }, + { href: '/admin/role-ui-configs', label: 'View Roles' }, ], }, ]; -const PAGE_TITLES: Array<{ prefix: string; title: string }> = [ - { prefix: '/admin/employees', title: 'Employee Management' }, - { prefix: '/admin/department', title: 'Department Management' }, - { prefix: '/admin/designation', title: 'Designation Management' }, - { prefix: '/admin/roles', title: 'Internal Role Management' }, - { prefix: '/admin/runtime-roles', title: 'External Role Management' }, - { prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' }, - { prefix: '/admin/internal-dashboard-management', title: 'Internal Dashboard Management' }, - { prefix: '/admin/external-dashboard-management', title: 'External Dashboard Management' }, - { prefix: '/admin/role-ui-configs', title: 'External Dashboard Management' }, - { prefix: '/admin/approval', title: 'Approval Management' }, - { prefix: '/admin/users', title: 'Users Management' }, - { prefix: '/admin/company', title: 'Company Management' }, - { prefix: '/admin/candidate', title: 'Candidate Management' }, - { prefix: '/admin/customer', title: 'Customer Management' }, - { prefix: '/admin/photographer', title: 'Photographer Management' }, - { prefix: '/admin/makeup-artist', title: 'Makeup Artist Management' }, - { prefix: '/admin/tutors', title: 'Tutors Management' }, - { prefix: '/admin/developers', title: 'Developers Management' }, - { prefix: '/admin/video-editors', title: 'Video Editor Management' }, - { prefix: '/admin/fitness-trainers', title: 'Fitness Trainer Management' }, - { prefix: '/admin/catering-services', title: 'Catering Services Management' }, - { prefix: '/admin/graphic-designers', title: 'Graphics Designer Management' }, - { prefix: '/admin/social-media-managers', title: 'Social Media Manager Management' }, - { prefix: '/admin/jobs', title: 'Jobs Management' }, - { prefix: '/admin/leads', title: 'Leads Management' }, - { prefix: '/admin/pricing', title: 'Pricing Management' }, - { prefix: '/admin/credit', title: 'Credit Management' }, - { prefix: '/admin/coupon', title: 'Coupon Management' }, - { prefix: '/admin/discount', title: 'Discount Management' }, - { prefix: '/admin/tax', title: 'Tax Management' }, - { prefix: '/admin/order', title: 'Order Management' }, - { prefix: '/admin/invoice', title: 'Invoice Management' }, - { prefix: '/admin/review', title: 'Review Management' }, - { prefix: '/admin/support', title: 'Support Management' }, - { prefix: '/admin/kb', title: 'Knowledge Base Management' }, - { prefix: '/admin/notifications', title: 'Notifications' }, - { prefix: '/admin/report', title: 'Report Management' }, - { prefix: '/admin/ledger', title: 'Ledger Management' }, - { prefix: '/admin/workspace', title: 'Dashboard Workspace' }, - { prefix: '/admin', title: 'Dashboard' }, -]; +function IconBell() { + return ( + + + + + ); +} + +function IconSearch() { + return ( + + + + + ); +} + +function IconHelp() { + return ( + + + + + + ); +} + +function IconCog() { + return ( + + + + + ); +} export default function AdminShell(props: { children: JSX.Element }) { const location = useLocation(); @@ -99,35 +68,46 @@ export default function AdminShell(props: { children: JSX.Element }) { const [searchParams] = useSearchParams(); const [checkedSession, setCheckedSession] = createSignal(false); const [adminName, setAdminName] = createSignal('Admin'); - const [adminRole, setAdminRole] = createSignal('Super Admin'); + const [sidebarOpen, setSidebarOpen] = createSignal(false); + const [tabsTrackEl, setTabsTrackEl] = createSignal(); + const [tabRefs, setTabRefs] = createSignal>({}); + const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false }); 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; - } + if (set.prefixes.some((p) => path === p || path.startsWith(`${p}/`))) return set.tabs; } return []; }); - const isTabActive = (tab: Tab) => { - if (tab.exact) return location.pathname === tab.href; - return location.pathname === tab.href || location.pathname.startsWith(`${tab.href}/`); + 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((prev) => ({ ...prev, ready: false })); + return; + } + const el = tabRefs()[activeTab.href]; + if (!el) return; + setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true }); }; - const pageTitle = createMemo(() => { - const path = location.pathname; - for (const item of PAGE_TITLES) { - if (path === item.prefix || path.startsWith(`${item.prefix}/`)) return item.title; - } - return 'Dashboard'; + createEffect(() => { + tabs(); + location.pathname; + requestAnimationFrame(refreshTabIndicator); }); onMount(() => { + const onResize = () => refreshTabIndicator(); + window.addEventListener('resize', onResize); + onCleanup(() => window.removeEventListener('resize', onResize)); + const isLocalDev = - typeof window !== 'undefined' && - (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); + typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'); const isPreview = searchParams._preview === '1' || (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); @@ -146,10 +126,7 @@ export default function AdminShell(props: { children: JSX.Element }) { return; } try { - const accessToken = - typeof sessionStorage !== 'undefined' - ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' - : ''; + const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const response = await fetch('/api/gateway/users/auth/me', { method: 'GET', headers: { @@ -162,7 +139,6 @@ 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); - if (payload?.role?.name) setAdminRole(payload.role.name); setCheckedSession(true); } catch { clearAdminSession(); @@ -188,110 +164,126 @@ export default function AdminShell(props: { children: JSX.Element }) { navigate('/login', { replace: true }); }; - const initials = () => adminName().charAt(0).toUpperCase() || 'A'; + const adminInitials = createMemo(() => { + const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); + if (parts.length === 0) return 'AD'; + if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); + return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase(); + }); return ( -
- {/* ── Fixed Header ── */} -
- {/* Left: logo */} -
- NXTGAUGE -
- - {/* Center: search */} -
- - - - -
- - {/* Right: actions + avatar */} -
- {/* Notification bell */} - - - {/* Settings */} - - -
- - {/* Name + avatar */} -
- {adminName()} -
- {initials()} -
+
+
+
+
+ + + NXTGAUGE +
- {/* Logout */} - +
+ +
- {/* ── Body: sidebar + main (fixed, below header) ── */} {checkedSession() ? ( -
- {/* Sidebar */} - +
+
setSidebarOpen(false)} + /> - {/* Main content */} -
- {/* Sub-tabs (shown for multi-tab sections) */} - {tabs().length > 0 && ( -
- {tabs().map((tab) => ( - - {tab.label} - - ))} -
- )} +
+ setSidebarOpen(false)} onLogout={onLogout} /> +
+ +
+ {props.children}
) : ( - /* Session check loading state */ -
-
-
-

Checking session…

+
+ )}
); } + +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={`relative rounded-t-xl px-6 pb-4 pt-4 text-[16px] font-semibold transition-colors ${ + props.isTabActive(tab) ? 'bg-white text-[#111827]' : 'text-[#636b7f] hover:text-[#111827]' + }`} + > + {tab.label} + + )} + +
+
+ ); +} diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index 5ce209b..6be0e6a 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -1,144 +1,122 @@ import { A, useLocation } from '@solidjs/router'; +import { For, Show } from 'solid-js'; +import { + Bell, + Briefcase, + ClipboardList, + FileText, + FolderCog, + HandHelping, + LayoutGrid, + Percent, + Receipt, + Sparkles, + UserCircle2, + Users, + WalletCards, +} from 'lucide-solid'; -type LinkItem = { +type Item = { href: string; label: string; - icon: string; + iconPath?: string; + icon?: any; aliasPrefix?: string; - group: string; + separatorBefore?: boolean; }; -const links: LinkItem[] = [ - { href: '/admin', label: 'Dashboard', icon: 'dashboard.svg', group: '__top__' }, - - { href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg', group: 'Management' }, - { href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg', group: 'Management' }, - { href: '/admin/onboarding-schemas', label: 'External Onboarding', icon: 'reviews.svg', group: 'Management' }, - { href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Mgmt.', icon: 'dashboard.svg', group: 'Management' }, - { href: '/admin/external-dashboard-management', label: 'External Dashboard Mgmt.', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs', group: 'Management' }, - { href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg', group: 'Management' }, - { href: '/admin/department', label: 'Department Management', icon: 'department.svg', group: 'Management' }, - { href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg', group: 'Management' }, - { href: '/admin/employees', label: 'Employee Management', icon: 'users.svg', group: 'Management' }, - { href: '/admin/users', label: 'Users Management', icon: 'users.svg', group: 'Management' }, - { href: '/admin/company', label: 'Company Management', icon: 'company.svg', group: 'Management' }, - { href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg', group: 'Management' }, - { href: '/admin/customer', label: 'Customer Management', icon: 'users.svg', group: 'Management' }, - { href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg', group: 'Management' }, - { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg',group: 'Management' }, - { href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg', group: 'Management' }, - { href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg', group: 'Management' }, - { href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg', group: 'Management' }, - { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg', group: 'Management' }, - { href: '/admin/catering-services', label: 'Catering Services', icon: 'company.svg', group: 'Management' }, - { href: '/admin/graphic-designers', label: 'Graphics Designer Mgmt.', icon: 'developers.svg', group: 'Management' }, - { href: '/admin/social-media-managers', label: 'Social Media Mgr.', icon: 'developers.svg', group: 'Management' }, - { href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg', group: 'Management' }, - { href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg', group: 'Management' }, - - { href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg', group: 'Finance' }, - { href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg', group: 'Finance' }, - { href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg', group: 'Finance' }, - { href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg', group: 'Finance' }, - { href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg', group: 'Finance' }, - { href: '/admin/order', label: 'Order Management', icon: 'order.svg', group: 'Finance' }, - { href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg', group: 'Finance' }, - { href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg', group: 'Finance' }, - - { href: '/admin/review', label: 'Review Management', icon: 'reviews.svg', group: 'Platform' }, - { href: '/admin/support', label: 'Support Management', icon: 'support.svg', group: 'Platform' }, - { href: '/admin/kb', label: 'Knowledge Base', icon: 'reviews.svg', group: 'Platform' }, - { href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg', group: 'Platform' }, - { href: '/admin/report', label: 'Report Management', icon: 'report.svg', group: 'Platform' }, +const items: Item[] = [ + { href: '/admin', label: 'Dashboard', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid }, + { href: '/admin/roles', label: 'Internal Role Management', iconPath: '/sidebar-icons/role.svg', icon: FolderCog }, + { href: '/admin/runtime-roles', label: 'External Role Management', iconPath: '/sidebar-icons/users.svg', icon: Users }, + { href: '/admin/onboarding-management', label: 'External Onboarding', iconPath: '/sidebar-icons/users.svg', icon: Users, aliasPrefix: '/admin/onboarding-schemas' }, + { href: '/admin/internal-dashboard-management', label: 'Internal Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid }, + { href: '/admin/external-dashboard-management', label: 'External Dashboards', iconPath: '/sidebar-icons/dashboard.svg', icon: LayoutGrid, aliasPrefix: '/admin/role-ui-configs' }, + { href: '/admin/approval', label: 'Approval Management', iconPath: '/sidebar-icons/approval.svg', icon: ClipboardList }, + { href: '/admin/department', label: 'Department Management', iconPath: '/sidebar-icons/department.svg', icon: Briefcase, separatorBefore: true }, + { href: '/admin/designation', label: 'Designation Management', iconPath: '/sidebar-icons/designation.svg', icon: Briefcase }, + { href: '/admin/employees', label: 'Employee Management', iconPath: '/sidebar-icons/users.svg', icon: UserCircle2 }, + { href: '/admin/users', label: 'Users Management', iconPath: '/sidebar-icons/users.svg', icon: Users }, + { href: '/admin/company', label: 'Company Management', iconPath: '/sidebar-icons/company.svg', icon: Briefcase }, + { href: '/admin/candidate', label: 'Candidate Management', iconPath: '/sidebar-icons/candidate.svg', icon: UserCircle2 }, + { href: '/admin/customer', label: 'Customer Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 }, + { href: '/admin/photographer', label: 'Photographer Management', iconPath: '/sidebar-icons/photographer.svg', icon: Sparkles, separatorBefore: true }, + { href: '/admin/makeup-artist', label: 'Makeup Artist Management', iconPath: '/sidebar-icons/makeup-artist.svg', icon: Sparkles }, + { href: '/admin/tutors', label: 'Tutors Management', iconPath: '/sidebar-icons/tutor.svg', icon: Sparkles }, + { href: '/admin/developers', label: 'Developers Management', iconPath: '/sidebar-icons/developers.svg', icon: Sparkles }, + { href: '/admin/jobs', label: 'Jobs Management', iconPath: '/sidebar-icons/jobs.svg', icon: Briefcase }, + { href: '/admin/leads', label: 'Leads Management', iconPath: '/sidebar-icons/leads.svg', icon: HandHelping }, + { href: '/admin/pricing', label: 'Pricing Management', iconPath: '/sidebar-icons/pricing.svg', icon: WalletCards }, + { href: '/admin/credit', label: 'Credit Management', iconPath: '/sidebar-icons/credits.svg', icon: WalletCards }, + { href: '/admin/coupon', label: 'Coupon Management', iconPath: '/sidebar-icons/coupon.svg', icon: Percent }, + { href: '/admin/discount', label: 'Discount Management', iconPath: '/sidebar-icons/discount.svg', icon: Percent }, + { href: '/admin/tax', label: 'Tax Management', iconPath: '/sidebar-icons/tax.svg', icon: Receipt }, + { href: '/admin/order', label: 'Order Management', iconPath: '/sidebar-icons/order.svg', icon: FileText }, + { href: '/admin/invoice', label: 'Invoice Management', iconPath: '/sidebar-icons/invoice.svg', icon: FileText }, + { href: '/admin/review', label: 'Review Management', iconPath: '/sidebar-icons/reviews.svg', icon: FileText }, + { href: '/admin/support', label: 'Support Management', iconPath: '/sidebar-icons/support.svg', icon: UserCircle2 }, + { href: '/admin/report', label: 'Report Management', iconPath: '/sidebar-icons/report.svg', icon: Bell }, + { href: '/admin/ledger', label: 'Ledger Management', iconPath: '/sidebar-icons/ledger.svg', icon: Receipt }, + { href: '/admin/kb', label: 'Knowledge Base', icon: FileText }, + { href: '/admin/notifications', label: 'Notifications', icon: Bell }, ]; -export default function AdminSidebar() { +function renderIcon(item: Item, isActive: boolean) { + if (item.iconPath) { + return ; + } + const Icon = item.icon || FileText; + return ; +} + +export default function AdminSidebar(props: { onNavigate?: () => void; onLogout?: () => void }) { const location = useLocation(); - const isActive = (href: string, aliasPrefix?: string) => { - if (href === '/admin') return location.pathname === '/admin'; - if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true; - return location.pathname === href || location.pathname.startsWith(`${href}/`); + const active = (item: Item) => { + if (item.href === '/admin') return location.pathname === '/admin'; + if (item.aliasPrefix && location.pathname.startsWith(item.aliasPrefix)) return true; + return location.pathname === item.href || location.pathname.startsWith(`${item.href}/`); }; - const NavItem = (item: LinkItem) => { - const active = isActive(item.href, item.aliasPrefix); - return ( - - - {item.label} - - ); - }; - - const topLinks = links.filter(l => l.group === '__top__'); - const mgmtLinks = links.filter(l => l.group === 'Management'); - const finLinks = links.filter(l => l.group === 'Finance'); - const platLinks = links.filter(l => l.group === 'Platform'); - return ( -