diff --git a/.claude/launch.json b/.claude/launch.json index 0c42af1..20f4eca 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,7 +4,7 @@ { "name": "admin-solid", "runtimeExecutable": "sh", - "runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port 3020 --host"], + "runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev"], "port": 3020 } ] diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index 64371fb..9bc3491 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -3,29 +3,16 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX, } from 'solid-js'; +import { Bell, Search, Settings, User } from 'lucide-solid'; import AdminSidebar from './AdminSidebar'; import { isExternalIdentity } from '~/lib/admin-auth'; import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; -import { Bell, Search, Settings, LogOut, User, ChevronDown } from 'lucide-solid'; - -// ── Types ──────────────────────────────────────────────────────────────────── 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[] }; - -// ── Tab sets ───────────────────────────────────────────────────────────────── +type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] }; const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ - { - prefixes: ['/admin/roles'], - tabs: [ - { href: '/admin/roles', label: 'Roles', exact: true }, - { href: '/admin/roles/create', label: 'Create Role' }, - { href: '/admin/roles/templates', label: 'View Roles' }, - ], - }, { prefixes: ['/admin/runtime-roles'], tabs: [ @@ -36,8 +23,6 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [ }, ]; -// ── Global search ───────────────────────────────────────────────────────────── - const SEARCH_MODULES = [ { label: 'Users', @@ -84,15 +69,6 @@ const SEARCH_MODULES = [ subtitleKeys: ['email', 'status'], detailBase: '/admin/leads', }, - { - label: 'Candidates', - viewAllHref: '/admin/candidate', - api: '/api/gateway/api/admin/candidates', - listKeys: ['candidates', 'items'], - titleKeys: ['full_name', 'name'], - subtitleKeys: ['email', 'phone'], - detailBase: '/admin/candidate', - }, ]; function pickStr(obj: Record, keys: string[]): string { @@ -107,9 +83,9 @@ function extractList(data: any, keys: string[]): any[] { } function GlobalSearch() { - const [query, setQuery] = createSignal(''); - const [open, setOpen] = createSignal(false); - const [groups, setGroups] = createSignal([]); + const [query, setQuery] = createSignal(''); + const [open, setOpen] = createSignal(false); + const [groups, setGroups] = createSignal([]); const [searching, setSearching] = createSignal(false); let wrapRef!: HTMLDivElement; let timer: ReturnType; @@ -120,9 +96,7 @@ function GlobalSearch() { 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); + 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; @@ -149,198 +123,67 @@ function GlobalSearch() { setQuery(val); clearTimeout(timer); if (val.trim().length < 2) { setGroups([]); setOpen(false); return; } - timer = setTimeout(() => doSearch(val), 400); + timer = setTimeout(() => doSearch(val), 350); }; const close = () => { setOpen(false); setQuery(''); setGroups([]); }; + const onOutside = (e: MouseEvent) => { if (!wrapRef.contains(e.target as Node)) setOpen(false); }; - const onOutside = (e: MouseEvent) => { - if (!wrapRef.contains(e.target as Node)) setOpen(false); - }; onMount(() => document.addEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener('mousedown', onOutside)); return ( -
- {/* Search icon */} - - +
+ handleInput(e.currentTarget.value)} onFocus={() => groups().length > 0 && setOpen(true)} onKeyDown={(e) => e.key === 'Escape' && close()} - class="h-10 w-full rounded-xl border border-gray-200 bg-gray-50 pl-10 pr-4 text-sm text-gray-700 placeholder:text-gray-400 transition-colors focus:border-gray-300 focus:bg-white focus:outline-none" + class="h-14 w-full rounded-2xl border-2 border-transparent bg-[#f9fafb] pl-12 pr-4 text-[16px] text-[#000032] placeholder:text-[rgba(0,0,50,0.4)] outline-none transition-all focus:border-[#e5e7eb] focus:bg-white" /> - {/* Results dropdown */} 0}> -
+
{(group) => ( -
- - {/* No results */} = 2 && groups().length === 0}> -
-

- No results for "{query()}" -

+
+ No results for "{query()}"
); } -// ── Notification bell ───────────────────────────────────────────────────────── - -function NotificationBell(props: { count: number }) { - const [open, setOpen] = createSignal(false); - let ref!: HTMLDivElement; - - const onOutside = (e: MouseEvent) => { - if (!ref.contains(e.target as Node)) setOpen(false); - }; - onMount(() => document.addEventListener('mousedown', onOutside)); - onCleanup(() => document.removeEventListener('mousedown', onOutside)); - - return ( -
- - - -
-
- Notifications - 0}> - - {props.count} new - - -
-
- -

Real-time notifications

-

Connecting to live feed…

