chore: checkpoint workspace updates

This commit is contained in:
Tracewebstudio Dev 2026-04-26 23:58:42 +02:00
parent 2c4e579725
commit b75c99408c
25 changed files with 7630 additions and 3772 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,70 @@
- generic [ref=e2]:
- main [ref=e3]:
- generic:
- generic:
- generic:
- img
- generic:
- img
- generic:
- img
- generic:
- img
- generic:
- img
- navigation [ref=e5]:
- link "Nxtgauge home" [ref=e6] [cursor=pointer]:
- /url: /
- img "NXTGAUGE" [ref=e7]
- generic [ref=e8]:
- link "Home" [ref=e9] [cursor=pointer]:
- /url: /
- link "Professionals" [ref=e10] [cursor=pointer]:
- /url: /professionals
- link "About Us" [ref=e11] [cursor=pointer]:
- /url: /about
- link "Help Center" [ref=e12] [cursor=pointer]:
- /url: /help-center
- link "Contact Us" [ref=e13] [cursor=pointer]:
- /url: /contact
- link "Login" [ref=e15] [cursor=pointer]:
- /url: /login
- generic [ref=e16]:
- generic [ref=e17]:
- img "Public Workspace" [ref=e18]
- generic [ref=e20]:
- paragraph [ref=e21]: Public Workspace
- heading "Welcome Back To Nxtgauge" [level=1] [ref=e22]
- paragraph [ref=e23]: Sign in to manage your profile, portfolio, and verification in one place.
- generic [ref=e24]:
- heading "Sign In" [level=2] [ref=e25]
- generic [ref=e26]:
- generic [ref=e27]: EMAIL
- textbox "EMAIL" [ref=e28]:
- /placeholder: Enter your email
- paragraph [ref=e29]: • Enter a valid email format
- generic [ref=e30]:
- generic [ref=e31]: PASSWORD
- generic [ref=e32]:
- textbox "PASSWORD" [ref=e33]:
- /placeholder: Enter your password
- button "Show password" [ref=e34] [cursor=pointer]:
- img [ref=e35]
- generic [ref=e38]:
- generic [ref=e39]: CAPTCHA
- generic [ref=e40]:
- button "↻" [ref=e41]
- generic "Captcha image" [ref=e42]
- textbox "Enter captcha" [ref=e43]
- button "Sign In" [ref=e44]
- generic [ref=e45]:
- paragraph [ref=e46]: Secure login with email verification.
- paragraph [ref=e47]:
- text: New user?
- link "Sign Up" [ref=e48] [cursor=pointer]:
- /url: /signup
- paragraph [ref=e49]:
- link "Forgot Password?" [ref=e50] [cursor=pointer]:
- /url: /forgot-password
- button "AI Assistant" [ref=e51] [cursor=pointer]:
- img [ref=e52]

View file

@ -0,0 +1,5 @@
> dev
> vinxi dev
vinxi v0.5.11

View file

@ -1 +1 @@
7741 61044

1
admin-solid.start.log Normal file
View file

@ -0,0 +1 @@
Listening on http://[::]:3000

1
admin-solid.start.pid Normal file
View file

@ -0,0 +1 @@
72260

View file

@ -9,3 +9,74 @@ vinxi starting dev server
➜ Local: http://localhost:3000/ ➜ Local: http://localhost:3000/
➜ Network: use --host to expose ➜ Network: use --host to expose
1:30:54 PM [vite] (ssr) page reload vinxi/routes
1:30:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:30:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:31:04 PM [vite] (ssr) page reload vinxi/routes
1:31:04 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:31:04 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:31:15 PM [vite] (ssr) page reload vinxi/routes
1:31:15 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:31:16 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:31:47 PM [vite] (ssr) page reload vinxi/routes
1:31:47 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:31:47 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:32:06 PM [vite] (ssr) page reload vinxi/routes
1:32:06 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:32:06 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:32:13 PM [vite] (ssr) page reload vinxi/routes
1:32:13 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:32:13 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/employees/index.tsx?pick=default&pick=$css
1:39:55 PM [vite] (ssr) page reload vinxi/routes
1:39:55 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:39:55 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:40:16 PM [vite] (ssr) page reload vinxi/routes
1:40:16 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:40:16 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:42:29 PM [vite] (ssr) page reload vinxi/routes
1:42:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:42:29 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:43:02 PM [vite] (ssr) page reload vinxi/routes
1:43:02 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:43:02 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:43:06 PM [vite] (ssr) page reload vinxi/routes
1:43:06 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:43:06 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:46:00 PM [vite] (ssr) page reload vinxi/routes
1:46:00 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:46:00 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
1:47:24 PM [vite] (ssr) page reload vinxi/routes
1:47:24 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
1:47:24 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
3:43:28 PM [vite] (ssr) page reload vinxi/routes
3:43:28 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
3:43:28 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
3:43:29 PM [vite] (client) hmr invalidate /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
3:43:29 PM [vite] (client) page reload src/routes/admin/external-roles.tsx
3:49:14 PM [vite] (ssr) page reload vinxi/routes
3:49:14 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
3:49:14 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
3:50:11 PM [vite] (ssr) page reload vinxi/routes
3:50:11 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
3:50:11 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
3:50:36 PM [vite] (ssr) page reload vinxi/routes
3:50:36 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
3:50:36 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:15:08 PM [vite] (ssr) page reload vinxi/routes
5:15:08 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:15:09 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:15:14 PM [vite] (ssr) page reload vinxi/routes
5:15:14 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:15:15 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:15:21 PM [vite] (ssr) page reload vinxi/routes
5:15:21 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:15:21 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:15:38 PM [vite] (ssr) page reload vinxi/routes
5:15:38 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:15:38 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:31:17 PM [vite] (ssr) page reload vinxi/routes
5:31:17 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:31:17 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
5:43:15 PM [vite] (ssr) page reload vinxi/routes
5:43:15 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
5:43:15 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css

16
package-lock.json generated
View file

@ -7,8 +7,8 @@
"name": "nxtgauge-admin-solid", "name": "nxtgauge-admin-solid",
"dependencies": { "dependencies": {
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0", "@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.3.2", "@solidjs/start": "^1.3.0",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@thisbeyond/solid-dnd": "^0.7.5", "@thisbeyond/solid-dnd": "^0.7.5",
"apexcharts": "^5.10.4", "apexcharts": "^5.10.4",
@ -2747,18 +2747,18 @@
} }
}, },
"node_modules/@solidjs/router": { "node_modules/@solidjs/router": {
"version": "0.15.4", "version": "0.15.3",
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.4.tgz", "resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz",
"integrity": "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==", "integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"solid-js": "^1.8.6" "solid-js": "^1.8.6"
} }
}, },
"node_modules/@solidjs/start": { "node_modules/@solidjs/start": {
"version": "1.3.2", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/@solidjs/start/-/start-1.3.2.tgz", "resolved": "https://registry.npmjs.org/@solidjs/start/-/start-1.3.0.tgz",
"integrity": "sha512-tasDl3utVbtP0rr4InB3ntBIFV2upvEiFrOOCkRrAA3yBfjx9elpxnc94sJQXo65PNYdAAAkPIC6h93vLrtwHg==", "integrity": "sha512-FMqc0ZaAUIFBVOEUV87Y1W6LuCN5OveOigXvjZ9CarB/TQSC3QqDBSX+EyWkvreGIU7zsEIi0mka6NGJgJ5oOQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/server-functions-plugin": "1.121.21", "@tanstack/server-functions-plugin": "1.121.21",

View file

@ -30,8 +30,8 @@
}, },
"dependencies": { "dependencies": {
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0", "@solidjs/router": "^0.15.3",
"@solidjs/start": "^1.3.2", "@solidjs/start": "^1.3.0",
"@tailwindcss/vite": "^4.2.2", "@tailwindcss/vite": "^4.2.2",
"@thisbeyond/solid-dnd": "^0.7.5", "@thisbeyond/solid-dnd": "^0.7.5",
"apexcharts": "^5.10.4", "apexcharts": "^5.10.4",
@ -61,9 +61,9 @@
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"storybook": "^10.3.3", "storybook": "^10.3.3",
"storybook-solidjs-vite": "^10.0.11", "storybook-solidjs-vite": "^10.0.11",
"typescript": "^5.5.0",
"visbug": "^0.1.14", "visbug": "^0.1.14",
"vitest": "^4.1.1",
"vite-plugin-solid": "^2.11.12", "vite-plugin-solid": "^2.11.12",
"typescript": "^5.5.0" "vitest": "^4.1.1"
} }
} }

View file

