chore: checkpoint workspace updates
This commit is contained in:
parent
2c4e579725
commit
b75c99408c
25 changed files with 7630 additions and 3772 deletions
2045
.playwright-cli/console-2026-04-21T21-45-00-110Z.log
Normal file
2045
.playwright-cli/console-2026-04-21T21-45-00-110Z.log
Normal file
File diff suppressed because it is too large
Load diff
70
.playwright-cli/page-2026-04-21T21-45-02-136Z.yml
Normal file
70
.playwright-cli/page-2026-04-21T21-45-02-136Z.yml
Normal 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]
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
> dev
|
||||||
|
> vinxi dev
|
||||||
|
|
||||||
|
vinxi v0.5.11
|
||||||
|
|
@ -1 +1 @@
|
||||||
7741
|
61044
|
||||||
|
|
|
||||||
1
admin-solid.start.log
Normal file
1
admin-solid.start.log
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Listening on http://[::]:3000
|
||||||
1
admin-solid.start.pid
Normal file
1
admin-solid.start.pid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
72260
|
||||||
71
admin.log
71
admin.log
|
|
@ -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
16
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { default } from './designation';
|
import Designation from "./designation";
|
||||||
|
export default Designation;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
368
tests/e2e/verification-approval-role-lifecycle.spec.ts
Normal file
368
tests/e2e/verification-approval-role-lifecycle.spec.ts
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
30
vite.config.timestamp_1776803609838.js
Normal file
30
vite.config.timestamp_1776803609838.js
Normal 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
|
||||||
|
};
|
||||||
Loading…
Add table
Reference in a new issue