-
-
-
-
- ); -} - -// ── User menu ───────────────────────────────────────────────────────────────── - -function UserMenu(props: { name: string; initials: string; onLogout: () => void }) { - const [open, setOpen] = createSignal(false); - let ref!: HTMLDivElement; - - const onOutside = (e: MouseEvent) => { - if (!ref.contains(e.target as Node)) setOpen(false); - }; - onMount(() => document.addEventListener('mousedown', onOutside)); - onCleanup(() => document.removeEventListener('mousedown', onOutside)); - - return ( -
- - - - - ); -} - -// ── Tabs ────────────────────────────────────────────────────────────────────── - function ShowTabs(props: { tabs: Tab[]; isTabActive: (tab: Tab) => boolean; @@ -350,10 +193,7 @@ function ShowTabs(props: { }) { 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-[#fd6216]' - : 'text-slate-500 hover:text-slate-800' + props.isTabActive(tab) ? 'text-[#fa5014]' : 'text-[rgba(0,0,50,0.6)] hover:text-[#000032]' }`} > {tab.label} @@ -371,38 +209,32 @@ function ShowTabs(props: { )}
); } -// ── Main shell ──────────────────────────────────────────────────────────────── - export default function AdminShell(props: { children: JSX.Element }) { - const location = useLocation(); - const navigate = useNavigate(); + const location = useLocation(); + const navigate = useNavigate(); const [searchParams] = useSearchParams(); const [checkedSession, setCheckedSession] = createSignal(false); - const [adminName, setAdminName] = createSignal('Admin User'); - const [sidebarOpen, setSidebarOpen] = createSignal(false); + const [adminName, setAdminName] = createSignal('Admin User'); + const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); - const [notifCount] = createSignal(0); // wired in Phase 2 (SSE) + const [notifCount] = createSignal(0); const [tabsTrackEl, setTabsTrackEl] = createSignal(); - const [tabRefs, setTabRefs] = createSignal>({}); + const [tabRefs, setTabRefs] = createSignal>({}); const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false }); - // ── Tab logic ────────────────────────────────────────────────────────────── 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 []; }); @@ -422,7 +254,8 @@ export default function AdminShell(props: { children: JSX.Element }) { }; createEffect(() => { - tabs(); location.pathname; + tabs(); + location.pathname; requestAnimationFrame(refreshTabIndicator); }); @@ -433,12 +266,10 @@ export default function AdminShell(props: { children: JSX.Element }) { const isLocalDev = 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'); + (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); if (isPreview || isLocalDev) { - if (typeof sessionStorage !== 'undefined') - sessionStorage.setItem('nxtgauge_admin_preview', '1'); + if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); setAdminSession(); setCheckedSession(true); return; @@ -471,23 +302,10 @@ export default function AdminShell(props: { children: JSX.Element }) { navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); } }; + void verify(); }); - const onLogout = async () => { - await fetch('/api/gateway/users/auth/logout', { - method: 'POST', - headers: { Accept: 'application/json', 'x-portal-target': 'admin' }, - credentials: 'include', - }).catch(() => {}); - clearAdminSession(); - if (typeof sessionStorage !== 'undefined') { - sessionStorage.removeItem('nxtgauge_admin_access_token'); - sessionStorage.removeItem('nxtgauge_admin_preview'); - } - navigate('/login', { replace: true }); - }; - const adminInitials = createMemo(() => { const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); if (parts.length === 0) return 'U'; @@ -495,96 +313,16 @@ export default function AdminShell(props: { children: JSX.Element }) { return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); }); - // Sidebar width — synced between header logo section and sidebar - const sidebarW = () => (sidebarCollapsed() ? 'w-[72px]' : 'w-64'); - return ( -
- - {/* ── Header ── */} -
- - {/* Logo section — same width as sidebar */} - - - {/* Search bar */} -
- -
- - {/* Mobile menu button */} - - - {/* Right: Bell + Gear + User */} -
- - {/* ── Body ── */} +
-
-
-

Checking session…