@ -7,7 +7,7 @@ import "./app.css";
export default function App() { export default function App() {
return ( return (
<Router <Router
root={props => ( root={(props) => (
<MetaProvider> <MetaProvider>
<Title>ADMIN PANEL | NXTGAUGE</Title> <Title>ADMIN PANEL | NXTGAUGE</Title>
<Suspense>{props.children}</Suspense> <Suspense>{props.children}</Suspense>

View file

@ -1,172 +1,214 @@
import { A, useLocation, useNavigate, useSearchParams } from '@solidjs/router'; import { A, useLocation, useNavigate, useSearchParams } from "@solidjs/router";
import { import {
For, Show, createEffect, createMemo, createSignal, For,
onCleanup, onMount, type JSX, Show,
} from 'solid-js'; createEffect,
import { Bell, Moon, Search, Settings, Sun, User } from 'lucide-solid'; createMemo,
import AdminSidebar from './AdminSidebar'; createSignal,
import { isExternalIdentity } from '~/lib/admin-auth'; onCleanup,
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session'; onMount,
import { normalizeAllowedModules } from '~/lib/admin/module-access'; 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 Tab = { href: string; label: string; exact?: boolean };
type SearchResult = { id: string; title: string; subtitle: string; href: string }; type SearchResult = { id: string; title: string; subtitle: string; href: string };
type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] }; type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] };
const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [
{ prefix: '/admin', label: 'Dashboard', exact: true }, { prefix: "/admin", label: "Dashboard", exact: true },
{ prefix: '/admin/department', label: 'Department Management' }, { prefix: "/admin/department", label: "Department Management" },
{ prefix: '/admin/designation', label: 'Designation Management' }, { prefix: "/admin/designation", label: "Designation Management" },
{ prefix: '/admin/roles', label: 'Internal Role Management' }, { prefix: "/admin/roles", label: "Internal Role Management" },
{ prefix: '/admin/employees', label: 'Employee Management' }, { prefix: "/admin/employees", label: "Employee Management" },
{ prefix: '/admin/external-roles', label: 'External Role Management' }, { prefix: "/admin/external-roles", label: "External Role Management" },
{ prefix: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management' }, { prefix: "/admin/internal-dashboard-management", label: "Internal Dashboard Management" },
{ prefix: '/admin/external-dashboard-management', label: 'External Dashboard Management' }, { prefix: "/admin/external-dashboard-management", label: "External Dashboard Management" },
{ prefix: '/admin/role-ui-configs', label: 'External Dashboard Management' }, { prefix: "/admin/role-ui-configs", label: "External Dashboard Management" },
{ prefix: '/admin/verification', label: 'Verification Management' }, { prefix: "/admin/verification", label: "Verification Management" },
{ prefix: '/admin/verification-status', label: 'Verification Management' }, { prefix: "/admin/verification-status", label: "Verification Management" },
{ prefix: '/admin/approval', label: 'Approval Management' }, { prefix: "/admin/approval", label: "Approval Management" },
{ prefix: '/admin/approvals', label: 'Approval Management' }, { prefix: "/admin/approvals", label: "Approval Management" },
{ prefix: '/admin/approval-management', label: 'Approval Management' }, { prefix: "/admin/approval-management", label: "Approval Management" },
{ prefix: '/admin/users', label: 'Users Management' }, { prefix: "/admin/users", label: "Users Management" },
{ prefix: '/admin/company', label: 'Company Management' }, { prefix: "/admin/company", label: "Company Management" },
{ prefix: '/admin/candidate', label: 'Candidate Management' }, { prefix: "/admin/candidate", label: "Candidate Management" },
{ prefix: '/admin/customer', label: 'Customer Management' }, { prefix: "/admin/customer", label: "Customer Management" },
{ prefix: '/admin/photographer', label: 'Photographer Management' }, { prefix: "/admin/photographer", label: "Photographer Management" },
{ prefix: '/admin/makeup-artist', label: 'Makeup Artist Management' }, { prefix: "/admin/makeup-artist", label: "Makeup Artist Management" },
{ prefix: '/admin/tutors', label: 'Tutors Management' }, { prefix: "/admin/tutors", label: "Tutors Management" },
{ prefix: '/admin/developers', label: 'Developers Management' }, { prefix: "/admin/developers", label: "Developers Management" },
{ prefix: '/admin/video-editors', label: 'Video Editor Management' }, { prefix: "/admin/video-editors", label: "Video Editor Management" },
{ prefix: '/admin/fitness-trainers', label: 'Fitness Trainer Management' }, { prefix: "/admin/fitness-trainers", label: "Fitness Trainer Management" },
{ prefix: '/admin/catering-services', label: 'Catering Services Management' }, { prefix: "/admin/catering-services", label: "Catering Services Management" },
{ prefix: '/admin/ugc-content-creators', label: 'UGC Content Creator Management' }, { prefix: "/admin/ugc-content-creators", label: "UGC Content Creator Management" },
{ prefix: '/admin/graphic-designers', label: 'Graphic Designer Management' }, { prefix: "/admin/graphic-designers", label: "Graphic Designer Management" },
{ prefix: '/admin/social-media-managers', label: 'Social Media Manager Management' }, { prefix: "/admin/social-media-managers", label: "Social Media Manager Management" },
{ prefix: '/admin/jobs', label: 'Jobs Management' }, { prefix: "/admin/jobs", label: "Jobs Management" },
{ prefix: '/admin/leads', label: 'Leads Management' }, { prefix: "/admin/leads", label: "Leads Management" },
{ prefix: '/admin/applications', label: 'Applications Management' }, { prefix: "/admin/applications", label: "Applications Management" },
{ prefix: '/admin/responses', label: 'Responses Management' }, { prefix: "/admin/responses", label: "Responses Management" },
{ prefix: '/admin/pricing', label: 'Pricing Management' }, { prefix: "/admin/pricing", label: "Pricing Management" },
{ prefix: '/admin/credit', label: 'Credit Management' }, { prefix: "/admin/credit", label: "Credit Management" },
{ prefix: '/admin/coupon', label: 'Coupon Management' }, { prefix: "/admin/coupon", label: "Coupon Management" },
{ prefix: '/admin/discount', label: 'Discount Management' }, { prefix: "/admin/discount", label: "Discount Management" },
{ prefix: '/admin/tax', label: 'Tax Management' }, { prefix: "/admin/tax", label: "Tax Management" },
{ prefix: '/admin/order', label: 'Order Management' }, { prefix: "/admin/order", label: "Order Management" },
{ prefix: '/admin/invoice', label: 'Invoice Management' }, { prefix: "/admin/invoice", label: "Invoice Management" },
{ prefix: '/admin/payment-gateway', label: 'Payment Gateway Management' }, { prefix: "/admin/payment-gateway", label: "Payment Gateway Management" },
{ prefix: '/admin/smtp', label: 'SMTP Management' }, { prefix: "/admin/smtp", label: "SMTP Management" },
{ prefix: '/admin/kb', label: 'Knowledge Base Management' }, { prefix: "/admin/kb", label: "Knowledge Base Management" },
{ prefix: '/admin/notifications', label: 'Notifications' }, { prefix: "/admin/notifications", label: "Notifications" },
{ prefix: '/admin/review', label: 'Review Management' }, { prefix: "/admin/review", label: "Review Management" },
{ prefix: '/admin/support', label: 'Support Management' }, { prefix: "/admin/support", label: "Support Management" },
{ prefix: '/admin/report', label: 'Report Management' }, { prefix: "/admin/report", label: "Report Management" },
{ prefix: '/admin/ledger', label: 'Ledger Management' }, { prefix: "/admin/ledger", label: "Ledger Management" },
]; ];
const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = []; const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [];
const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [ const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [
{ prefix: '/admin', keys: ['ADMIN_DASHBOARD', 'DASHBOARD'] }, { prefix: "/admin", keys: ["ADMIN_DASHBOARD", "DASHBOARD"] },
{ prefix: '/admin/department', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, { prefix: "/admin/department", keys: ["DEPARTMENT_MANAGEMENT", "DEPARTMENTS"] },
{ prefix: '/admin/department-management', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] }, { prefix: "/admin/department-management", keys: ["DEPARTMENT_MANAGEMENT", "DEPARTMENTS"] },
{ prefix: '/admin/designation', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] }, { prefix: "/admin/designation", keys: ["DESIGNATION_MANAGEMENT", "DESIGNATIONS"] },
{ prefix: '/admin/designation-management', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] }, { prefix: "/admin/designation-management", keys: ["DESIGNATION_MANAGEMENT", "DESIGNATIONS"] },
{ prefix: '/admin/roles', keys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] }, { prefix: "/admin/roles", keys: ["INTERNAL_ROLE_MANAGEMENT", "ROLES"] },
{ prefix: '/admin/employees', keys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] }, { prefix: "/admin/employees", keys: ["EMPLOYEE_MANAGEMENT", "EMPLOYEES"] },
{ prefix: '/admin/external-roles', keys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] }, { 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/internal-dashboard-management",
{ prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] }, keys: ["INTERNAL_DASHBOARD_MANAGEMENT", "INTERNAL_DASHBOARDS", "INTERNAL_DASHBOARD_CONFIG"],
{ prefix: '/admin/verification', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, },
{ prefix: '/admin/verification-status', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] }, {
{ prefix: '/admin/approval', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, prefix: "/admin/external-dashboard-management",
{ prefix: '/admin/approvals', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, keys: [
{ prefix: '/admin/approval-management', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] }, "DASHBOARD_CONFIG_MANAGEMENT",
{ prefix: '/admin/users', keys: ['USER_MANAGEMENT', 'USERS'] }, "EXTERNAL_DASHBOARD_MANAGEMENT",
{ prefix: '/admin/company', keys: ['COMPANY_MANAGEMENT', 'COMPANIES'] }, "EXTERNAL_DASHBOARDS",
{ prefix: '/admin/candidate', keys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] }, "EXTERNAL_DASHBOARD_CONFIG",
{ prefix: '/admin/customer', keys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] }, "RUNTIME_ROLES",
{ 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/role-ui-configs",
{ prefix: '/admin/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] }, keys: [
{ prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] }, "DASHBOARD_CONFIG_MANAGEMENT",
{ prefix: '/admin/catering-services', keys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] }, "EXTERNAL_DASHBOARD_MANAGEMENT",
{ prefix: '/admin/ugc-content-creator', keys: ['UGC_CONTENT_CREATOR_MANAGEMENT', 'UGC_CONTENT_CREATOR'] }, "EXTERNAL_DASHBOARDS",
{ prefix: '/admin/graphic-designers', keys: ['GRAPHIC_DESIGNER_MANAGEMENT', 'GRAPHIC_DESIGNERS'] }, "EXTERNAL_DASHBOARD_CONFIG",
{ prefix: '/admin/social-media-managers', keys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] }, "RUNTIME_ROLES",
{ 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/verification", keys: ["VERIFICATION_MANAGEMENT", "VERIFICATIONS"] },
{ prefix: '/admin/responses', keys: ['RESPONSES_MANAGEMENT', 'RESPONSES'] }, { prefix: "/admin/verification-status", keys: ["VERIFICATION_MANAGEMENT", "VERIFICATIONS"] },
{ prefix: '/admin/pricing', keys: ['PRICING_MANAGEMENT', 'PRICING'] }, { prefix: "/admin/approval", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
{ prefix: '/admin/credit', keys: ['CREDIT_MANAGEMENT', 'CREDITS'] }, { prefix: "/admin/approvals", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
{ prefix: '/admin/coupon', keys: ['COUPON_MANAGEMENT', 'COUPONS'] }, { prefix: "/admin/approval-management", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
{ prefix: '/admin/discount', keys: ['DISCOUNT_MANAGEMENT', 'DISCOUNTS'] }, { prefix: "/admin/users", keys: ["USER_MANAGEMENT", "USERS"] },
{ prefix: '/admin/tax', keys: ['TAX_MANAGEMENT', 'TAXES'] }, { prefix: "/admin/company", keys: ["COMPANY_MANAGEMENT", "COMPANIES"] },
{ prefix: '/admin/order', keys: ['ORDER_MANAGEMENT', 'ORDERS'] }, { prefix: "/admin/candidate", keys: ["CANDIDATE_MANAGEMENT", "CANDIDATES"] },
{ prefix: '/admin/invoice', keys: ['INVOICE_MANAGEMENT', 'INVOICES'] }, { prefix: "/admin/customer", keys: ["CUSTOMER_MANAGEMENT", "CUSTOMERS"] },
{ prefix: '/admin/payment-gateway', keys: ['PAYMENT_GATEWAY_MANAGEMENT', 'PAYMENT_GATEWAY'] }, { prefix: "/admin/photographer", keys: ["PHOTOGRAPHER_MANAGEMENT", "PHOTOGRAPHERS"] },
{ prefix: '/admin/smtp', keys: ['SMTP_MANAGEMENT', 'SMTP'] }, { prefix: "/admin/makeup-artist", keys: ["MAKEUP_ARTIST_MANAGEMENT", "MAKEUP_ARTISTS"] },
{ prefix: '/admin/kb', keys: ['KNOWLEDGE_BASE_MANAGEMENT', 'KNOWLEDGE_BASE', 'KB'] }, { prefix: "/admin/tutors", keys: ["TUTOR_MANAGEMENT", "TUTORS"] },
{ prefix: '/admin/notifications', keys: ['NOTIFICATIONS_MANAGEMENT', 'NOTIFICATIONS'] }, { prefix: "/admin/developers", keys: ["DEVELOPER_MANAGEMENT", "DEVELOPERS"] },
{ prefix: '/admin/review', keys: ['REVIEW_MANAGEMENT', 'REVIEWS'] }, { prefix: "/admin/video-editors", keys: ["VIDEO_EDITOR_MANAGEMENT", "VIDEO_EDITORS"] },
{ prefix: '/admin/support', keys: ['SUPPORT_MANAGEMENT', 'SUPPORT'] }, { prefix: "/admin/fitness-trainers", keys: ["FITNESS_TRAINER_MANAGEMENT", "FITNESS_TRAINERS"] },
{ prefix: '/admin/report', keys: ['REPORT_MANAGEMENT', 'REPORTS'] }, {
{ prefix: '/admin/ledger', keys: ['LEDGER', 'LEDGER_MANAGEMENT'] }, 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 = [ const SEARCH_MODULES = [
{ {
label: 'Users', label: "Users",
viewAllHref: '/admin/users', viewAllHref: "/admin/users",
api: '/api/admin/users', api: "/api/admin/users",
listKeys: ['users', 'items'], listKeys: ["users", "items"],
titleKeys: ['full_name', 'name'], titleKeys: ["full_name", "name"],
subtitleKeys: ['email', 'phone'], subtitleKeys: ["email", "phone"],
detailBase: '/admin/users', detailBase: "/admin/users",
}, },
{ {
label: 'Companies', label: "Companies",
viewAllHref: '/admin/company', viewAllHref: "/admin/company",
api: '/api/admin/companies', api: "/api/admin/companies",
listKeys: ['companies', 'items'], listKeys: ["companies", "items"],
titleKeys: ['name', 'companyName'], titleKeys: ["name", "companyName"],
subtitleKeys: ['email', 'phone'], subtitleKeys: ["email", "phone"],
detailBase: '/admin/company', detailBase: "/admin/company",
}, },
{ {
label: 'Employees', label: "Employees",
viewAllHref: '/admin/employees', viewAllHref: "/admin/employees",
api: '/api/admin/employees', api: "/api/admin/employees",
listKeys: ['employees', 'items'], listKeys: ["employees", "items"],
titleKeys: ['full_name', 'name'], titleKeys: ["full_name", "name"],
subtitleKeys: ['email', 'department_name'], subtitleKeys: ["email", "department_name"],
detailBase: '/admin/employees', detailBase: "/admin/employees",
}, },
{ {
label: 'Jobs', label: "Jobs",
viewAllHref: '/admin/jobs', viewAllHref: "/admin/jobs",
api: '/api/admin/jobs', api: "/api/admin/jobs",
listKeys: ['jobs', 'items'], listKeys: ["jobs", "items"],
titleKeys: ['title', 'name'], titleKeys: ["title", "name"],
subtitleKeys: ['status', 'company_name'], subtitleKeys: ["status", "company_name"],
detailBase: '/admin/jobs', detailBase: "/admin/jobs",
}, },
{ {
label: 'Leads', label: "Leads",
viewAllHref: '/admin/leads', viewAllHref: "/admin/leads",
api: '/api/admin/leads', api: "/api/admin/leads",
listKeys: ['leads', 'items'], listKeys: ["leads", "items"],
titleKeys: ['name', 'full_name'], titleKeys: ["name", "full_name"],
subtitleKeys: ['email', 'status'], subtitleKeys: ["email", "status"],
detailBase: '/admin/leads', detailBase: "/admin/leads",
}, },
]; ];
function pickStr(obj: Record<string, any>, keys: string[]): string { function pickStr(obj: Record<string, any>, keys: string[]): string {
for (const k of keys) if (obj[k]) return String(obj[k]); for (const k of keys) if (obj[k]) return String(obj[k]);
return '—'; return "—";
} }
function extractList(data: any, keys: string[]): any[] { function extractList(data: any, keys: string[]): any[] {
@ -176,7 +218,7 @@ function extractList(data: any, keys: string[]): any[] {
} }
function GlobalSearch() { function GlobalSearch() {
const [query, setQuery] = createSignal(''); const [query, setQuery] = createSignal("");
const [open, setOpen] = createSignal(false); const [open, setOpen] = createSignal(false);
const [groups, setGroups] = createSignal<SearchGroup[]>([]); const [groups, setGroups] = createSignal<SearchGroup[]>([]);
const [searching, setSearching] = createSignal(false); const [searching, setSearching] = createSignal(false);
@ -185,11 +227,17 @@ function GlobalSearch() {
const doSearch = async (q: string) => { const doSearch = async (q: string) => {
const trimmed = q.trim(); const trimmed = q.trim();
if (trimmed.length < 2) { setGroups([]); setOpen(false); return; } if (trimmed.length < 2) {
setGroups([]);
setOpen(false);
return;
}
setSearching(true); setSearching(true);
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
SEARCH_MODULES.map(async (mod) => { 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; if (!res?.ok) return null;
const data = await res.json().catch(() => null); const data = await res.json().catch(() => null);
if (!data) return null; if (!data) return null;
@ -205,9 +253,9 @@ function GlobalSearch() {
href: `${mod.detailBase}/${item.id}`, href: `${mod.detailBase}/${item.id}`,
})), })),
} satisfies SearchGroup; } satisfies SearchGroup;
}), })
); );
setGroups(settled.flatMap((r) => (r.status === 'fulfilled' && r.value ? [r.value] : []))); setGroups(settled.flatMap((r) => (r.status === "fulfilled" && r.value ? [r.value] : [])));
setOpen(true); setOpen(true);
setSearching(false); setSearching(false);
}; };
@ -215,26 +263,39 @@ function GlobalSearch() {
const handleInput = (val: string) => { const handleInput = (val: string) => {
setQuery(val); setQuery(val);
clearTimeout(timer); clearTimeout(timer);
if (val.trim().length < 2) { setGroups([]); setOpen(false); return; } if (val.trim().length < 2) {
setGroups([]);
setOpen(false);
return;
}
timer = setTimeout(() => doSearch(val), 350); timer = setTimeout(() => doSearch(val), 350);
}; };
const close = () => { setOpen(false); setQuery(''); setGroups([]); }; const close = () => {
const onOutside = (e: MouseEvent) => { if (!wrapRef.contains(e.target as Node)) setOpen(false); }; setOpen(false);
setQuery("");
setGroups([]);
};
const onOutside = (e: MouseEvent) => {
if (!wrapRef.contains(e.target as Node)) setOpen(false);
};
onMount(() => document.addEventListener('mousedown', onOutside)); onMount(() => document.addEventListener("mousedown", onOutside));
onCleanup(() => document.removeEventListener('mousedown', onOutside)); onCleanup(() => document.removeEventListener("mousedown", onOutside));
return ( return (
<div ref={wrapRef!} class="relative ml-10 w-[560px] shrink-0"> <div ref={wrapRef!} class="relative ml-10 w-[560px] shrink-0">
<Search size={20} class="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-[#9498ad]" /> <Search
size={20}
class="pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 text-[#9498ad]"
/>
<input <input
type="text" type="text"
value={query()} value={query()}
placeholder="Search system resources..." placeholder="Search system resources..."
onInput={(e) => handleInput(e.currentTarget.value)} onInput={(e) => handleInput(e.currentTarget.value)}
onFocus={() => groups().length > 0 && setOpen(true)} onFocus={() => groups().length > 0 && setOpen(true)}
onKeyDown={(e) => e.key === 'Escape' && close()} 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" 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"
/> />
@ -244,19 +305,35 @@ function GlobalSearch() {
{(group) => ( {(group) => (
<div class="border-b border-[#f1f2f5] px-4 py-3 last:border-b-0"> <div class="border-b border-[#f1f2f5] px-4 py-3 last:border-b-0">
<div class="mb-2 flex items-center justify-between"> <div class="mb-2 flex items-center justify-between">
<span class="text-[10px] font-bold uppercase tracking-[0.12em] text-[#9aa0b9]">{group.label}</span> <span class="text-[10px] font-bold uppercase tracking-[0.12em] text-[#9aa0b9]">
<A href={group.viewAllHref} onClick={close} class="text-[12px] font-semibold text-[#FF5E13]">View all</A> {group.label}
</span>
<A
href={group.viewAllHref}
onClick={close}
class="text-[12px] font-semibold text-[#FF5E13]"
>
View all
</A>
</div> </div>
<div class="space-y-1"> <div class="space-y-1">
<For each={group.results}> <For each={group.results}>
{(item) => ( {(item) => (
<A href={item.href} onClick={close} class="flex items-center gap-3 rounded-xl px-2 py-2 hover:bg-[#f9fafb]"> <A
href={item.href}
onClick={close}
class="flex items-center gap-3 rounded-xl px-2 py-2 hover:bg-[#f9fafb]"
>
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-[rgba(255,94,19,0.12)] text-[12px] font-bold text-[#FF5E13]"> <div class="flex h-8 w-8 items-center justify-center rounded-full bg-[rgba(255,94,19,0.12)] text-[12px] font-bold text-[#FF5E13]">
{item.title.trim().slice(0, 1).toUpperCase()} {item.title.trim().slice(0, 1).toUpperCase()}
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-[13px] font-semibold text-[#0D0D2A]">{item.title}</p> <p class="truncate text-[13px] font-semibold text-[#0D0D2A]">
<p class="truncate text-[12px] text-[rgba(13,13,42,0.55)]">{item.subtitle}</p> {item.title}
</p>
<p class="truncate text-[12px] text-[rgba(13,13,42,0.55)]">
{item.subtitle}
</p>
</div> </div>
</A> </A>
)} )}
@ -281,20 +358,27 @@ function ShowTabs(props: {
tabs: Tab[]; tabs: Tab[];
isTabActive: (tab: Tab) => boolean; isTabActive: (tab: Tab) => boolean;
setTabsTrackEl: (el: HTMLDivElement) => void; setTabsTrackEl: (el: HTMLDivElement) => void;
setTabRefs: (fn: (prev: Record<string, HTMLAnchorElement>) => Record<string, HTMLAnchorElement>) => void; setTabRefs: (
fn: (prev: Record<string, HTMLAnchorElement>) => Record<string, HTMLAnchorElement>
) => void;
tabIndicator: () => { left: number; width: number; ready: boolean }; tabIndicator: () => { left: number; width: number; ready: boolean };
}) { }) {
if (props.tabs.length === 0) return null; if (props.tabs.length === 0) return null;
return ( return (
<div ref={props.setTabsTrackEl} class="relative mb-6 mt-1 flex items-center gap-1 border-b border-[#e5e7eb]"> <div
ref={props.setTabsTrackEl}
class="relative mb-6 mt-1 flex items-center gap-1 border-b border-[#e5e7eb]"
>
<For each={props.tabs}> <For each={props.tabs}>
{(tab) => ( {(tab) => (
<A <A
href={tab.href} href={tab.href}
ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))} ref={(el) => props.setTabRefs((prev) => ({ ...prev, [tab.href]: el }))}
aria-current={props.isTabActive(tab) ? 'page' : undefined} aria-current={props.isTabActive(tab) ? "page" : undefined}
class={`px-4 pb-3 pt-3 text-[14px] font-semibold transition-colors ${ 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]' props.isTabActive(tab)
? "text-[#FF5E13]"
: "text-[rgba(13,13,42,0.6)] hover:text-[#0D0D2A]"
}`} }`}
> >
{tab.label} {tab.label}
@ -302,7 +386,7 @@ function ShowTabs(props: {
)} )}
</For> </For>
<div <div
class={`absolute bottom-0 h-[2px] bg-[#FF5E13] transition-all duration-300 ease-out ${props.tabIndicator().ready ? 'opacity-100' : 'opacity-0'}`} class={`absolute bottom-0 h-[2px] bg-[#FF5E13] transition-all duration-300 ease-out ${props.tabIndicator().ready ? "opacity-100" : "opacity-0"}`}
style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }} style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }}
/> />
</div> </div>
@ -314,14 +398,14 @@ export default function AdminShell(props: { children: JSX.Element }) {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [checkedSession, setCheckedSession] = createSignal(false); const [checkedSession, setCheckedSession] = createSignal(true);
const [adminName, setAdminName] = createSignal('Admin User'); const [adminName, setAdminName] = createSignal("Admin User");
const [allowedModules, setAllowedModules] = createSignal<string[] | null>(null); const [allowedModules, setAllowedModules] = createSignal<string[] | null>(null);
const [isSuperAdmin, setIsSuperAdmin] = createSignal(false); const [isSuperAdmin, setIsSuperAdmin] = createSignal(false);
const [sidebarOpen, setSidebarOpen] = createSignal(false); const [sidebarOpen, setSidebarOpen] = createSignal(false);
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false); const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
const [unreadCount, setUnreadCount] = createSignal(0); const [unreadCount, setUnreadCount] = createSignal(0);
const [theme, setTheme] = createSignal<'light' | 'dark'>('light'); const [theme, setTheme] = createSignal<"light" | "dark">("light");
const [routeTransitioning, setRouteTransitioning] = createSignal(false); const [routeTransitioning, setRouteTransitioning] = createSignal(false);
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>(); const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
@ -331,25 +415,26 @@ export default function AdminShell(props: { children: JSX.Element }) {
const logout = async () => { const logout = async () => {
try { try {
const accessToken = typeof sessionStorage !== 'undefined' const accessToken =
? sessionStorage.getItem('nxtgauge_admin_access_token') || '' typeof sessionStorage !== "undefined"
: ''; ? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
await fetch('/api/auth/logout', { : "";
method: 'POST', await fetch("/api/auth/logout", {
method: "POST",
headers: { headers: {
Accept: 'application/json', Accept: "application/json",
'x-portal-target': 'admin', "x-portal-target": "admin",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
}, },
credentials: 'include', credentials: "include",
}).catch(() => null); }).catch(() => null);
} finally { } finally {
if (typeof sessionStorage !== 'undefined') { if (typeof sessionStorage !== "undefined") {
sessionStorage.removeItem('nxtgauge_admin_access_token'); sessionStorage.removeItem("nxtgauge_admin_access_token");
sessionStorage.removeItem('nxtgauge_admin_preview'); sessionStorage.removeItem("nxtgauge_admin_preview");
} }
clearAdminSession(); clearAdminSession();
navigate('/login', { replace: true }); navigate("/login", { replace: true });
} }
}; };
@ -369,7 +454,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
const refreshTabIndicator = () => { const refreshTabIndicator = () => {
const activeTab = tabs().find((tab) => isTabActive(tab)); const activeTab = tabs().find((tab) => isTabActive(tab));
const track = tabsTrackEl(); const track = tabsTrackEl();
if (!activeTab || !track) { setTabIndicator((p) => ({ ...p, ready: false })); return; } if (!activeTab || !track) {
setTabIndicator((p) => ({ ...p, ready: false }));
return;
}
const el = tabRefs()[activeTab.href]; const el = tabRefs()[activeTab.href];
if (!el) return; if (!el) return;
setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true }); setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true });
@ -383,56 +471,47 @@ export default function AdminShell(props: { children: JSX.Element }) {
createEffect(() => { createEffect(() => {
location.pathname; location.pathname;
setRouteTransitioning(true); if (contentScrollRef) {
requestAnimationFrame(() => { contentScrollRef.scrollTop = 0;
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(() => { onMount(() => {
const savedTheme = (typeof localStorage !== 'undefined' const savedTheme = (
? localStorage.getItem('nxtgauge_admin_theme') typeof localStorage !== "undefined" ? localStorage.getItem("nxtgauge_admin_theme") : null
: null) as 'light' | 'dark' | null; ) as "light" | "dark" | null;
const nextTheme = savedTheme === 'dark' ? 'dark' : 'light'; const nextTheme = savedTheme === "dark" ? "dark" : "light";
setTheme(nextTheme); setTheme(nextTheme);
if (typeof document !== 'undefined') { if (typeof document !== "undefined") {
document.documentElement.setAttribute('data-theme', nextTheme); document.documentElement.setAttribute("data-theme", nextTheme);
} }
window.addEventListener('resize', refreshTabIndicator); window.addEventListener("resize", refreshTabIndicator);
onCleanup(() => window.removeEventListener('resize', refreshTabIndicator)); onCleanup(() => window.removeEventListener("resize", refreshTabIndicator));
// Fetch unread notification count and poll every 30 seconds // Fetch unread notification count and poll every 30 seconds
const fetchUnreadCount = async () => { const fetchUnreadCount = async () => {
try { try {
const accessToken = typeof sessionStorage !== 'undefined' const accessToken =
? sessionStorage.getItem('nxtgauge_admin_access_token') || '' typeof sessionStorage !== "undefined"
: ''; ? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
: "";
if (!accessToken) return; if (!accessToken) return;
const res = await fetch('/api/me/notifications/unread-count', { const res = await fetch("/api/me/notifications/unread-count", {
method: 'GET', method: "GET",
headers: { headers: {
Accept: 'application/json', Accept: "application/json",
'x-portal-target': 'admin', "x-portal-target": "admin",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
}, },
credentials: 'include', credentials: "include",
}); });
if (res.ok) { if (res.ok) {
const data = await res.json(); const data = await res.json();
setUnreadCount(data.unread_count || 0); setUnreadCount(data.unread_count || 0);
} }
} catch (e) { } catch (e) {
console.error('Failed to fetch unread count:', e); console.error("Failed to fetch unread count:", e);
} }
}; };
@ -440,11 +519,14 @@ export default function AdminShell(props: { children: JSX.Element }) {
const interval = setInterval(fetchUnreadCount, 30000); const interval = setInterval(fetchUnreadCount, 30000);
onCleanup(() => clearInterval(interval)); onCleanup(() => clearInterval(interval));
const isPreview = searchParams._preview === '1' || const isPreview =
(typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1'); searchParams._preview === "1" ||
(typeof sessionStorage !== "undefined" &&
sessionStorage.getItem("nxtgauge_admin_preview") === "1");
if (isPreview) { if (isPreview) {
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1'); if (typeof sessionStorage !== "undefined")
sessionStorage.setItem("nxtgauge_admin_preview", "1");
setAdminSession(); setAdminSession();
setCheckedSession(true); setCheckedSession(true);
return; return;
@ -452,52 +534,57 @@ export default function AdminShell(props: { children: JSX.Element }) {
const verify = async () => { const verify = async () => {
if (!hasAdminSession()) { if (!hasAdminSession()) {
navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, {
replace: true,
});
return; return;
} }
try { try {
const accessToken = typeof sessionStorage !== 'undefined' const accessToken =
? sessionStorage.getItem('nxtgauge_admin_access_token') || '' typeof sessionStorage !== "undefined"
: ''; ? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
const response = await fetch('/api/auth/session', { : "";
method: 'GET', const response = await fetch("/api/auth/session", {
method: "GET",
headers: { headers: {
Accept: 'application/json', Accept: "application/json",
'x-portal-target': 'admin', "x-portal-target": "admin",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
}, },
credentials: 'include', credentials: "include",
}); });
const payload = await response.json().catch(() => ({})); const payload = await response.json().catch(() => ({}));
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized'); if (!response.ok || isExternalIdentity(payload)) throw new Error("Unauthorized");
if (payload?.full_name) setAdminName(payload.full_name); if (payload?.full_name) setAdminName(payload.full_name);
const roleKey = String( const roleKey = String(
payload?.active_role payload?.active_role ||
|| payload?.role payload?.role ||
|| payload?.user?.active_role payload?.user?.active_role ||
|| payload?.user?.active_role_key payload?.user?.active_role_key ||
|| payload?.user?.role payload?.user?.role ||
|| payload?.user?.role_key payload?.user?.role_key ||
|| '', ""
).toUpperCase(); ).toUpperCase();
setIsSuperAdmin(roleKey === 'SUPER_ADMIN'); setIsSuperAdmin(roleKey === "SUPER_ADMIN");
try { try {
const res = await fetch('/api/runtime-config', { const res = await fetch("/api/runtime-config", {
method: 'GET', method: "GET",
headers: { headers: {
Accept: 'application/json', Accept: "application/json",
'x-portal-target': 'admin', "x-portal-target": "admin",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
}, },
credentials: 'include', credentials: "include",
}); });
const runtime = await res.json().catch(() => ({})); const runtime = await res.json().catch(() => ({}));
if (res.ok) { if (res.ok) {
setAllowedModules(normalizeAllowedModules(runtime)); setAllowedModules(normalizeAllowedModules(runtime));
const activeRole = String(runtime?.active_role || runtime?.user?.active_role || roleKey || '').toUpperCase(); const activeRole = String(
if (activeRole) setIsSuperAdmin(activeRole === 'SUPER_ADMIN'); runtime?.active_role || runtime?.user?.active_role || roleKey || ""
).toUpperCase();
if (activeRole) setIsSuperAdmin(activeRole === "SUPER_ADMIN");
} else { } else {
setAllowedModules(null); setAllowedModules(null);
} }
@ -508,7 +595,9 @@ export default function AdminShell(props: { children: JSX.Element }) {
setCheckedSession(true); setCheckedSession(true);
} catch { } catch {
clearAdminSession(); clearAdminSession();
navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true }); navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, {
replace: true,
});
} }
}; };
@ -518,29 +607,36 @@ export default function AdminShell(props: { children: JSX.Element }) {
const pageTitle = createMemo(() => { const pageTitle = createMemo(() => {
const path = location.pathname; const path = location.pathname;
for (const entry of PAGE_TITLES) { for (const entry of PAGE_TITLES) {
if (entry.exact ? path === entry.prefix : (path === entry.prefix || path.startsWith(`${entry.prefix}/`))) { if (
entry.exact
? path === entry.prefix
: path === entry.prefix || path.startsWith(`${entry.prefix}/`)
) {
return entry.label; return entry.label;
} }
} }
return 'Admin'; return "Admin";
}); });
const adminInitials = createMemo(() => { const adminInitials = createMemo(() => {
if (adminName().trim().toLowerCase() === 'admin user') return 'AD'; if (adminName().trim().toLowerCase() === "admin user") return "AD";
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean); const parts = adminName()
if (parts.length === 0) return 'U'; .split(" ")
.map((s) => s.trim())
.filter(Boolean);
if (parts.length === 0) return "U";
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase(); if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
}); });
createEffect(() => { createEffect(() => {
const t = theme(); const t = theme();
if (typeof localStorage !== 'undefined') localStorage.setItem('nxtgauge_admin_theme', t); if (typeof localStorage !== "undefined") localStorage.setItem("nxtgauge_admin_theme", t);
if (typeof document !== 'undefined') document.documentElement.setAttribute('data-theme', t); if (typeof document !== "undefined") document.documentElement.setAttribute("data-theme", t);
}); });
const toggleTheme = () => setTheme((v) => (v === 'dark' ? 'light' : 'dark')); const toggleTheme = () => setTheme((v) => (v === "dark" ? "light" : "dark"));
const isDark = () => theme() === 'dark'; const isDark = () => theme() === "dark";
createEffect(() => { createEffect(() => {
if (!checkedSession()) return; if (!checkedSession()) return;
@ -550,29 +646,53 @@ export default function AdminShell(props: { children: JSX.Element }) {
if (!modules || modules.length === 0) return; if (!modules || modules.length === 0) return;
const path = location.pathname; const path = location.pathname;
if (path === '/admin') return; if (path === "/admin") return;
const matches = ROUTE_MODULE_KEYS.filter( const matches = ROUTE_MODULE_KEYS.filter(
(entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`), (entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`)
); );
const guard = matches.sort((a, b) => b.prefix.length - a.prefix.length)[0]; const guard = matches.sort((a, b) => b.prefix.length - a.prefix.length)[0];
if (!guard) return; if (!guard) return;
const allowed = new Set(modules.map((m) => String(m || '').trim().toUpperCase()).filter(Boolean)); const allowed = new Set(
modules
.map((m) =>
String(m || "")
.trim()
.toUpperCase()
)
.filter(Boolean)
);
const ok = guard.keys.some((k) => allowed.has(String(k).toUpperCase())); const ok = guard.keys.some((k) => allowed.has(String(k).toUpperCase()));
if (ok) return; if (ok) return;
navigate(`/admin?denied=${encodeURIComponent(guard.keys[0])}&from=${encodeURIComponent(path)}`, { replace: true }); navigate(
`/admin?denied=${encodeURIComponent(guard.keys[0])}&from=${encodeURIComponent(path)}`,
{ replace: true }
);
}); });
return ( return (
<div class="min-h-screen" style={{ background: isDark() ? '#0B1220' : '#F9FAFB', color: isDark() ? '#E5E7EB' : '#0D0D2A' }}> <div
class="min-h-screen"
style={{
background: isDark() ? "#0B1220" : "#F9FAFB",
color: isDark() ? "#E5E7EB" : "#0D0D2A",
}}
>
<Show <Show
when={checkedSession()} when={checkedSession()}
fallback={<div class="flex min-h-screen items-center justify-center text-[14px] text-[rgba(13,13,42,0.55)]">Checking session</div>} fallback={
<div class="flex min-h-screen items-center justify-center text-[14px] text-[rgba(13,13,42,0.55)]">
Checking session
</div>
}
> >
<div style="display:flex;height:100vh;overflow:hidden"> <div style="display:flex;height:100vh;overflow:hidden">
<div class={`fixed inset-0 z-20 bg-black/30 transition-opacity lg:hidden ${sidebarOpen() ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'}`} onClick={() => setSidebarOpen(false)} /> <div
class={`fixed inset-0 z-20 bg-black/30 transition-opacity lg:hidden ${sidebarOpen() ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0"}`}
onClick={() => setSidebarOpen(false)}
/>
<div style="height:100%;display:flex;flex-shrink:0"> <div style="height:100%;display:flex;flex-shrink:0">
<AdminSidebar <AdminSidebar
@ -588,24 +708,43 @@ export default function AdminShell(props: { children: JSX.Element }) {
</div> </div>
<div class="flex min-w-0 flex-1 flex-col"> <div class="flex min-w-0 flex-1 flex-col">
<header style={`height:64px;border-bottom:1px solid ${isDark() ? '#1F2937' : '#E5E7EB'};background:${isDark() ? '#111827' : 'white'};flex-shrink:0`}> <header
style={`height:64px;border-bottom:1px solid ${isDark() ? "#1F2937" : "#E5E7EB"};background:${isDark() ? "#111827" : "white"};flex-shrink:0`}
>
<div style="display:flex;height:100%;width:100%;align-items:center;justify-content:flex-end;padding:0 32px"> <div style="display:flex;height:100%;width:100%;align-items:center;justify-content:flex-end;padding:0 32px">
<div style="display:flex;align-items:center;gap:4px"> <div style="display:flex;align-items:center;gap:4px">
<button type="button" onClick={toggleTheme} style={`display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? '#CBD5E1' : '#6B7280'};background:none;border:none;cursor:pointer`} aria-label="Toggle theme"> <button
type="button"
onClick={toggleTheme}
style={`display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? "#CBD5E1" : "#6B7280"};background:none;border:none;cursor:pointer`}
aria-label="Toggle theme"
>
<Show when={isDark()} fallback={<Moon size={18} />}> <Show when={isDark()} fallback={<Moon size={18} />}>
<Sun size={18} /> <Sun size={18} />
</Show> </Show>
</button> </button>
<button type="button" style={`position:relative;display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? '#CBD5E1' : '#6B7280'};background:none;border:none;cursor:pointer`} aria-label="Notifications"> <button
<Bell size={18} /> type="button"
<Show when={unreadCount() > 0}> style={`position:relative;display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? "#CBD5E1" : "#6B7280"};background:none;border:none;cursor:pointer`}
<span style={`position:absolute;right:8px;top:8px;width:7px;height:7px;border-radius:50%;border:2px solid ${isDark() ? '#111827' : 'white'};background:#FF5E13`} /> aria-label="Notifications"
</Show> >
</button> <Bell size={18} />
<button type="button" style={`display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? '#CBD5E1' : '#6B7280'};background:none;border:none;cursor:pointer`} aria-label="Settings"> <Show when={unreadCount() > 0}>
<span
style={`position:absolute;right:8px;top:8px;width:7px;height:7px;border-radius:50%;border:2px solid ${isDark() ? "#111827" : "white"};background:#FF5E13`}
/>
</Show>
</button>
<button
type="button"
style={`display:inline-flex;width:36px;height:36px;align-items:center;justify-content:center;border-radius:8px;color:${isDark() ? "#CBD5E1" : "#6B7280"};background:none;border:none;cursor:pointer`}
aria-label="Settings"
>
<Settings size={18} /> <Settings size={18} />
</button> </button>
<div style={`width:1px;height:24px;background:${isDark() ? '#1F2937' : '#E5E7EB'};margin:0 8px`} /> <div
style={`width:1px;height:24px;background:${isDark() ? "#1F2937" : "#E5E7EB"};margin:0 8px`}
/>
<button <button
type="button" type="button"
style="display:inline-flex;align-items:center;gap:8px;border-radius:8px;padding:4px 8px 4px 4px;background:none;border:none;cursor:pointer" style="display:inline-flex;align-items:center;gap:8px;border-radius:8px;padding:4px 8px 4px 4px;background:none;border:none;cursor:pointer"
@ -615,14 +754,22 @@ export default function AdminShell(props: { children: JSX.Element }) {
{adminInitials()} {adminInitials()}
</div> </div>
<div style="text-align:left"> <div style="text-align:left">
<p style={`font-size:13px;font-weight:600;color:${isDark() ? '#E5E7EB' : '#111827'};line-height:1.3`}>{adminName()}</p> <p
<p style={`font-size:11px;color:${isDark() ? '#94A3B8' : '#6B7280'};line-height:1.3`}>Super Admin</p> style={`font-size:13px;font-weight:600;color:${isDark() ? "#E5E7EB" : "#111827"};line-height:1.3`}
>
{adminName()}
</p>
<p
style={`font-size:11px;color:${isDark() ? "#94A3B8" : "#6B7280"};line-height:1.3`}
>
Super Admin
</p>
</div> </div>
</button> </button>
<button <button
type="button" type="button"
onClick={() => void logout()} onClick={() => void logout()}
style={`height:32px;border-radius:8px;border:1px solid ${isDark() ? '#374151' : '#E5E7EB'};background:${isDark() ? '#1F2937' : 'white'};padding:0 12px;font-size:12px;font-weight:600;color:${isDark() ? '#E5E7EB' : '#374151'};cursor:pointer`} style={`height:32px;border-radius:8px;border:1px solid ${isDark() ? "#374151" : "#E5E7EB"};background:${isDark() ? "#1F2937" : "white"};padding:0 12px;font-size:12px;font-weight:600;color:${isDark() ? "#E5E7EB" : "#374151"};cursor:pointer`}
> >
Logout Logout
</button> </button>
@ -631,18 +778,20 @@ export default function AdminShell(props: { children: JSX.Element }) {
</header> </header>
<div <div
ref={(el) => { contentScrollRef = el; }} ref={(el) => {
contentScrollRef = el;
}}
class="min-h-0 flex-1 overflow-y-scroll" class="min-h-0 flex-1 overflow-y-scroll"
style={{ background: isDark() ? '#0B1220' : '#F9FAFB', 'scrollbar-gutter': 'stable' }} style={{ background: isDark() ? "#0B1220" : "#F9FAFB", "scrollbar-gutter": "stable" }}
> >
<main <main
class="admin-main" class="admin-main"
style={{ style={{
width: '100%', width: "100%",
padding: '28px 24px 36px 24px', padding: "28px 24px 36px 24px",
filter: isDark() ? 'brightness(0.96)' : 'none', filter: isDark() ? "brightness(0.96)" : "none",
transition: 'opacity 150ms ease', transition: "opacity 150ms ease",
opacity: routeTransitioning() ? '0.92' : '1', opacity: routeTransitioning() ? "0.92" : "1",
}} }}
> >
{props.children} {props.children}

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,37 @@
import { A, useParams } from '@solidjs/router'; import { A, useParams } from "@solidjs/router";
import { createMemo } from 'solid-js'; import { createMemo, lazy } from "solid-js";
import ApprovalManagementPage from './approval';
import VerificationManagementPage from './verification'; const ApprovalManagementPage = lazy(() => import("./approval"));
import UsersManagementPage from './users'; const VerificationManagementPage = lazy(() => import("./verification"));
import ExternalDashboardManagementPage from './external-dashboard-management'; const UsersManagementPage = lazy(() => import("./users"));
import InternalDashboardManagementPage from './internal-dashboard-management'; const ExternalDashboardManagementPage = lazy(() => import("./external-dashboard-management"));
const InternalDashboardManagementPage = lazy(() => import("./internal-dashboard-management"));
function toTitle(value: string): string { function toTitle(value: string): string {
return value return value
.split(/[-_/]/g) .split(/[-_/]/g)
.filter(Boolean) .filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' '); .join(" ");
} }
const LEGACY_ADMIN_ORIGIN = import.meta.env.VITE_LEGACY_ADMIN_ORIGIN || 'http://localhost:9201'; const LEGACY_ADMIN_ORIGIN = import.meta.env.VITE_LEGACY_ADMIN_ORIGIN || "http://localhost:9201";
function resolveLegacyPath(modulePath: string): string { function resolveLegacyPath(modulePath: string): string {
switch (modulePath) { switch (modulePath) {
case 'roles': case "roles":
return '/roles?scope=internal'; return "/roles?scope=internal";
case 'approval-management': case "approval-management":
case 'approvals': case "approvals":
return '/approval'; return "/approval";
case 'onboarding-management': case "onboarding-management":
return '/external-dashboard-management'; return "/external-dashboard-management";
case 'internal-dashboard-management': case "internal-dashboard-management":
return '/internal-dashboard-management'; return "/internal-dashboard-management";
case 'external-dashboard-management': case "external-dashboard-management":
return '/external-dashboard-management'; return "/external-dashboard-management";
case 'support': case "support":
return '/help'; return "/help";
default: default:
return `/${modulePath}`; return `/${modulePath}`;
} }
@ -38,29 +39,42 @@ function resolveLegacyPath(modulePath: string): string {
export default function LegacyModuleShellPage() { export default function LegacyModuleShellPage() {
const params = useParams(); const params = useParams();
const modulePath = String((params as any).module || '').trim(); const modulePath = String((params as any).module || "").trim();
if (modulePath === 'approval' || modulePath === 'approval-management' || modulePath === 'approvals' || modulePath === 'approval-status') { if (
modulePath === "approval" ||
modulePath === "approval-management" ||
modulePath === "approvals" ||
modulePath === "approval-status"
) {
return <ApprovalManagementPage />; return <ApprovalManagementPage />;
} }
if (modulePath === 'verification' || modulePath === 'verification-status' || modulePath === 'verification-management') { if (
modulePath === "verification" ||
modulePath === "verification-status" ||
modulePath === "verification-management"
) {
return <VerificationManagementPage />; return <VerificationManagementPage />;
} }
if (modulePath === 'users' || modulePath === 'users-management' || modulePath === 'user-management') { if (
modulePath === "users" ||
modulePath === "users-management" ||
modulePath === "user-management"
) {
return <UsersManagementPage />; return <UsersManagementPage />;
} }
if (modulePath === 'external-dashboard-management' || modulePath === 'onboarding-management') { if (modulePath === "external-dashboard-management" || modulePath === "onboarding-management") {
return <ExternalDashboardManagementPage />; return <ExternalDashboardManagementPage />;
} }
if (modulePath === 'internal-dashboard-management') { if (modulePath === "internal-dashboard-management") {
return <InternalDashboardManagementPage />; return <InternalDashboardManagementPage />;
} }
const moduleName = createMemo(() => toTitle(modulePath || 'Management')); const moduleName = createMemo(() => toTitle(modulePath || "Management"));
const legacyPath = createMemo(() => resolveLegacyPath(modulePath)); const legacyPath = createMemo(() => resolveLegacyPath(modulePath));
const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`); const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`);
@ -72,12 +86,24 @@ export default function LegacyModuleShellPage() {
</p> </p>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm"> <section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="actions"> <div class="actions">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={legacyUrl()} target="_blank">Open Module In New Tab</A> <A
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
href={legacyUrl()}
target="_blank"
>
Open Module In New Tab
</A>
</div> </div>
<iframe <iframe
src={legacyUrl()} src={legacyUrl()}
title={`${moduleName()} (Legacy)`} title={`${moduleName()} (Legacy)`}
style={{ width: '100%', height: '72vh', border: '1px solid #e2e8f0', 'border-radius': '10px', 'margin-top': '10px' }} style={{
width: "100%",
height: "72vh",
border: "1px solid #e2e8f0",
"border-radius": "10px",
"margin-top": "10px",
}}
/> />
</section> </section>
</div> </div>

View file

@ -5,23 +5,11 @@ import type { CrudRecord } from "~/lib/admin/types";
const API = ""; const API = "";
type DepartmentRecord = CrudRecord & { type DepartmentRecord = CrudRecord & {
code?: string;
description?: string; description?: string;
totalEmployees?: number; totalEmployees?: number;
createdDate?: string; createdDate?: string;
departmentHead?: string;
departmentEmail?: string;
}; };
const permissionGroups = [
{
title: "Employee Management",
items: ["View Employees", "Create Employees", "Edit Employees", "Delete Employees"],
},
{ title: "Role Management", items: ["View Roles", "Assign Roles"] },
{ title: "Department Settings", items: ["Manage Department Settings"] },
];
type DepartmentListResponse = { type DepartmentListResponse = {
departments?: any[]; departments?: any[];
data?: any[]; data?: any[];
@ -36,11 +24,8 @@ function normalizeDepartment(item: any, idx: number): DepartmentRecord {
return { return {
id: String(item.id ?? `dep-${idx + 1}`), id: String(item.id ?? `dep-${idx + 1}`),
name: String(item.name ?? ""), name: String(item.name ?? ""),
code: item.code ? String(item.code) : undefined,
description: item.description ? String(item.description) : undefined, description: item.description ? String(item.description) : undefined,
totalEmployees: Number(item.total_employees ?? 0), totalEmployees: Number(item.total_employees ?? 0),
departmentHead: item.department_head ? String(item.department_head) : undefined,
departmentEmail: item.department_email ? String(item.department_email) : undefined,
status: isActive ? "ACTIVE" : "INACTIVE", status: isActive ? "ACTIVE" : "INACTIVE",
updatedAt: String(item.updated_at ?? ""), updatedAt: String(item.updated_at ?? ""),
createdDate: String(item.created_at ?? ""), createdDate: String(item.created_at ?? ""),
@ -92,7 +77,7 @@ export default function DepartmentManagementPage() {
const isPreview = () => searchParams._preview === "1"; const isPreview = () => searchParams._preview === "1";
const [view, setView] = createSignal<"list" | "form">("list"); const [view, setView] = createSignal<"list" | "form">("list");
const [formTab, setFormTab] = createSignal<"general" | "settings" | "permissions">("general"); const [formTab, setFormTab] = createSignal<"general" | "settings">("general");
const [listTab, setListTab] = createSignal<"all" | "create" | "view" | "inactive">("all"); const [listTab, setListTab] = createSignal<"all" | "create" | "view" | "inactive">("all");
const [search, setSearch] = createSignal(""); const [search, setSearch] = createSignal("");
const [statusFilter, setStatusFilter] = createSignal("all"); const [statusFilter, setStatusFilter] = createSignal("all");
@ -110,10 +95,7 @@ export default function DepartmentManagementPage() {
const [isDeleting, setIsDeleting] = createSignal(false); const [isDeleting, setIsDeleting] = createSignal(false);
const [name, setName] = createSignal(""); const [name, setName] = createSignal("");
const [code, setCode] = createSignal("");
const [description, setDescription] = createSignal(""); const [description, setDescription] = createSignal("");
const [departmentHead, setDepartmentHead] = createSignal("");
const [departmentEmail, setDepartmentEmail] = createSignal("");
const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE"); const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE");
const [isLoading, setIsLoading] = createSignal(false); const [isLoading, setIsLoading] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false);
@ -172,9 +154,6 @@ export default function DepartmentManagementPage() {
r = r.filter( r = r.filter(
(d) => (d) =>
d.name.toLowerCase().includes(q) || d.name.toLowerCase().includes(q) ||
String(d.code ?? "")
.toLowerCase()
.includes(q) ||
String(d.description ?? "") String(d.description ?? "")
.toLowerCase() .toLowerCase()
.includes(q) .includes(q)
@ -197,10 +176,7 @@ export default function DepartmentManagementPage() {
const resetForm = () => { const resetForm = () => {
setEditingId(null); setEditingId(null);
setName(""); setName("");
setCode("");
setDescription(""); setDescription("");
setDepartmentHead("");
setDepartmentEmail("");
setStatus("ACTIVE"); setStatus("ACTIVE");
setFormTab("general"); setFormTab("general");
setError(""); setError("");
@ -214,10 +190,7 @@ export default function DepartmentManagementPage() {
const openEdit = (row: DepartmentRecord) => { const openEdit = (row: DepartmentRecord) => {
setEditingId(row.id); setEditingId(row.id);
setName(row.name || ""); setName(row.name || "");
setCode(String(row.code || ""));
setDescription(String(row.description || "")); setDescription(String(row.description || ""));
setDepartmentHead(String(row.departmentHead || ""));
setDepartmentEmail(String(row.departmentEmail || ""));
setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE"); setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE");
setFormTab("general"); setFormTab("general");
setView("form"); setView("form");
@ -230,20 +203,12 @@ export default function DepartmentManagementPage() {
setFormTab("general"); setFormTab("general");
return; return;
} }
if (!code().trim()) {
setError("Department code is required.");
setFormTab("general");
return;
}
setIsSaving(true); setIsSaving(true);
setError(""); setError("");
const payload = { const payload = {
name: name().trim(), name: name().trim(),
code: code().trim() || null,
description: description().trim() || null, description: description().trim() || null,
department_head: departmentHead().trim() || null,
department_email: departmentEmail().trim() || null,
status: status(), status: status(),
}; };
@ -358,34 +323,8 @@ export default function DepartmentManagementPage() {
</div> </div>
<StatusBadge status={viewingDept()!.status} /> <StatusBadge status={viewingDept()!.status} />
</div> </div>
{/* Details grid — 3 cols using flex rows */} {/* Details grid — 2 cols using flex rows */}
<div> <div>
<div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Code
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.code || "—"}
</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Head
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.departmentHead || "—"}
</p>
</div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Email
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingDept()!.departmentEmail || "—"}
</p>
</div>
</div>
<div style="display:flex;border-bottom:1px solid #F3F4F6"> <div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6"> <div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF"> <p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
@ -561,9 +500,6 @@ export default function DepartmentManagementPage() {
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap"> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Department Name Department Name
</th> </th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Department Code
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap"> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Description Description
</th> </th>
@ -586,7 +522,7 @@ export default function DepartmentManagementPage() {
when={filteredRows().length > 0} when={filteredRows().length > 0}
fallback={ fallback={
<tr> <tr>
<td colspan="7" class="px-6 py-16 text-center"> <td colspan="6" class="px-6 py-16 text-center">
<p class="text-[15px] font-semibold text-[#111827]"> <p class="text-[15px] font-semibold text-[#111827]">
No departments found No departments found
</p> </p>
@ -613,11 +549,6 @@ export default function DepartmentManagementPage() {
<td style="padding:12px 20px"> <td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p> <p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td> </td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">
{String(row.code || "—")}
</span>
</td>
<td style="padding:12px 20px;max-width:340px"> <td style="padding:12px 20px;max-width:340px">
<p style="font-size:13px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"> <p style="font-size:13px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
{String(row.description || "—")} {String(row.description || "—")}
@ -891,8 +822,8 @@ export default function DepartmentManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden"> <div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */} {/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px"> <div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "settings", "permissions"] as const).map((tab, i) => { {(["general", "settings"] as const).map((tab, i) => {
const labels = ["General Information", "Department Settings", "Permissions"]; const labels = ["General Information", "Department Settings"];
const active = () => formTab() === tab; const active = () => formTab() === tab;
return ( return (
<button <button
@ -919,22 +850,13 @@ export default function DepartmentManagementPage() {
{/* General Information */} {/* General Information */}
<Show when={formTab() === "general"}> <Show when={formTab() === "general"}>
<div style="display:flex;flex-direction:column;gap:20px"> <div style="display:flex;flex-direction:column;gap:20px">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px"> <FormInput
<FormInput label="Department Name"
label="Department Name" required
required value={name()}
value={name()} onInput={setName}
onInput={setName} placeholder="e.g. Engineering"
placeholder="e.g. Engineering" />
/>
<FormInput
label="Department Code"
required
value={code()}
onInput={setCode}
placeholder="e.g. ENG-001"
/>
</div>
<label style="display:block"> <label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span> <span style="font-size:13px;font-weight:600;color:#374151">Description</span>
<textarea <textarea
@ -945,21 +867,6 @@ export default function DepartmentManagementPage() {
style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit" style="display:block;margin-top:6px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:10px 14px;font-size:13px;color:#111827;outline:none;resize:none;box-sizing:border-box;font-family:inherit"
/> />
</label> </label>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormInput
label="Department Head"
value={departmentHead()}
onInput={setDepartmentHead}
placeholder="e.g. Arun Kumar"
/>
<FormInput
label="Department Email"
type="email"
value={departmentEmail()}
onInput={setDepartmentEmail}
placeholder="dept@nxtgauge.com"
/>
</div>
</div> </div>
</Show> </Show>
@ -983,98 +890,6 @@ export default function DepartmentManagementPage() {
))} ))}
</div> </div>
</div> </div>
<div>
<p style="font-size:14px;font-weight:600;color:#111827">Department Visibility</p>
<p style="margin-top:2px;font-size:13px;color:#6B7280">
Choose who can see this department
</p>
<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px">
{[
{
key: "INTERNAL",
label: "Internal",
desc: "Visible to internal employees only",
},
{
key: "EXTERNAL",
label: "External",
desc: "Visible to external users and partners",
},
].map((opt) => (
<button
type="button"
onClick={() => setVisibility(opt.key as "INTERNAL" | "EXTERNAL")}
style={`display:flex;align-items:flex-start;gap:10px;border-radius:12px;border:1px solid ${visibility() === opt.key ? "#FF5E13" : "#E5E7EB"};background:${visibility() === opt.key ? "#FFF7ED" : "#F9FAFB"};padding:14px 16px;text-align:left;cursor:pointer`}
>
<div
style={`margin-top:2px;width:16px;height:16px;border-radius:50%;border:2px solid ${visibility() === opt.key ? "#FF5E13" : "#D1D5DB"};display:flex;align-items:center;justify-content:center;flex-shrink:0`}
>
<Show when={visibility() === opt.key}>
<div style="width:6px;height:6px;border-radius:50%;background:#FF5E13" />
</Show>
</div>
<div>
<p style="font-size:13px;font-weight:600;color:#111827">{opt.label}</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">{opt.desc}</p>
</div>
</button>
))}
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">
Allow Employee Transfers
</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">
Employees can request to transfer into this department
</p>
</div>
<button
type="button"
onClick={() => setTransfersEnabled((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${transfersEnabled() ? "#FF5E13" : "#E5E7EB"};transition:background 0.2s;flex-shrink:0`}
>
<span
style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${transfersEnabled() ? "22px" : "2px"}`}
/>
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === "permissions"}>
<div style="display:flex;flex-direction:column;gap:24px">
<p style="font-size:13px;color:#6B7280">
Select the permissions available to employees in this department.
</p>
<For each={permissionGroups}>
{(group) => (
<div>
<p style="margin-bottom:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#9CA3AF">
{group.title}
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<For each={group.items}>
{(item) => (
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input
type="checkbox"
style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer"
/>
<span style="font-size:13px;font-weight:500;color:#374151">
{item}
</span>
</label>
)}
</For>
</div>
</div>
)}
</For>
</div> </div>
</Show> </Show>
</div> </div>

View file

@ -1 +1,2 @@
export { default } from './designation'; import Designation from "./designation";
export default Designation;

View file

@ -5,28 +5,15 @@ import type { CrudRecord } from "~/lib/admin/types";
const API = ""; const API = "";
type DesignationRecord = CrudRecord & { type DesignationRecord = CrudRecord & {
code?: string;
department?: string; department?: string;
departmentId?: string; departmentId?: string;
level?: string;
description?: string; description?: string;
totalEmployees?: number; totalEmployees?: number;
createdDate?: string; createdDate?: string;
canManageTeam?: boolean;
canApprove?: boolean;
}; };
type DepartmentOption = { id: string; name: string }; type DepartmentOption = { id: string; name: string };
const permissionGroups = [
{
title: "Employee Management",
items: ["View Employees", "Create Employees", "Edit Employees", "Delete Employees"],
},
{ title: "Role Management", items: ["View Roles", "Assign Roles"] },
{ title: "Workflow Actions", items: ["Approve Requests", "Manage Team Members"] },
];
type DesignationListResponse = { type DesignationListResponse = {
designations?: any[]; designations?: any[];
data?: any[]; data?: any[];
@ -41,14 +28,10 @@ function normalizeDesignation(item: any, idx: number): DesignationRecord {
return { return {
id: String(item.id ?? `des-${idx + 1}`), id: String(item.id ?? `des-${idx + 1}`),
name: String(item.name ?? ""), name: String(item.name ?? ""),
code: item.code ? String(item.code) : undefined,
department: item.department_name ? String(item.department_name) : undefined, department: item.department_name ? String(item.department_name) : undefined,
departmentId: item.department_id ? String(item.department_id) : undefined, departmentId: item.department_id ? String(item.department_id) : undefined,
level: item.level ? String(item.level) : undefined,
description: item.description ? String(item.description) : undefined, description: item.description ? String(item.description) : undefined,
totalEmployees: Number(item.total_employees ?? 0), totalEmployees: Number(item.total_employees ?? 0),
canManageTeam: Boolean(item.can_manage_team ?? false),
canApprove: Boolean(item.can_approve ?? false),
status: isActive ? "ACTIVE" : "INACTIVE", status: isActive ? "ACTIVE" : "INACTIVE",
updatedAt: String(item.updated_at ?? ""), updatedAt: String(item.updated_at ?? ""),
createdDate: String(item.created_at ?? ""), createdDate: String(item.created_at ?? ""),
@ -123,7 +106,7 @@ export default function DesignationManagementPage() {
const isPreview = () => searchParams._preview === "1"; const isPreview = () => searchParams._preview === "1";
const [view, setView] = createSignal<"list" | "form">("list"); const [view, setView] = createSignal<"list" | "form">("list");
const [formTab, setFormTab] = createSignal<"general" | "settings" | "permissions">("general"); const [formTab, setFormTab] = createSignal<"general" | "settings">("general");
const [listTab, setListTab] = createSignal<"all" | "create" | "view">("all"); const [listTab, setListTab] = createSignal<"all" | "create" | "view">("all");
const [search, setSearch] = createSignal(""); const [search, setSearch] = createSignal("");
const [deptFilter, setDeptFilter] = createSignal("all"); const [deptFilter, setDeptFilter] = createSignal("all");
@ -142,13 +125,9 @@ export default function DesignationManagementPage() {
const [isDeleting, setIsDeleting] = createSignal(false); const [isDeleting, setIsDeleting] = createSignal(false);
const [name, setName] = createSignal(""); const [name, setName] = createSignal("");
const [code, setCode] = createSignal("");
const [departmentId, setDepartmentId] = createSignal(""); const [departmentId, setDepartmentId] = createSignal("");
const [level, setLevel] = createSignal("");
const [description, setDescription] = createSignal(""); const [description, setDescription] = createSignal("");
const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE"); const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE");
const [canManageTeam, setCanManageTeam] = createSignal(false);
const [canApprove, setCanApprove] = createSignal(false);
const [isLoading, setIsLoading] = createSignal(false); const [isLoading, setIsLoading] = createSignal(false);
const [isSaving, setIsSaving] = createSignal(false); const [isSaving, setIsSaving] = createSignal(false);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
@ -278,12 +257,10 @@ export default function DesignationManagementPage() {
}); });
const exportCsv = () => { const exportCsv = () => {
const headers = ["Designation Name", "Code", "Department", "Level", "Employees", "Status"]; const headers = ["Designation Name", "Department", "Employees", "Status"];
const rowsData = filteredRows().map((row) => [ const rowsData = filteredRows().map((row) => [
row.name || "", row.name || "",
row.code || "",
row.department || "", row.department || "",
row.level || "",
String(row.totalEmployees ?? 0), String(row.totalEmployees ?? 0),
row.status || "", row.status || "",
]); ]);
@ -305,13 +282,9 @@ export default function DesignationManagementPage() {
const resetForm = () => { const resetForm = () => {
setEditingId(null); setEditingId(null);
setName(""); setName("");
setCode("");
setDepartmentId(""); setDepartmentId("");
setLevel("");
setDescription(""); setDescription("");
setStatus("ACTIVE"); setStatus("ACTIVE");
setCanManageTeam(false);
setCanApprove(false);
setFormTab("general"); setFormTab("general");
setError(""); setError("");
}; };
@ -324,13 +297,9 @@ export default function DesignationManagementPage() {
const openEdit = (row: DesignationRecord) => { const openEdit = (row: DesignationRecord) => {
setEditingId(row.id); setEditingId(row.id);
setName(row.name || ""); setName(row.name || "");
setCode(row.code || "");
setDepartmentId(row.departmentId || ""); setDepartmentId(row.departmentId || "");
setLevel(row.level || "");
setDescription(row.description || ""); setDescription(row.description || "");
setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE"); setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE");
setCanManageTeam(Boolean(row.canManageTeam));
setCanApprove(Boolean(row.canApprove));
setFormTab("general"); setFormTab("general");
setView("form"); setView("form");
setOpenMenuId(null); setOpenMenuId(null);
@ -348,12 +317,8 @@ export default function DesignationManagementPage() {
setError(""); setError("");
const payload: Record<string, unknown> = { const payload: Record<string, unknown> = {
name: name().trim(), name: name().trim(),
code: code().trim() || null,
level: level().trim() || null,
description: description().trim() || null, description: description().trim() || null,
status: status(), status: status(),
can_manage_team: canManageTeam(),
can_approve: canApprove(),
}; };
if (departmentId().trim()) { if (departmentId().trim()) {
payload.department_id = departmentId().trim(); payload.department_id = departmentId().trim();
@ -481,14 +446,6 @@ export default function DesignationManagementPage() {
{/* Details grid */} {/* Details grid */}
<div> <div>
<div style="display:flex;border-bottom:1px solid #F3F4F6"> <div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Designation Code
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingRecord()!.code || "—"}
</p>
</div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6"> <div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF"> <p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Department Department
@ -497,14 +454,6 @@ export default function DesignationManagementPage() {
{viewingRecord()!.department || "—"} {viewingRecord()!.department || "—"}
</p> </p>
</div> </div>
<div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Level
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingRecord()!.level || "—"}
</p>
</div>
</div> </div>
<div style="display:flex;border-bottom:1px solid #F3F4F6"> <div style="display:flex;border-bottom:1px solid #F3F4F6">
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6"> <div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
@ -515,14 +464,6 @@ export default function DesignationManagementPage() {
{String(viewingRecord()!.totalEmployees ?? 0)} {String(viewingRecord()!.totalEmployees ?? 0)}
</p> </p>
</div> </div>
<div style="flex:1;padding:16px 24px;border-right:1px solid #F3F4F6">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Can Manage Team
</p>
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
{viewingRecord()!.canManageTeam ? "Yes" : "No"}
</p>
</div>
<div style="flex:1;padding:16px 24px"> <div style="flex:1;padding:16px 24px">
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF"> <p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
Created Date Created Date
@ -722,15 +663,9 @@ export default function DesignationManagementPage() {
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap"> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Designation Name Designation Name
</th> </th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Code
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap"> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Department Department
</th> </th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Level
</th>
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap"> <th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
Employees Employees
</th> </th>
@ -747,7 +682,7 @@ export default function DesignationManagementPage() {
when={filteredRows().length > 0} when={filteredRows().length > 0}
fallback={ fallback={
<tr> <tr>
<td colspan="7" class="px-6 py-16 text-center"> <td colspan="5" class="px-6 py-16 text-center">
<p class="text-[15px] font-semibold text-[#111827]"> <p class="text-[15px] font-semibold text-[#111827]">
No designations found No designations found
</p> </p>
@ -774,19 +709,9 @@ export default function DesignationManagementPage() {
<td style="padding:12px 20px"> <td style="padding:12px 20px">
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p> <p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
</td> </td>
<td style="padding:12px 20px">
<span style="font-size:12px;font-family:monospace;color:#6B7280">
{String(row.code || "—")}
</span>
</td>
<td style="padding:12px 20px;font-size:13px;color:#374151"> <td style="padding:12px 20px;font-size:13px;color:#374151">
{String(row.department || "—")} {String(row.department || "—")}
</td> </td>
<td style="padding:12px 20px">
<span style="display:inline-flex;border-radius:9999px;background:#EFF6FF;color:#2563EB;padding:2px 10px;font-size:12px;font-weight:500">
{String(row.level || "—")}
</span>
</td>
<td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827"> <td style="padding:12px 20px;font-size:14px;font-weight:600;color:#111827">
{Number(row.totalEmployees || 0)} {Number(row.totalEmployees || 0)}
</td> </td>
@ -1046,8 +971,8 @@ export default function DesignationManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden"> <div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */} {/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px"> <div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "settings", "permissions"] as const).map((tab, i) => { {(["general", "settings"] as const).map((tab, i) => {
const labels = ["General Information", "Designation Settings", "Permissions"]; const labels = ["General Information", "Designation Settings"];
const active = () => formTab() === tab; const active = () => formTab() === tab;
return ( return (
<button <button
@ -1082,34 +1007,10 @@ export default function DesignationManagementPage() {
onInput={setName} onInput={setName}
placeholder="e.g. Senior Software Engineer" placeholder="e.g. Senior Software Engineer"
/> />
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">
Designation Code
</span>
<input
type="text"
value={code()}
onInput={(e) => setCode(e.currentTarget.value)}
placeholder="e.g. SSE-001"
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
<FormSelect label="Department" value={departmentId()} onChange={setDepartmentId}> <FormSelect label="Department" value={departmentId()} onChange={setDepartmentId}>
<option value="">Select department</option> <option value="">Select department</option>
<For each={departments()}>{(d) => <option value={d.id}>{d.name}</option>}</For> <For each={departments()}>{(d) => <option value={d.id}>{d.name}</option>}</For>
</FormSelect> </FormSelect>
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Level</span>
<input
type="text"
value={level()}
onInput={(e) => setLevel(e.currentTarget.value)}
placeholder="e.g. Senior, Manager, Lead"
style="display:block;margin-top:6px;height:40px;width:100%;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:13px;color:#111827;outline:none;box-sizing:border-box"
/>
</label>
</div> </div>
<label style="display:block"> <label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Description</span> <span style="font-size:13px;font-weight:600;color:#374151">Description</span>
@ -1144,75 +1045,6 @@ export default function DesignationManagementPage() {
))} ))}
</div> </div>
</div> </div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Manage Team</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">
This designation can manage team members
</p>
</div>
<button
type="button"
onClick={() => setCanManageTeam((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canManageTeam() ? "#FF5E13" : "#E5E7EB"};transition:background 0.2s;flex-shrink:0`}
>
<span
style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canManageTeam() ? "22px" : "2px"}`}
/>
</button>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;border-radius:12px;border:1px solid #E5E7EB;background:#F9FAFB;padding:14px 16px">
<div>
<p style="font-size:13px;font-weight:600;color:#111827">Can Approve Requests</p>
<p style="margin-top:2px;font-size:12px;color:#6B7280">
This designation can approve employee requests
</p>
</div>
<button
type="button"
onClick={() => setCanApprove((v) => !v)}
style={`position:relative;width:44px;height:24px;border-radius:12px;border:none;cursor:pointer;background:${canApprove() ? "#FF5E13" : "#E5E7EB"};transition:background 0.2s;flex-shrink:0`}
>
<span
style={`position:absolute;top:2px;width:20px;height:20px;border-radius:50%;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.2);transition:left 0.2s;left:${canApprove() ? "22px" : "2px"}`}
/>
</button>
</div>
</div>
</Show>
{/* Permissions */}
<Show when={formTab() === "permissions"}>
<div style="display:flex;flex-direction:column;gap:24px">
<p style="font-size:13px;color:#6B7280">
Select the permissions available to employees with this designation.
</p>
<For each={permissionGroups}>
{(group) => (
<div>
<p style="margin-bottom:10px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#9CA3AF">
{group.title}
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<For each={group.items}>
{(item) => (
<label style="display:flex;align-items:center;gap:10px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px 14px;cursor:pointer">
<input
type="checkbox"
style="width:14px;height:14px;accent-color:#FF5E13;cursor:pointer"
/>
<span style="font-size:13px;font-weight:500;color:#374151">
{item}
</span>
</label>
)}
</For>
</div>
</div>
)}
</For>
</div> </div>
</Show> </Show>
</div> </div>

View file

@ -90,12 +90,15 @@ export default function CreateEmployeePage() {
const [depts] = createResource(fetchDepts); const [depts] = createResource(fetchDepts);
const [desigs] = createResource(fetchDesigs); const [desigs] = createResource(fetchDesigs);
const [fullName, setFullName] = createSignal(""); const [firstName, setFirstName] = createSignal("");
const [lastName, setLastName] = createSignal("");
const [email, setEmail] = createSignal(""); const [email, setEmail] = createSignal("");
const [employeeCode, setEmployeeCode] = createSignal(""); const [employeeCode, setEmployeeCode] = createSignal("");
const [createLoginCreds, setCreateLoginCreds] = createSignal(true); const [createLoginCreds, setCreateLoginCreds] = createSignal(true);
const [loginPassword, setLoginPassword] = createSignal(""); const [loginPassword, setLoginPassword] = createSignal("");
const [confirmLoginPassword, setConfirmLoginPassword] = createSignal(""); const [confirmLoginPassword, setConfirmLoginPassword] = createSignal("");
const [showLoginPassword, setShowLoginPassword] = createSignal(false);
const [showConfirmPassword, setShowConfirmPassword] = createSignal(false);
const [roleId, setRoleId] = createSignal(""); const [roleId, setRoleId] = createSignal("");
const [deptId, setDeptId] = createSignal(""); const [deptId, setDeptId] = createSignal("");
const [desigId, setDesigId] = createSignal(""); const [desigId, setDesigId] = createSignal("");
@ -157,8 +160,12 @@ export default function CreateEmployeePage() {
const handleSave = async (e: Event) => { const handleSave = async (e: Event) => {
e.preventDefault(); e.preventDefault();
if (!fullName().trim()) { if (!firstName().trim()) {
setError("Full name is required"); setError("First name is required");
return;
}
if (!lastName().trim()) {
setError("Last name is required");
return; return;
} }
if (!email().trim()) { if (!email().trim()) {
@ -204,8 +211,8 @@ export default function CreateEmployeePage() {
credentials: "include", credentials: "include",
body: JSON.stringify({ body: JSON.stringify({
email: email().trim(), email: email().trim(),
first_name: fullName().trim().split(" ")[0] || "", first_name: firstName().trim(),
last_name: fullName().trim().split(" ").slice(1).join(" ") || "", last_name: lastName().trim(),
role_code: roleId(), role_code: roleId(),
department_id: deptId().trim(), department_id: deptId().trim(),
designation_id: desigId().trim(), designation_id: desigId().trim(),
@ -260,17 +267,33 @@ export default function CreateEmployeePage() {
<form onSubmit={handleSave} class="p-6 space-y-5"> <form onSubmit={handleSave} class="p-6 space-y-5">
<div class="grid grid-cols-2 gap-5"> <div class="grid grid-cols-2 gap-5">
{/* Full Name */} {/* First Name */}
<div> <div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5"> <label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Full Name <span class="text-red-500">*</span> First Name <span class="text-red-500">*</span>
</label> </label>
<input <input
type="text" type="text"
required required
placeholder="e.g. Arjun Sharma" placeholder="e.g. Arjun"
value={fullName()} value={firstName()}
onInput={(e) => setFullName(e.currentTarget.value)} onInput={(e) => setFirstName(e.currentTarget.value)}
maxlength="100"
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/>
</div>
{/* Last Name */}
<div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Last Name <span class="text-red-500">*</span>
</label>
<input
type="text"
required
placeholder="e.g. Sharma"
value={lastName()}
onInput={(e) => setLastName(e.currentTarget.value)}
maxlength="100" maxlength="100"
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]" class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/> />
@ -328,25 +351,97 @@ export default function CreateEmployeePage() {
<label class="block text-[13px] font-medium text-[#111827] mb-1.5"> <label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Login Password <span class="text-red-500">*</span> Login Password <span class="text-red-500">*</span>
</label> </label>
<input <div style="position:relative">
type="password" <input
value={loginPassword()} type={showLoginPassword() ? "text" : "password"}
onInput={(e) => setLoginPassword(e.currentTarget.value)} value={loginPassword()}
placeholder="Minimum 8 characters" onInput={(e) => setLoginPassword(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]" placeholder="Minimum 8 characters"
/> class="w-full px-3 py-2.5 pr-10 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/>
<button
type="button"
onClick={() => setShowLoginPassword((v) => !v)}
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:#6B7280;padding:4px"
>
<Show
when={showLoginPassword()}
fallback={
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
</Show>
</button>
</div>
</div> </div>
<div> <div>
<label class="block text-[13px] font-medium text-[#111827] mb-1.5"> <label class="block text-[13px] font-medium text-[#111827] mb-1.5">
Confirm Password <span class="text-red-500">*</span> Confirm Password <span class="text-red-500">*</span>
</label> </label>
<input <div style="position:relative">
type="password" <input
value={confirmLoginPassword()} type={showConfirmPassword() ? "text" : "password"}
onInput={(e) => setConfirmLoginPassword(e.currentTarget.value)} value={confirmLoginPassword()}
placeholder="Repeat password" onInput={(e) => setConfirmLoginPassword(e.currentTarget.value)}
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]" placeholder="Repeat password"
/> class="w-full px-3 py-2.5 pr-10 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] text-[#111827] placeholder-[#9CA3AF]"
/>
<button
type="button"
onClick={() => setShowConfirmPassword((v) => !v)}
style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:#6B7280;padding:4px"
>
<Show
when={showConfirmPassword()}
fallback={
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
<line x1="1" y1="1" x2="23" y2="23" />
</svg>
</Show>
</button>
</div>
</div> </div>
</> </>
</Show> </Show>
@ -362,7 +457,7 @@ export default function CreateEmployeePage() {
class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#111827]" class="w-full px-3 py-2.5 text-[13px] border border-[#E5E7EB] rounded-lg outline-none focus:border-[#FF5E13] focus:ring-1 focus:ring-[#FF5E13] bg-white text-[#111827]"
> >
<option value="">Select role</option> <option value="">Select role</option>
<For each={roles() ?? []}>{(r) => <option value={r.id}>{r.name}</option>}</For> <For each={roles() ?? []}>{(r) => <option value={r.key}>{r.name}</option>}</For>
</select> </select>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,27 @@ const API = "";
type RoleOption = { id: string; key: string; name: string }; type RoleOption = { id: string; key: string; name: string };
type Module = {
id: string;
module_key: string;
module_name: string;
category: string;
default_sidebar_label: string;
default_route: string;
icon_key: string;
};
type RolePermission = {
module_key: string;
module_name: string;
category: string;
can_view: boolean;
can_list: boolean;
can_create: boolean;
can_update: boolean;
can_delete: boolean;
};
type ExternalDashboard = { type ExternalDashboard = {
id: string; id: string;
roleId: string; roleId: string;
@ -18,19 +39,99 @@ type ExternalDashboard = {
previewPath: string; previewPath: string;
status: "ACTIVE" | "INACTIVE" | "DRAFT"; status: "ACTIVE" | "INACTIVE" | "DRAFT";
updatedAt: string; updatedAt: string;
pkgCardColors?: Record<string, string>;
}; };
const AVAILABLE_WIDGETS = [ const ROLE_WIDGETS: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER", string[]> = {
"kpi_summary", CUSTOMER: [
"pending_approvals", "total_requirements",
"user_growth", "open_requirements",
"active_sessions", "closed_requirements",
"system_health", "responses_received",
"recent_activity", "shortlisted_responses",
"quick_actions", "credits",
"team_performance", ],
COMPANY: [
"total_jobs",
"active_jobs",
"pending_jobs",
"applications_received",
"shortlisted_candidates",
"credits",
],
JOB_SEEKER: [
"available_jobs",
"my_applications",
"shortlisted",
"saved_jobs",
"profile_status",
"portfolio",
],
PROFESSIONAL: [
"open_leads",
"my_requests",
"accepted_requests",
"tracecoins",
"portfolio",
],
};
const ROLE_PORTFOLIO_TABS: Record<string, string[]> = {
PHOTOGRAPHER: ["about", "services_pricing", "portfolio_gallery", "experience_equipment", "testimonials", "faqs"],
MAKEUP_ARTIST: ["about", "services_pricing", "gallery", "experience_certifications", "testimonials", "faqs"],
TUTOR: ["about", "subjects_pricing", "student_work", "qualifications", "testimonials", "faqs"],
DEVELOPER: ["about", "services_pricing", "projects", "tech_stack_experience", "testimonials", "faqs"],
VIDEO_EDITOR: ["about", "services_pricing", "showreel", "experience_tools", "testimonials", "faqs"],
UGC_CONTENT_CREATOR: ["about", "services_pricing", "content_portfolio", "experience_tools", "testimonials", "faqs"],
GRAPHIC_DESIGNER: ["about", "services_pricing", "portfolio_gallery", "experience_tools", "testimonials", "faqs"],
SOCIAL_MEDIA_MANAGER: ["about", "services_pricing", "case_studies", "experience_tools", "testimonials", "faqs"],
FITNESS_TRAINER: ["about", "training_plans", "client_results", "certifications", "testimonials", "faqs"],
CATERING_SERVICES: ["about", "packages_pricing", "gallery", "experience_certifications", "testimonials", "faqs"],
JOB_SEEKER: ["about", "education", "work_experience", "skills_certifications", "projects"],
};
const COMMON_PROFILE_FIELDS = [
"full_name",
"email",
"phone",
"location",
"verification_status",
"approval_status",
];
const AVAILABLE_TABS = [
"overview",
"basic_information",
"documents",
"about",
"services_pricing",
"portfolio_gallery",
"gallery",
"projects",
"showreel",
"content_portfolio",
"case_studies",
"experience_equipment",
"experience_tools",
"experience_certifications",
"qualifications",
"tech_stack_experience",
"training_plans",
"client_results",
"certifications",
"student_work",
"subjects_pricing",
"packages_pricing",
"education",
"work_experience",
"skills_certifications",
"testimonials",
"faqs",
"approvals",
"users",
"reports",
"audit_logs",
"settings",
]; ];
const AVAILABLE_TABS = ["overview", "approvals", "users", "reports", "audit_logs", "settings"];
const ROLE_BASED_SIDEBAR: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER", string[]> = const ROLE_BASED_SIDEBAR: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER", string[]> =
{ {
PROFESSIONAL: [ PROFESSIONAL: [
@ -44,7 +145,6 @@ const ROLE_BASED_SIDEBAR: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CU
"Verification", "Verification",
"Help Center", "Help Center",
"Settings", "Settings",
"Switch Services",
"Logout", "Logout",
], ],
COMPANY: [ COMPANY: [
@ -193,6 +293,23 @@ function rolePreviewPath(roleKey: string): string {
return "/signup"; return "/signup";
} }
function defaultTabsForRole(roleKey: string): string[] {
const key = String(roleKey || "").toUpperCase();
if (!key) return ["overview"];
if (ROLE_PORTFOLIO_TABS[key]) return ROLE_PORTFOLIO_TABS[key];
if (key.includes("COMPANY")) return ["overview", "jobs", "applications", "shortlisted_candidates"];
if (key.includes("CUSTOMER") || key.includes("SERVICE_SEEKER")) return ["overview", "my_requirements", "received_responses", "shortlisted_responses"];
return ["overview"];
}
function defaultFieldsForRole(roleKey: string): string[] {
const key = String(roleKey || "").toUpperCase();
if (key.includes("COMPANY")) return [...COMMON_PROFILE_FIELDS, "company_name"];
if (key.includes("CUSTOMER") || key.includes("SERVICE_SEEKER")) return [...COMMON_PROFILE_FIELDS, "service_preferences"];
if (key.includes("JOB_SEEKER") || key.includes("JOBSEEKER")) return [...COMMON_PROFILE_FIELDS, "work_experience", "skills"];
return [...COMMON_PROFILE_FIELDS, "service_categories", "experience_years"];
}
function asStringArray(value: unknown): string[] { function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return []; if (!Array.isArray(value)) return [];
return value.map((item) => String(item || "").trim()).filter(Boolean); return value.map((item) => String(item || "").trim()).filter(Boolean);
@ -209,6 +326,7 @@ function normalizeDashboard(item: any): ExternalDashboard {
const sidebarItems = asStringArray(cfg?.sidebar_items ?? cfg?.sidebarItems); const sidebarItems = asStringArray(cfg?.sidebar_items ?? cfg?.sidebarItems);
const fields = asStringArray(cfg?.fields); const fields = asStringArray(cfg?.fields);
const previewPath = String(cfg?.preview_path || cfg?.previewPath || "").trim(); const previewPath = String(cfg?.preview_path || cfg?.previewPath || "").trim();
const pkgCardColors = cfg?.pkg_card && typeof cfg.pkg_card === "object" ? cfg.pkg_card : undefined;
const isInactive = const isInactive =
item?.is_active === false || String(item?.status || "").toUpperCase() === "INACTIVE"; item?.is_active === false || String(item?.status || "").toUpperCase() === "INACTIVE";
@ -227,6 +345,7 @@ function normalizeDashboard(item: any): ExternalDashboard {
previewPath, previewPath,
status: isInactive ? "INACTIVE" : isDraft ? "DRAFT" : "ACTIVE", status: isInactive ? "INACTIVE" : isDraft ? "DRAFT" : "ACTIVE",
updatedAt: String(item?.updated_at || ""), updatedAt: String(item?.updated_at || ""),
pkgCardColors,
}; };
} }
@ -263,7 +382,7 @@ export default function ExternalDashboardManagementPage() {
const [view, setView] = createSignal<"list" | "form">("list"); const [view, setView] = createSignal<"list" | "form">("list");
const [editingId, setEditingId] = createSignal<string | null>(null); const [editingId, setEditingId] = createSignal<string | null>(null);
const [formTab, setFormTab] = createSignal< const [formTab, setFormTab] = createSignal<
"general" | "tabs" | "sidebar" | "fields" | "preview" | "full_preview" "general" | "tabs" | "sidebar" | "fields" | "theme" | "preview" | "full_preview"
>("general"); >("general");
const [isFullscreenPreview, setIsFullscreenPreview] = createSignal(false); const [isFullscreenPreview, setIsFullscreenPreview] = createSignal(false);
const [listTab, setListTab] = createSignal<"all" | "create">("all"); const [listTab, setListTab] = createSignal<"all" | "create">("all");
@ -290,6 +409,22 @@ export default function ExternalDashboardManagementPage() {
const [activePreviewSidebar, setActivePreviewSidebar] = createSignal(""); const [activePreviewSidebar, setActivePreviewSidebar] = createSignal("");
const [activePreviewTab, setActivePreviewTab] = createSignal(""); const [activePreviewTab, setActivePreviewTab] = createSignal("");
type PkgCardColors = {
white?: string;
coin_bg?: string;
coin_avatar_bg?: string;
best_value_bg?: string;
border_default?: string;
text_secondary?: string;
text_primary?: string;
text_accent?: string;
text_muted?: string;
text_success?: string;
shadow_default?: string;
shadow_accent?: string;
};
const [pkgCardColors, setPkgCardColors] = createSignal<PkgCardColors>({});
const rolePersonaById = createMemo(() => { const rolePersonaById = createMemo(() => {
const map: Record<string, "PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER"> = {}; const map: Record<string, "PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER"> = {};
for (const role of roles()) { for (const role of roles()) {
@ -314,6 +449,11 @@ export default function ExternalDashboardManagementPage() {
return "PROFESSIONAL"; // photographer, makeup, tutor, developer, video, graphic, social, fitness, catering, etc. return "PROFESSIONAL"; // photographer, makeup, tutor, developer, video, graphic, social, fitness, catering, etc.
}; };
const availableWidgetOptions = createMemo(() => {
const persona = personaFromKey(formRoleKey()) || "PROFESSIONAL";
return ROLE_WIDGETS[persona] || [];
});
const sidebarLooksCustomer = createMemo(() => { const sidebarLooksCustomer = createMemo(() => {
const joined = sidebarItems().join(" ").toLowerCase(); const joined = sidebarItems().join(" ").toLowerCase();
return ( return (
@ -340,10 +480,17 @@ export default function ExternalDashboardManagementPage() {
: sidebarLooksCustomer() : sidebarLooksCustomer()
? ROLE_BASED_SIDEBAR.CUSTOMER ? ROLE_BASED_SIDEBAR.CUSTOMER
: ["My Dashboard", "My Profile", "Switch Services", "Logout"]; : ["My Dashboard", "My Profile", "Switch Services", "Logout"];
return mergeSidebarForPersona( const merged = mergeSidebarForPersona(
baseItems, baseItems,
persona ?? (sidebarLooksCustomer() ? "CUSTOMER" : null) persona ?? (sidebarLooksCustomer() ? "CUSTOMER" : null)
); );
// Deduplicate by normalized key to prevent duplicates from any merge path
const seen = new Map<string, string>();
for (const item of merged) {
const key = normalizeToken(item);
if (!seen.has(key)) seen.set(key, item);
}
return Array.from(seen.values());
}); });
const previewTabs = createMemo(() => (tabs().length ? tabs() : ["overview"])); const previewTabs = createMemo(() => (tabs().length ? tabs() : ["overview"]));
@ -400,120 +547,111 @@ export default function ExternalDashboardManagementPage() {
setPreviewPath(rolePreviewPath(role.key)); setPreviewPath(rolePreviewPath(role.key));
}; };
const applyTabsPresetForRole = (selectedRoleId: string, force = false) => {
const role = roles().find((r) => r.id === selectedRoleId);
if (!role) return;
if (!force && tabs().length > 0) return;
setTabs(defaultTabsForRole(role.key));
};
const applyFieldsPresetForRole = (selectedRoleId: string, force = false) => {
const role = roles().find((r) => r.id === selectedRoleId);
if (!role) return;
if (!force && fields().length > 0) return;
setFields(defaultFieldsForRole(role.key));
};
const loadAll = async () => { const loadAll = async () => {
setLoading(true); setLoading(true);
setError(""); setError("");
try { try {
const [dashRes, rolesRes] = await Promise.all([ const [rolesRes, modulesRes] = await Promise.all([
fetch(`${API}/api/admin/dashboard-config`, { fetch(`${API}/api/admin/external-roles?per_page=200`, {
headers: authHeaders(), headers: authHeaders(),
credentials: "include", credentials: "include",
}), }),
fetch(`${API}/api/admin/roles?audience=EXTERNAL&per_page=200`, { fetch(`${API}/api/admin/modules`, {
headers: authHeaders(), headers: authHeaders(),
credentials: "include", credentials: "include",
}), }),
]); ]);
if (!dashRes.ok) throw new Error(`Failed to load dashboard config (${dashRes.status})`); if (!rolesRes.ok) throw new Error(`Failed to load roles (${rolesRes.status})`);
if (!rolesRes.ok) throw new Error(`Failed to load external roles (${rolesRes.status})`); if (!modulesRes.ok) throw new Error(`Failed to load modules (${modulesRes.status})`);
const dashData = await dashRes.json().catch(() => []);
const roleData = await rolesRes.json().catch(() => []); const roleData = await rolesRes.json().catch(() => []);
if (looksLikeGateway404(dashData) || looksLikeGateway404(roleData)) { const modulesData = await modulesRes.json().catch(() => []);
throw new Error("Required admin runtime endpoints are unavailable.");
}
const dashRows = Array.isArray(dashData) const roleRows = roleData?.roles || roleData?.items || roleData || [];
? dashData const modules: Module[] = modulesData?.items || modulesData || [];
: dashData?.items || dashData?.configs || [];
const roleRows = Array.isArray(roleData) const moduleByKey = new Map(modules.map((m: Module) => [m.module_key, m]));
? roleData
: roleData?.roles || roleData?.items || [];
setRoles( setRoles(
roleRows roleRows
.filter((r: any) => { .filter((r: any) => {
const hasRoleShape = Boolean(r?.id && (r?.key || r?.name)); return r?.status === "ACTIVE" || r?.status === "INACTIVE";
if (hasRoleShape && r?.audience == null) return true;
return String(r?.audience || "").toUpperCase() === "EXTERNAL";
}) })
.map((r: any) => ({ .map((r: any) => ({
id: String(r?.id || ""), id: String(r?.id || ""),
key: String(r?.key || "").toUpperCase(), key: String(r?.code || r?.key || "").toUpperCase(),
name: String(r?.name || r?.key || "External Role"), name: String(r?.name || r?.key || "External Role"),
})) }))
.filter((r: RoleOption) => r.id) .filter((r: RoleOption) => r.id)
); );
const roleKeyById = new Map<string, string>(); const permissionMap: Record<string, RolePermission[]> = {};
roleRows.forEach((r: any) => { for (const role of roleRows) {
const id = String(r?.id || "").trim(); try {
const key = String(r?.key || "") const permRes = await fetch(`${API}/api/admin/roles/${role.id}/permissions`, {
.toUpperCase() headers: authHeaders(),
.trim(); credentials: "include",
if (id) roleKeyById.set(id, key); });
}); if (permRes.ok) {
const perms = await permRes.json();
permissionMap[role.id] = perms || [];
}
} catch {
permissionMap[role.id] = [];
}
}
const dashboardRows: ExternalDashboard[] = roleRows
.filter((r: any) => r?.status === "ACTIVE" || r?.status === "INACTIVE")
.map((r: any) => {
const roleKey = String(r?.code || r?.key || "").toUpperCase();
const roleId = String(r?.id || "");
const roleName = String(r?.name || normalizeRoleNameFromKey(roleKey));
const perms = permissionMap[roleId] || [];
const viewablePerms = perms.filter((p: RolePermission) => p.can_view || p.can_list);
const sidebarItems = viewablePerms
.map((p: RolePermission) => {
const mod = moduleByKey.get(p.module_key);
return mod?.default_sidebar_label || p.module_name;
})
.filter(Boolean);
const externalRoleKeySet = new Set(
roleRows
.filter((r: any) => String(r?.audience || "").toUpperCase() === "EXTERNAL")
.map((r: any) => String(r?.key || "").toUpperCase())
.filter(Boolean)
);
const externalDashRows = dashRows
.filter((item: any) => {
const audience = String(item?.audience || "").toUpperCase();
if (audience === "EXTERNAL") return true;
const rowRoleKey = String(
item?.role_key || item?.config_json?.role_key || ""
).toUpperCase();
return rowRoleKey ? externalRoleKeySet.has(rowRoleKey) : false;
})
.map(normalizeDashboard)
.map((row: ExternalDashboard) => {
const resolvedRoleKey = String(
row.roleKey || roleKeyById.get(row.roleId) || ""
).toUpperCase();
const persona = personaFromKey(resolvedRoleKey);
return { return {
...row, id: `dashboard-${roleId}`,
roleKey: resolvedRoleKey || row.roleKey, roleId,
sidebarItems: mergeSidebarForPersona(row.sidebarItems, persona), roleKey,
name: `${roleName} Dashboard`,
code: `EXTERNAL-${roleKey}`,
widgets: [],
tabs: defaultTabsForRole(roleKey),
sidebarItems,
fields: defaultFieldsForRole(roleKey),
previewPath: rolePreviewPath(roleKey),
status: (r?.status === "INACTIVE" ? "INACTIVE" : "ACTIVE") as "ACTIVE" | "INACTIVE" | "DRAFT",
updatedAt: r?.updated_at || r?.created_date || "",
}; };
}); });
// Fallback: if runtime rows are empty but external roles exist, synthesize draft rows so UI isn't blank. setRows(dashboardRows.sort((a: ExternalDashboard, b: ExternalDashboard) =>
const fallbackRows = externalDashRows.length b.updatedAt.localeCompare(a.updatedAt)
? externalDashRows ));
: roleRows
.filter((r: any) => String(r?.audience || "").toUpperCase() === "EXTERNAL")
.map((r: any) => {
const roleKey = String(r?.key || "").toUpperCase();
const roleId = String(r?.id || "");
const roleName = String(r?.name || normalizeRoleNameFromKey(roleKey));
const persona = personaFromKey(roleKey);
return {
id: `draft-${roleId || roleKey}`,
roleId,
roleKey,
name: `${roleName} Dashboard`,
code: `EXTERNAL-${roleKey}`,
widgets: [],
tabs: [],
sidebarItems: mergeSidebarForPersona([], persona),
fields: [],
previewPath: rolePreviewPath(roleKey),
status: "DRAFT" as const,
updatedAt: "",
};
});
setRows(
fallbackRows.sort((a: ExternalDashboard, b: ExternalDashboard) =>
b.updatedAt.localeCompare(a.updatedAt)
)
);
} catch (e: any) { } catch (e: any) {
setRows([]); setRows([]);
setRoles([]); setRoles([]);
@ -562,6 +700,7 @@ export default function ExternalDashboardManagementPage() {
setIsActive(true); setIsActive(true);
setActivePreviewSidebar("My Profile"); setActivePreviewSidebar("My Profile");
setActivePreviewTab("basic info"); setActivePreviewTab("basic info");
setPkgCardColors({});
}; };
const openCreate = () => { const openCreate = () => {
@ -586,6 +725,7 @@ export default function ExternalDashboardManagementPage() {
setIsActive(row.status === "ACTIVE"); setIsActive(row.status === "ACTIVE");
setActivePreviewSidebar("My Profile"); setActivePreviewSidebar("My Profile");
setActivePreviewTab("basic info"); setActivePreviewTab("basic info");
setPkgCardColors(row.pkgCardColors || {});
setListTab("create"); setListTab("create");
setView("form"); setView("form");
}; };
@ -597,6 +737,8 @@ export default function ExternalDashboardManagementPage() {
const matchedRole = roles().find((r) => r.id === selected); const matchedRole = roles().find((r) => r.id === selected);
if (matchedRole) setFormRoleKey(matchedRole.key); if (matchedRole) setFormRoleKey(matchedRole.key);
applySidebarPresetForRole(selected, false); applySidebarPresetForRole(selected, false);
applyTabsPresetForRole(selected, false);
applyFieldsPresetForRole(selected, false);
applyPreviewPathForRole(selected, false); applyPreviewPathForRole(selected, false);
}); });
@ -616,6 +758,14 @@ export default function ExternalDashboardManagementPage() {
if (!items.includes(activePreviewSidebar())) setActivePreviewSidebar(items[0] || ""); if (!items.includes(activePreviewSidebar())) setActivePreviewSidebar(items[0] || "");
}); });
createEffect(() => {
const selected = roleId();
const persona = rolePersonaById()[selected];
if (!persona) return;
if (view() !== "form" || editingId()) return;
setSidebarItems(mergeSidebarForPersona(ROLE_BASED_SIDEBAR[persona], persona));
});
createEffect(() => { createEffect(() => {
const list = tabs(); // use configured tabs only — not the fallback ['overview'] const list = tabs(); // use configured tabs only — not the fallback ['overview']
if (!list.length) return; // sidebar-driven tabs (profile, portfolio, etc.) manage validation internally if (!list.length) return; // sidebar-driven tabs (profile, portfolio, etc.) manage validation internally
@ -667,6 +817,7 @@ export default function ExternalDashboardManagementPage() {
enabled: true, enabled: true,
intent: "role_marketplace_and_profile_verification", intent: "role_marketplace_and_profile_verification",
}, },
pkg_card: pkgCardColors(),
}, },
}), }),
}); });
@ -721,7 +872,7 @@ export default function ExternalDashboardManagementPage() {
</div> </div>
<div style="display:flex;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 16px;background:#FAFAFA"> <div style="display:flex;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 16px;background:#FAFAFA">
{(["general", "tabs", "sidebar", "fields", "preview", "full_preview"] as const).map( {(["general", "tabs", "sidebar", "fields", "theme", "preview", "full_preview"] as const).map(
(tab) => ( (tab) => (
<button <button
type="button" type="button"
@ -787,7 +938,7 @@ export default function ExternalDashboardManagementPage() {
Available Widgets Available Widgets
</p> </p>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px"> <div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
<For each={AVAILABLE_WIDGETS}> <For each={availableWidgetOptions()}>
{(key) => ( {(key) => (
<button <button
type="button" type="button"
@ -919,6 +1070,160 @@ export default function ExternalDashboardManagementPage() {
</div> </div>
</Show> </Show>
<Show when={formTab() === "theme"}>
<div style="display:flex;flex-direction:column;gap:16px">
<p style="font-size:13px;color:#6B7280;margin:0">
Configure colors for the Buy Credits package cards. Leave blank to use defaults.
</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Text Accent (Price Color)</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().text_accent ?? "#FF5E13"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_accent: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().text_accent ?? "#FF5E13"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_accent: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#FF5E13"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Text Primary</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().text_primary ?? "#111827"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_primary: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().text_primary ?? "#111827"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_primary: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#111827"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Text Secondary</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().text_secondary ?? "#6B7280"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_secondary: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().text_secondary ?? "#6B7280"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_secondary: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#6B7280"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Text Muted</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().text_muted ?? "#9CA3AF"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_muted: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().text_muted ?? "#9CA3AF"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_muted: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#9CA3AF"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Text Success (Coins per )</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().text_success ?? "#16A34A"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_success: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().text_success ?? "#16A34A"}
onInput={(e) => setPkgCardColors(c => ({ ...c, text_success: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#16A34A"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Coin Background</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().coin_bg ?? "#FFFBF8"}
onInput={(e) => setPkgCardColors(c => ({ ...c, coin_bg: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().coin_bg ?? "#FFFBF8"}
onInput={(e) => setPkgCardColors(c => ({ ...c, coin_bg: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#FFFBF8"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Best Value Badge BG</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().best_value_bg?.split(" ")[2]?.replace(/[^#0-9A-Fa-f]/g, "") ?? "#FF5E13"}
onInput={(e) => setPkgCardColors(c => ({ ...c, best_value_bg: `linear-gradient(135deg, ${e.currentTarget.value} 0%, #FF8A5C 100%)` }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().best_value_bg ?? ""}
onInput={(e) => setPkgCardColors(c => ({ ...c, best_value_bg: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="linear-gradient(...)"
/>
</div>
</label>
<label style="display:block">
<span style="font-size:12px;font-weight:600;color:#374151">Border Default</span>
<div style="display:flex;gap:8px;align-items:center;margin-top:4px">
<input
type="color"
value={pkgCardColors().border_default ?? "#E5E7EB"}
onInput={(e) => setPkgCardColors(c => ({ ...c, border_default: e.currentTarget.value }))}
style="width:36px;height:36px;border-radius:8px;border:1px solid #E5E7EB;cursor:pointer"
/>
<input
type="text"
value={pkgCardColors().border_default ?? "#E5E7EB"}
onInput={(e) => setPkgCardColors(c => ({ ...c, border_default: e.currentTarget.value }))}
style="flex:1;height:36px;border-radius:8px;border:1px solid #E5E7EB;padding:0 10px;font-size:13px"
placeholder="#E5E7EB"
/>
</div>
</label>
</div>
</div>
</Show>
<Show when={formTab() === "preview"}> <Show when={formTab() === "preview"}>
<div style="display:flex;flex-direction:column;gap:10px"> <div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid #E5E7EB;border-radius:10px;background:#FAFAFA"> <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid #E5E7EB;border-radius:10px;background:#FAFAFA">
@ -952,6 +1257,7 @@ export default function ExternalDashboardManagementPage() {
roleKey={selectedRoleKey() || formRoleKey()} roleKey={selectedRoleKey() || formRoleKey()}
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))} exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
onOpenFullscreen={() => setFormTab("full_preview")} onOpenFullscreen={() => setFormTab("full_preview")}
dashboardConfig={{ pkg_card: pkgCardColors() }}
/> />
</div> </div>
</Show> </Show>
@ -998,6 +1304,7 @@ export default function ExternalDashboardManagementPage() {
mode={"customer_external"} mode={"customer_external"}
roleKey={selectedRoleKey() || formRoleKey()} roleKey={selectedRoleKey() || formRoleKey()}
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))} exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
dashboardConfig={{ pkg_card: pkgCardColors() }}
/> />
</div> </div>
</div> </div>
@ -1307,6 +1614,7 @@ export default function ExternalDashboardManagementPage() {
mode={"customer_external"} mode={"customer_external"}
roleKey={selectedRoleKey() || formRoleKey()} roleKey={selectedRoleKey() || formRoleKey()}
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))} exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
dashboardConfig={{ pkg_card: pkgCardColors() }}
/> />
</div> </div>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ import {
import type { RuntimeDashboardLayout } from "~/lib/runtime/types"; import type { RuntimeDashboardLayout } from "~/lib/runtime/types";
import { loadAdminDashboardLayout, saveAdminDashboardLayout } from "~/lib/runtime/storage"; import { loadAdminDashboardLayout, saveAdminDashboardLayout } from "~/lib/runtime/storage";
const API = "/api"; const API = "";
async function fetchMetrics() { async function fetchMetrics() {
const accessToken = const accessToken =

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,368 @@
import { expect, test } from "@playwright/test";
type RoleCase = {
roleKey: string;
applicantName: string;
type: "profile" | "job" | "requirement";
route?: string;
endpoint?: string;
viewLabel?: string;
};
const roleCases: RoleCase[] = [
{ roleKey: "COMPANY", applicantName: "Nxtgauge Labs", type: "job" },
{ roleKey: "CUSTOMER", applicantName: "Arun Customer", type: "requirement" },
{ roleKey: "JOB_SEEKER", applicantName: "Jaya Jobseeker", type: "profile" },
{ roleKey: "PHOTOGRAPHER", applicantName: "Priya Photographer", type: "profile", route: "/admin/photographer", endpoint: "/api/admin/photographers", viewLabel: "View Profile" },
{ roleKey: "MAKEUP_ARTIST", applicantName: "Maya Makeup", type: "profile", route: "/admin/makeup-artist", endpoint: "/api/admin/makeup-artists", viewLabel: "View Profile" },
{ roleKey: "TUTOR", applicantName: "Tejas Tutor", type: "profile", route: "/admin/tutors", endpoint: "/api/admin/tutors", viewLabel: "View Profile" },
{ roleKey: "DEVELOPER", applicantName: "Dev Developer", type: "profile", route: "/admin/developers", endpoint: "/api/admin/developers", viewLabel: "View Profile" },
{ roleKey: "VIDEO_EDITOR", applicantName: "Vani Video", type: "profile", route: "/admin/video-editors", endpoint: "/api/admin/video-editors", viewLabel: "View Profile" },
{ roleKey: "UGC_CONTENT_CREATOR", applicantName: "Uma UGC", type: "profile", route: "/admin/ugc-content-creator", viewLabel: "View Profile" },
{ roleKey: "GRAPHIC_DESIGNER", applicantName: "Gita Graphic", type: "profile", route: "/admin/graphic-designers", endpoint: "/api/admin/graphic-designers", viewLabel: "View Profile" },
{ roleKey: "SOCIAL_MEDIA_MANAGER", applicantName: "Soma Social", type: "profile", route: "/admin/social-media-managers", endpoint: "/api/admin/social-media-managers", viewLabel: "View Profile" },
{ roleKey: "FITNESS_TRAINER", applicantName: "Farah Fitness", type: "profile", route: "/admin/fitness-trainers", endpoint: "/api/admin/fitness-trainers", viewLabel: "View Profile" },
{ roleKey: "CATERING_SERVICES", applicantName: "Chetan Catering", type: "profile", route: "/admin/catering-services", endpoint: "/api/admin/catering-services", viewLabel: "View Profile" },
];
const now = new Date().toISOString();
function toTitle(value: string) {
return String(value || "")
.toLowerCase()
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
test.describe("Verification to approval lifecycle across roles", () => {
test("all roles validate docs, request docs, approve, and appear in management with view actions", async ({ page }) => {
test.setTimeout(120000);
const verificationMap = new Map(
roleCases.map((item, idx) => {
const payload: any = {
first_name: item.applicantName.split(" ")[0] || "User",
last_name: item.applicantName.split(" ").slice(1).join(" ") || "",
company_name: item.roleKey === "COMPANY" ? "Nxtgauge Labs Pvt Ltd" : undefined,
city: "Chennai",
area: "Anna Nagar",
role_key: item.roleKey,
documents: [
{
id: `${item.roleKey.toLowerCase()}-doc-image`,
title: `${toTitle(item.roleKey)} ID Proof`,
type: "IMAGE",
url: "/nxtgauge-logo.png",
status: "SUBMITTED",
},
{
id: `${item.roleKey.toLowerCase()}-doc-pdf`,
title: `${toTitle(item.roleKey)} Address Proof`,
type: "PDF",
url: "/nxtgauge-icon.png",
status: "SUBMITTED",
},
],
};
if (item.type === "job") payload.job_id = "job-001";
if (item.type === "requirement") payload.requirement_id = "req-001";
return [
item.roleKey,
{
id: `ver-${idx + 1}`,
user_id: `user-${idx + 1}`,
user_name: item.applicantName,
role_key: item.roleKey,
type: item.type,
case_type: item.type,
status: "PENDING",
created_at: now,
updated_at: now,
payload,
},
];
})
);
const finalApprovedRoles = new Set<string>();
let jobFinalApproved = false;
let requirementFinalApproved = false;
const statusForRole = (roleKey: string) => {
if (roleKey === "COMPANY") return jobFinalApproved ? "ACTIVE" : "PENDING";
if (roleKey === "CUSTOMER") return requirementFinalApproved ? "ACTIVE" : "PENDING";
return finalApprovedRoles.has(roleKey) ? "ACTIVE" : "PENDING";
};
const asProfessionRow = (roleKey: string, applicantName: string) => {
const parts = applicantName.split(" ");
return {
id: `pro-${roleKey.toLowerCase()}`,
first_name: parts[0] || applicantName,
last_name: parts.slice(1).join(" ") || "User",
email: `${roleKey.toLowerCase()}@example.test`,
phone: "9999999999",
status: statusForRole(roleKey),
created_at: now,
};
};
await page.route("**/api/admin/**", async (route) => {
const req = route.request();
const url = new URL(req.url());
const path = url.pathname;
const method = req.method();
if (path === "/api/admin/verifications" && method === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ items: Array.from(verificationMap.values()) }),
});
return;
}
if (path.startsWith("/api/admin/verifications/") && method === "GET") {
const id = path.split("/").pop() || "";
const row = Array.from(verificationMap.values()).find((item) => item.id === id);
await route.fulfill({
status: row ? 200 : 404,
contentType: "application/json",
body: JSON.stringify(row || { error: "Not found" }),
});
return;
}
if (path.startsWith("/api/admin/verifications/") && method === "POST") {
const parts = path.split("/");
const id = parts[4] || "";
const action = parts[5] || "";
const row = Array.from(verificationMap.values()).find((item) => item.id === id);
if (!row) {
await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ error: "Not found" }) });
return;
}
if (action === "approve") row.status = "APPROVED";
if (action === "reject") row.status = "REJECTED";
if (action === "request-documents") row.status = "DOCUMENTS_REQUESTED";
if (action === "request-revision") row.status = "REVISION_REQUESTED";
row.updated_at = new Date().toISOString();
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }) });
return;
}
if (path.startsWith("/api/admin/approvals/jobs/") && method === "POST") {
if (path.endsWith("/approve")) jobFinalApproved = true;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }) });
return;
}
if (path.startsWith("/api/admin/approvals/requirements/") && method === "POST") {
if (path.endsWith("/approve")) requirementFinalApproved = true;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }) });
return;
}
if (path.startsWith("/api/admin/approvals/profiles/") && method === "POST") {
const verificationId = path.split("/")[5] || "";
const row = Array.from(verificationMap.values()).find((item) => item.id === verificationId);
if (path.endsWith("/approve") && row) finalApprovedRoles.add(row.role_key);
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }) });
return;
}
if (path === "/api/admin/companies" && method === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{
id: "cmp-001",
company_name: "Nxtgauge Labs Pvt Ltd",
registration_number: "TN-REG-0021",
industry: "Software",
city: "Chennai",
status: jobFinalApproved ? "APPROVED" : "PENDING",
created_at: now,
updated_at: now,
job_postings_count: 3,
total_hires: 1,
},
]),
});
return;
}
if (path === "/api/admin/companies/jobs" && method === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{
id: "job-001",
title: "Senior Frontend Engineer",
company_name: "Nxtgauge Labs Pvt Ltd",
location: "Chennai",
salary_min: 1200000,
salary_max: 1800000,
status: jobFinalApproved ? "ACTIVE" : "PENDING_APPROVAL",
created_at: now,
updated_at: now,
},
]),
});
return;
}
if (path === "/api/admin/leads" && method === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{
id: "req-001",
title: "Wedding Photography Requirement",
profession: "photographer",
budget_range: "INR 75,000",
location: "Chennai",
status: requirementFinalApproved ? "ACTIVE" : "PENDING",
description: "Need full-day candid and cinematic coverage",
created_at: now,
updated_at: now,
},
]),
});
return;
}
if (path === "/api/admin/users" && method === "GET") {
const requestedRole = String(url.searchParams.get("role") || "").toUpperCase();
const users = roleCases.map((item, idx) => {
const status = statusForRole(item.roleKey);
const names = item.applicantName.split(" ");
return {
id: `user-${idx + 1}`,
first_name: names[0] || "User",
last_name: names.slice(1).join(" ") || "",
full_name: item.applicantName,
email: `${item.roleKey.toLowerCase()}@example.test`,
status,
roles: status === "ACTIVE" ? [item.roleKey] : [],
created_at: now,
updated_at: now,
};
});
const payload = requestedRole ? users.filter((u) => u.roles.includes(requestedRole)) : users;
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(payload) });
return;
}
const professionEndpointMap: Record<string, string> = {
"/api/admin/photographers": "PHOTOGRAPHER",
"/api/admin/makeup-artists": "MAKEUP_ARTIST",
"/api/admin/tutors": "TUTOR",
"/api/admin/developers": "DEVELOPER",
"/api/admin/video-editors": "VIDEO_EDITOR",
"/api/admin/graphic-designers": "GRAPHIC_DESIGNER",
"/api/admin/social-media-managers": "SOCIAL_MEDIA_MANAGER",
"/api/admin/fitness-trainers": "FITNESS_TRAINER",
"/api/admin/catering-services": "CATERING_SERVICES",
};
if (method === "GET" && professionEndpointMap[path]) {
const roleKey = professionEndpointMap[path];
const roleCase = roleCases.find((item) => item.roleKey === roleKey)!;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([asProfessionRow(roleKey, roleCase.applicantName)]),
});
return;
}
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]) });
});
await page.goto("/admin/verification?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Verification Management" })).toBeVisible();
for (const item of roleCases) {
await expect(page.getByText(toTitle(item.roleKey), { exact: false }).first()).toBeVisible();
}
const firstRole = roleCases[0];
const firstRow = page.locator("tr", { hasText: firstRole.applicantName }).first();
await firstRow.getByRole("button", { name: "View" }).click();
await expect(page.getByText("Submitted Documents")).toBeVisible();
await page.getByRole("button", { name: "View" }).nth(1).click();
await expect(page.getByRole("button", { name: "Close" })).toBeVisible();
await page.getByRole("button", { name: "Close" }).click();
const requestCheckbox = page.locator('input[type="checkbox"]').nth(1);
await requestCheckbox.check();
await page.getByRole("button", { name: "Request Selected Documents" }).click();
await expect(page.getByText(/Document request sent/i)).toBeVisible();
await page.getByRole("button", { name: "Approve" }).first().click();
await expect(page.getByText("Successfully verified and sent to Approval Management.")).toBeVisible();
await page.getByRole("button", { name: "Back to List" }).click();
for (const item of roleCases.slice(1)) {
const row = page.locator("tr", { hasText: item.applicantName }).first();
await row.getByRole("button", { name: "View" }).click();
await page.getByRole("button", { name: "Approve" }).first().click();
await page.getByRole("button", { name: "Back to List" }).click();
}
await page.goto("/admin/approval?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Approval Management" })).toBeVisible();
for (const item of roleCases) {
const row = page.locator("tr", { hasText: item.applicantName }).first();
await expect(row).toBeVisible();
}
const firstApprovalRow = page.locator("tbody tr").first();
await firstApprovalRow.locator("button").first().click();
await page.getByRole("button", { name: /^Approve$/ }).first().click();
jobFinalApproved = true;
requirementFinalApproved = true;
for (const item of roleCases) {
if (item.type === "profile") finalApprovedRoles.add(item.roleKey);
}
await page.goto("/admin/jobs?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Jobs Management" })).toBeVisible();
await expect(page.locator("tr", { hasText: "Senior Frontend Engineer" })).toContainText("Active");
await page.goto("/admin/leads?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Leads Management" })).toBeVisible();
await expect(page.locator("tr", { hasText: "Wedding Photography Requirement" })).toContainText("ACTIVE");
await expect(page.getByRole("link", { name: "View" }).first()).toBeVisible();
await page.goto("/admin/company?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Company Management" })).toBeVisible();
const companyRow = page.locator("tr", { hasText: "Nxtgauge Labs Pvt Ltd" }).first();
await expect(companyRow).toContainText("Active");
await companyRow.locator("button").first().click();
await companyRow.getByRole("button", { name: "View Company" }).click();
await expect(page.getByRole("button", { name: "Back to List" })).toBeVisible();
await page.goto("/admin/users?_preview=1", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Users Management" })).toBeVisible();
for (const item of roleCases) {
await expect(page.getByText(toTitle(item.roleKey), { exact: false }).first()).toBeVisible();
}
for (const item of roleCases.filter((row) => row.route)) {
await page.goto(`${item.route}?_preview=1`, { waitUntil: "domcontentloaded" });
await expect(page.getByText(item.applicantName.split(" ")[0], { exact: false }).first()).toBeVisible();
const tableRow = page.locator("tr", { hasText: item.applicantName.split(" ")[0] }).first();
await tableRow.locator("button").first().click();
await expect(page.getByRole("link", { name: item.viewLabel || "View Profile" }).first()).toBeVisible();
}
});
});

View file

@ -0,0 +1,30 @@
// vite.config.ts
import { defineConfig } from "@solidjs/start/config";
import tailwindcss from "@tailwindcss/vite";
var vite_config_default = defineConfig({
ssr: false,
vite: {
plugins: [tailwindcss()],
server: {
proxy: {
"/api": {
target: "http://localhost:9100",
changeOrigin: true,
secure: false,
ws: true,
configureProxy: (proxy) => {
proxy.on("proxyReq", (proxyReq, req) => {
const cookie = req.headers.cookie;
if (cookie) {
proxyReq.setHeader("Cookie", cookie);
}
});
}
}
}
}
}
});
export {
vite_config_default as default
};