diff --git a/package.json b/package.json index cb564cb..64a97cd 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,11 @@ "dev": "vinxi dev", "build": "vinxi build", "start": "vinxi start", + "start:3000": "HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs", + "admin:restart:3000": "bash ./scripts/admin-3000-service.sh restart", + "admin:stop:3000": "bash ./scripts/admin-3000-service.sh stop", + "admin:status:3000": "bash ./scripts/admin-3000-service.sh status", + "admin:start:3000": "bash ./scripts/admin-3000-service.sh start", "test": "node --test --experimental-strip-types src/lib/**/*.test.ts", "test:e2e": "playwright test", "test:e2e:headed": "playwright test --headed", diff --git a/scripts/admin-3000-daemon.sh b/scripts/admin-3000-daemon.sh new file mode 100755 index 0000000..f022e83 --- /dev/null +++ b/scripts/admin-3000-daemon.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -u + +ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid" +APP_LOG="/tmp/nxtgauge-admin-3000.log" + +cd "$ROOT_DIR" || exit 1 + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] admin-3000 daemon started" >> "$APP_LOG" + +while true; do + if [[ ! -f ".output/server/index.mjs" ]]; then + echo "[$(date '+%Y-%m-%d %H:%M:%S')] build output missing, running build..." >> "$APP_LOG" + npm run build >> "$APP_LOG" 2>&1 + fi + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] launching admin on 0.0.0.0:3000" >> "$APP_LOG" + HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs >> "$APP_LOG" 2>&1 + code=$? + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] admin exited with code ${code}, restarting in 2s" >> "$APP_LOG" + sleep 2 +done + diff --git a/scripts/admin-3000-service.sh b/scripts/admin-3000-service.sh new file mode 100755 index 0000000..910304c --- /dev/null +++ b/scripts/admin-3000-service.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid" +PID_FILE="/tmp/nxtgauge-admin-3000.pid" +APP_LOG="/tmp/nxtgauge-admin-3000.log" +APP_URL="http://127.0.0.1:3000/admin/external-dashboard-management" + +kill_listeners() { + lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill -9 || true +} + +start() { + if [[ -f "$PID_FILE" ]]; then + old_pid="$(cat "$PID_FILE" || true)" + if [[ -n "${old_pid:-}" ]] && kill -0 "$old_pid" 2>/dev/null; then + if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then + echo "admin-3000 already running (pid $old_pid)" + echo "url: $APP_URL" + return 0 + fi + kill -9 "$old_pid" || true + fi + rm -f "$PID_FILE" + fi + + pkill -f "admin-3000-daemon.sh" || true + pkill -f ".output/server/index.mjs" || true + kill_listeners + + nohup /bin/zsh -lc "cd '$ROOT_DIR' && HOST=0.0.0.0 PORT=3000 node .output/server/index.mjs" >>"$APP_LOG" 2>&1 & + new_pid=$! + echo "$new_pid" > "$PID_FILE" + + for _ in {1..30}; do + if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then + echo "admin-3000: running (pid $new_pid)" + echo "url: $APP_URL" + return 0 + fi + sleep 1 + done + + echo "admin-3000 failed to become healthy" + tail -n 80 /tmp/nxtgauge-admin-3000.log || true + exit 1 +} + +stop() { + if [[ -f "$PID_FILE" ]]; then + pid="$(cat "$PID_FILE" || true)" + if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" || true + fi + rm -f "$PID_FILE" + fi + + pkill -f "admin-3000-daemon.sh|.output/server/index.mjs" || true + kill_listeners + echo "admin-3000: stopped" +} + +status() { + app_state="stopped" + app_pid="" + if [[ -f "$PID_FILE" ]]; then + app_pid="$(cat "$PID_FILE" || true)" + if [[ -n "${app_pid:-}" ]] && kill -0 "$app_pid" 2>/dev/null; then + app_state="running" + fi + fi + + if lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then + echo "admin-3000: running" + echo "process: $app_state${app_pid:+ (pid $app_pid)}" + lsof -nP -iTCP:3000 -sTCP:LISTEN + exit 0 + fi + + echo "admin-3000: stopped" + echo "process: $app_state${app_pid:+ (pid $app_pid)}" + exit 1 +} + +action="${1:-restart}" +case "$action" in + start) start ;; + stop) stop ;; + restart) stop; start ;; + status) status ;; + *) + echo "Usage: $0 [start|stop|restart|status]" + exit 2 + ;; +esac diff --git a/scripts/restart-admin-3000.sh b/scripts/restart-admin-3000.sh new file mode 100755 index 0000000..cc139ec --- /dev/null +++ b/scripts/restart-admin-3000.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="/Users/ashwin/workspace/nxtgauge-admin-solid" +PID_FILE="/tmp/nxtgauge-admin-3000.pid" +LOG_FILE="/tmp/nxtgauge-admin-3000.log" +APP_URL="http://127.0.0.1:3000/admin/external-dashboard-management" + +stop_server() { + if [[ -f "$PID_FILE" ]]; then + pid="$(cat "$PID_FILE" || true)" + if [[ -n "${pid:-}" ]] && kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" || true + fi + rm -f "$PID_FILE" + fi + + lsof -tiTCP:3000 -sTCP:LISTEN | xargs -r kill -9 || true +} + +status_server() { + if lsof -nP -iTCP:3000 -sTCP:LISTEN >/dev/null 2>&1; then + echo "admin-3000: running" + lsof -nP -iTCP:3000 -sTCP:LISTEN + exit 0 + fi + echo "admin-3000: stopped" + exit 1 +} + +start_server() { + cd "$ROOT_DIR" + + if [[ ! -f ".output/server/index.mjs" ]]; then + echo "Build output missing. Running build..." + npm run build + fi + + nohup env HOST=127.0.0.1 PORT=3000 node .output/server/index.mjs >"$LOG_FILE" 2>&1 & + new_pid=$! + echo "$new_pid" > "$PID_FILE" + + for _ in {1..20}; do + if curl -fsS -m 3 "$APP_URL" >/dev/null 2>&1; then + echo "admin-3000: running (pid $new_pid)" + echo "url: $APP_URL" + exit 0 + fi + sleep 1 + done + + echo "admin-3000: failed to start" + echo "Last log lines:" + tail -n 40 "$LOG_FILE" || true + exit 1 +} + +action="${1:-restart}" +case "$action" in + restart) + stop_server + start_server + ;; + stop) + stop_server + echo "admin-3000: stopped" + ;; + status) + status_server + ;; + *) + echo "Usage: $0 [restart|stop|status]" + exit 2 + ;; +esac diff --git a/src/app.css b/src/app.css index a787453..8fb77fe 100644 --- a/src/app.css +++ b/src/app.css @@ -466,3 +466,128 @@ body { padding: 10px 11px; } } + +/* ===== Dark Theme Coverage ===== */ +html[data-theme='dark'] body { + background: #0b1220; + color: #e5e7eb; +} + +html[data-theme='dark'] .table-card { + background: #0f172a; + border-color: #243041; + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.35); +} + +html[data-theme='dark'] .data-table thead th { + background: #111827; + color: #e5e7eb; +} + +html[data-theme='dark'] .data-table tbody td { + color: #d1d5db; + border-bottom-color: #243041; +} + +html[data-theme='dark'] .data-table tbody tr:hover td { + background: #111a2f; +} + +html[data-theme='dark'] .data-table-empty { + color: #94a3b8; +} + +html[data-theme='dark'] .admin-main table.w-full thead th { + background: #111827; + color: #e5e7eb; +} + +html[data-theme='dark'] .admin-main table.w-full tbody td { + color: #d1d5db; + border-bottom-color: #243041; +} + +html[data-theme='dark'] .admin-main table.w-full tbody tr:hover td { + background: #111a2f; +} + +html[data-theme='dark'] .tab-bar { + border-bottom-color: #243041; +} + +html[data-theme='dark'] .tab-link { + color: #9ca3af; +} + +html[data-theme='dark'] .tab-link:hover { + color: #e5e7eb; +} + +html[data-theme='dark'] .tab-link[aria-current='page'] { + color: #ff5e13; + border-bottom-color: #ff5e13; +} + +html[data-theme='dark'] .admin-link-tabs { + border-color: #243041; + background: #0f172a; +} + +html[data-theme='dark'] .admin-link-tabs a { + color: #9ca3af; +} + +html[data-theme='dark'] .admin-link-tabs a[aria-current='page'] { + background: rgba(255, 94, 19, 0.16); + color: #ff8a52; +} + +html[data-theme='dark'] .preview-tabs button { + border-color: #243041; + background: #0f172a; + color: #cbd5e1; +} + +html[data-theme='dark'] .admin-main input[type='text'], +html[data-theme='dark'] .admin-main input[type='search'], +html[data-theme='dark'] .admin-main input[type='number'], +html[data-theme='dark'] .admin-main input[type='email'], +html[data-theme='dark'] .admin-main input[type='url'], +html[data-theme='dark'] .admin-main input[type='password'], +html[data-theme='dark'] .admin-main select, +html[data-theme='dark'] .admin-main textarea { + background: #0f172a; + color: #e5e7eb; + border-color: #334155; +} + +html[data-theme='dark'] .admin-main input::placeholder, +html[data-theme='dark'] .admin-main textarea::placeholder { + color: #94a3b8; +} + +html[data-theme='dark'] .btn-secondary, +html[data-theme='dark'] .action-btn { + background: #111827; + border-color: #334155; + color: #d1d5db; +} + +html[data-theme='dark'] .btn-secondary:hover, +html[data-theme='dark'] .action-btn:hover { + background: #1f2937; + border-color: #475569; +} + +html[data-theme='dark'] .admin-main div[style*='border-bottom:1px solid #E5E7EB'] { + border-bottom-color: #243041 !important; +} + +html[data-theme='dark'] .admin-main button[style*='padding-bottom:12px'][style*='font-size:14px'] { + color: #94a3b8 !important; +} + +html[data-theme='dark'] .admin-main button[style*='padding-bottom:12px'][style*='border-bottom:2px solid #FF5E13'] { + color: #ff8a52 !important; + border-bottom-color: #ff5e13 !important; +} diff --git a/src/components/AdminShell.tsx b/src/components/AdminShell.tsx index c53380e..ab2eb12 100644 --- a/src/components/AdminShell.tsx +++ b/src/components/AdminShell.tsx @@ -3,10 +3,11 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount, type JSX, } from 'solid-js'; -import { Bell, Search, Settings, User } from 'lucide-solid'; +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 }; @@ -27,6 +28,8 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ { 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' }, @@ -42,6 +45,8 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ { 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' }, @@ -49,6 +54,8 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ { prefix: '/admin/tax', label: 'Tax Management' }, { prefix: '/admin/order', label: 'Order Management' }, { prefix: '/admin/invoice', label: 'Invoice 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' }, @@ -57,19 +64,24 @@ 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', 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/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/onboarding-management', keys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { prefix: '/admin/onboarding-schemas', keys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { 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'] }, @@ -85,6 +97,8 @@ const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [ { 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'] }, @@ -92,6 +106,8 @@ const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [ { prefix: '/admin/tax', keys: ['TAX_MANAGEMENT', 'TAXES'] }, { prefix: '/admin/order', keys: ['ORDER_MANAGEMENT', 'ORDERS'] }, { prefix: '/admin/invoice', keys: ['INVOICE_MANAGEMENT', 'INVOICES'] }, + { 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'] }, @@ -303,10 +319,37 @@ export default function AdminShell(props: { children: JSX.Element }) { const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); const [notifCount] = 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/gateway/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; @@ -336,16 +379,40 @@ export default function AdminShell(props: { children: JSX.Element }) { 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)); - 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'); - if (isPreview || isLocalDev) { + if (isPreview) { if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); setAdminSession(); setCheckedSession(true); @@ -397,19 +464,7 @@ export default function AdminShell(props: { children: JSX.Element }) { }); 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); - } + setAllowedModules(normalizeAllowedModules(runtime)); const activeRole = String(runtime?.active_role || runtime?.user?.active_role || roleKey || '').toUpperCase(); if (activeRole) setIsSuperAdmin(activeRole === 'SUPER_ADMIN'); } else { @@ -447,6 +502,15 @@ export default function AdminShell(props: { children: JSX.Element }) { 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; @@ -457,7 +521,10 @@ export default function AdminShell(props: { children: JSX.Element }) { const path = location.pathname; if (path === '/admin') return; - const guard = ROUTE_MODULE_KEYS.find((entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`)); + 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)); @@ -468,7 +535,7 @@ export default function AdminShell(props: { children: JSX.Element }) { }); return ( -
+
Checking session…
} @@ -483,25 +550,31 @@ export default function AdminShell(props: { children: JSX.Element }) { 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}
diff --git a/src/components/AdminSidebar.tsx b/src/components/AdminSidebar.tsx index 2a20da2..75335df 100644 --- a/src/components/AdminSidebar.tsx +++ b/src/components/AdminSidebar.tsx @@ -7,7 +7,7 @@ import { WalletCards, CreditCard, Tag, Percent, Receipt, ShoppingCart, FileCheck, Star, HeadphonesIcon, BarChart3, ChevronLeft, BadgeCheck, Activity, Film, Utensils, PenTool, - Megaphone, + Megaphone, Bell, } from 'lucide-solid'; type NavItem = { @@ -20,7 +20,7 @@ type NavItem = { const GROUPS: NavItem[][] = [ [ - { href: '/admin', label: 'Dashboard', icon: LayoutGrid, moduleKeys: ['ADMIN_DASHBOARD'] }, + { href: '/admin', label: 'Dashboard', icon: LayoutGrid, moduleKeys: ['ADMIN_DASHBOARD', 'DASHBOARD'] }, ], [ { href: '/admin/department', label: 'Department Management', icon: Building2, moduleKeys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, @@ -30,9 +30,9 @@ const GROUPS: NavItem[][] = [ ], [ { 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/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas', moduleKeys: ['EXTERNAL_ONBOARDING_MANAGEMENT', 'ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] }, + { href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard, moduleKeys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS', 'INTERNAL_DASHBOARD_CONFIG'] }, + { 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', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] }, ], [ { href: '/admin/verification', label: 'Verification Management', icon: BadgeCheck, moduleKeys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, @@ -58,6 +58,8 @@ const GROUPS: NavItem[][] = [ [ { 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/applications', label: 'Applications Management', icon: FileText, moduleKeys: ['APPLICATIONS_MANAGEMENT', 'APPLICATIONS'] }, + { href: '/admin/responses', label: 'Responses Management', icon: FileText, moduleKeys: ['RESPONSES_MANAGEMENT', 'RESPONSES'] }, ], [ { href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, moduleKeys: ['PRICING_MANAGEMENT', 'PRICING'] }, @@ -69,6 +71,8 @@ const GROUPS: NavItem[][] = [ { href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck, moduleKeys: ['INVOICE_MANAGEMENT', 'INVOICES'] }, ], [ + { href: '/admin/kb', label: 'Knowledge Base Management', icon: BookOpen, moduleKeys: ['KNOWLEDGE_BASE_MANAGEMENT', 'KNOWLEDGE_BASE', 'KB'] }, + { href: '/admin/notifications', label: 'Notifications', icon: Bell, moduleKeys: ['NOTIFICATIONS_MANAGEMENT', 'NOTIFICATIONS'] }, { 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'] }, @@ -82,6 +86,7 @@ export default function AdminSidebar(props: { onNavigate?: () => void; adminName: string; adminInitials: string; + theme?: 'light' | 'dark'; allowedModules?: string[] | null; isSuperAdmin?: boolean; }) { @@ -106,6 +111,8 @@ export default function AdminSidebar(props: { return location.pathname === item.href || location.pathname.startsWith(`${item.href}/`); }; + const isDark = () => props.theme === 'dark'; + return (