-
-
- } + fallback={
Checking session…
} > -
- {/* Mobile overlay */} -
setSidebarOpen(false)} - /> +
+
setSidebarOpen(false)} /> - {/* Sidebar */} -
+
setSidebarCollapsed((v) => !v)} @@ -594,17 +332,49 @@ export default function AdminShell(props: { children: JSX.Element }) { />
- {/* Main content */} -
- - {props.children} -
+
+
+
+ +
+ + +
+ + +
+ +
+
+
+ +
+
+ + {props.children} +
+
+
diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index 92e764a..e03dd02 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -17,69 +17,61 @@ type NavItem = { aliasPrefix?: string; }; -// Groups match Figma sidebar order with dividers between sections const GROUPS: NavItem[][] = [ - // ── Internal management ────────────────────────────────────────────────── [ - { href: '/admin', label: 'Dashboard', icon: LayoutGrid }, - { href: '/admin/department', label: 'Department Management', icon: Building2 }, - { href: '/admin/designation', label: 'Designation Management', icon: Briefcase }, - { href: '/admin/internal-role-management', label: 'Internal Role Management', icon: ShieldCheck }, - { href: '/admin/employees', label: 'Employee Management', icon: Users }, + { href: '/admin', label: 'Dashboard', icon: LayoutGrid }, + { href: '/admin/department', label: 'Department Management', icon: Building2 }, + { href: '/admin/designation', label: 'Designation Management', icon: Briefcase }, + { href: '/admin/internal-role-management', label: 'Internal Role Management', icon: ShieldCheck, aliasPrefix: '/admin/roles' }, + { href: '/admin/employees', label: 'Employee Management', icon: Users }, ], - // ── External configuration ─────────────────────────────────────────────── [ - { href: '/admin/runtime-roles', label: 'External Role Management', icon: ShieldCheck, aliasPrefix: '/admin/external-role-management' }, - { 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/runtime-roles', label: 'External Role Management', icon: ShieldCheck, aliasPrefix: '/admin/external-role-management' }, + { 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' }, ], - // ── Compliance ─────────────────────────────────────────────────────────── [ - { href: '/admin/verification-status', label: 'Verification Management', icon: BadgeCheck }, - { href: '/admin/approval', label: 'Approval Management', icon: ClipboardList }, + { href: '/admin/verification-status', label: 'Verification Management', icon: BadgeCheck }, + { href: '/admin/approval', label: 'Approval Management', icon: ClipboardList }, ], - // ── External users ─────────────────────────────────────────────────────── [ - { 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/photographer', label: 'Photographer Management', icon: Camera }, - { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette }, - { href: '/admin/tutors', label: 'Tutor Management', icon: BookOpen }, - { href: '/admin/developers', label: 'Developer Management', icon: Code2 }, - { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity }, - { href: '/admin/graphic-designers', label: 'Graphic Designer Management', icon: PenTool }, - { href: '/admin/social-media-managers', label: 'Social Media Management', icon: Megaphone }, - { href: '/admin/video-editors', label: 'Video Editor Management', icon: Film }, - { href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils }, + { 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/photographer', label: 'Photographer Management', icon: Camera }, + { href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette }, + { href: '/admin/tutors', label: 'Tutor Management', icon: BookOpen }, + { href: '/admin/developers', label: 'Developer Management', icon: Code2 }, + { href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity }, + { href: '/admin/graphic-designers', label: 'Graphic Designer Management', icon: PenTool }, + { href: '/admin/social-media-managers', label: 'Social Media Management', icon: Megaphone }, + { href: '/admin/video-editors', label: 'Video Editor Management', icon: Film }, + { href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils }, ], - // ── Business operations ────────────────────────────────────────────────── [ - { href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness }, - { href: '/admin/leads', label: 'Leads Management', icon: HandHelping }, - { href: '/admin/applications', label: 'Applications Management', icon: ClipboardList }, - { href: '/admin/responses', label: 'Responses Management', icon: MessageSquare }, - { href: '/admin/review', label: 'Review Management', icon: Star }, + { href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness }, + { href: '/admin/leads', label: 'Leads Management', icon: HandHelping }, + { href: '/admin/applications', label: 'Applications Management', icon: ClipboardList }, + { href: '/admin/responses', label: 'Responses Management', icon: MessageSquare }, + { href: '/admin/review', label: 'Review Management', icon: Star }, ], - // ── Finance ────────────────────────────────────────────────────────────── [ - { 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/ledger', label: 'Ledger Management', icon: Receipt }, + { 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/ledger', label: 'Ledger Management', icon: Receipt }, ], - // ── Platform operations ────────────────────────────────────────────────── [ - { href: '/admin/kb', label: 'Knowledge Base Management', icon: BookMarked }, - { href: '/admin/notifications', label: 'Notifications', icon: Bell }, - { href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon }, - { href: '/admin/report', label: 'Report Management', icon: BarChart3 }, + { href: '/admin/kb', label: 'Knowledge Base Management', icon: BookMarked }, + { href: '/admin/notifications', label: 'Notifications', icon: Bell }, + { href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon }, + { href: '/admin/report', label: 'Report Management', icon: BarChart3 }, ], ]; @@ -93,47 +85,40 @@ export default function AdminSidebar(props: { const location = useLocation(); const isActive = (item: NavItem) => { - if (item.href === '/admin') return location.pathname === '/admin'; + if (location.pathname === '/admin') return item.href === '/admin/department'; + if (item.href === '/admin') return false; if (item.aliasPrefix && location.pathname.startsWith(item.aliasPrefix)) return true; return location.pathname === item.href || location.pathname.startsWith(`${item.href}/`); }; return (