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/
|
||||
➜ 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",
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.3.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@thisbeyond/solid-dnd": "^0.7.5",
|
||||
"apexcharts": "^5.10.4",
|
||||
|
|
@ -2747,18 +2747,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@solidjs/router": {
|
||||
"version": "0.15.4",
|
||||
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.4.tgz",
|
||||
"integrity": "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==",
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@solidjs/router/-/router-0.15.3.tgz",
|
||||
"integrity": "sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@solidjs/start": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@solidjs/start/-/start-1.3.2.tgz",
|
||||
"integrity": "sha512-tasDl3utVbtP0rr4InB3ntBIFV2upvEiFrOOCkRrAA3yBfjx9elpxnc94sJQXo65PNYdAAAkPIC6h93vLrtwHg==",
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@solidjs/start/-/start-1.3.0.tgz",
|
||||
"integrity": "sha512-FMqc0ZaAUIFBVOEUV87Y1W6LuCN5OveOigXvjZ9CarB/TQSC3QqDBSX+EyWkvreGIU7zsEIi0mka6NGJgJ5oOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/server-functions-plugin": "1.121.21",
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.0",
|
||||
"@solidjs/start": "^1.3.2",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@thisbeyond/solid-dnd": "^0.7.5",
|
||||
"apexcharts": "^5.10.4",
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
"pngjs": "^7.0.0",
|
||||
"storybook": "^10.3.3",
|
||||
"storybook-solidjs-vite": "^10.0.11",
|
||||
"typescript": "^5.5.0",
|
||||
"visbug": "^0.1.14",
|
||||
"vitest": "^4.1.1",
|
||||
"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() {
|
||||
return (
|
||||
<Router
|
||||
root={props => (
|
||||
root={(props) => (
|
||||
<MetaProvider>
|
||||
<Title>ADMIN PANEL | NXTGAUGE</Title>
|
||||
<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 {
|
||||
For, Show, createEffect, createMemo, createSignal,
|
||||
onCleanup, onMount, 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';
|
||||
For,
|
||||
Show,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
onMount,
|
||||
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 SearchResult = { id: string; title: string; subtitle: string; href: string };
|
||||
type SearchGroup = { label: string; viewAllHref: string; results: SearchResult[] };
|
||||
|
||||
const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [
|
||||
{ prefix: '/admin', label: 'Dashboard', exact: true },
|
||||
{ prefix: '/admin/department', label: 'Department Management' },
|
||||
{ prefix: '/admin/designation', label: 'Designation Management' },
|
||||
{ prefix: '/admin/roles', label: 'Internal Role Management' },
|
||||
{ prefix: '/admin/employees', label: 'Employee Management' },
|
||||
{ prefix: '/admin/external-roles', label: 'External Role Management' },
|
||||
{ prefix: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management' },
|
||||
{ prefix: '/admin/external-dashboard-management', label: 'External Dashboard Management' },
|
||||
{ prefix: '/admin/role-ui-configs', label: 'External Dashboard Management' },
|
||||
{ prefix: '/admin/verification', label: 'Verification Management' },
|
||||
{ prefix: '/admin/verification-status', label: 'Verification Management' },
|
||||
{ prefix: '/admin/approval', label: 'Approval Management' },
|
||||
{ prefix: '/admin/approvals', label: 'Approval Management' },
|
||||
{ prefix: '/admin/approval-management', label: 'Approval Management' },
|
||||
{ prefix: '/admin/users', label: 'Users Management' },
|
||||
{ prefix: '/admin/company', label: 'Company Management' },
|
||||
{ prefix: '/admin/candidate', label: 'Candidate Management' },
|
||||
{ prefix: '/admin/customer', label: 'Customer Management' },
|
||||
{ prefix: '/admin/photographer', label: 'Photographer Management' },
|
||||
{ prefix: '/admin/makeup-artist', label: 'Makeup Artist Management' },
|
||||
{ prefix: '/admin/tutors', label: 'Tutors Management' },
|
||||
{ prefix: '/admin/developers', label: 'Developers Management' },
|
||||
{ prefix: '/admin/video-editors', label: 'Video Editor Management' },
|
||||
{ prefix: '/admin/fitness-trainers', label: 'Fitness Trainer Management' },
|
||||
{ prefix: '/admin/catering-services', label: 'Catering Services Management' },
|
||||
{ prefix: '/admin/ugc-content-creators', label: 'UGC Content Creator Management' },
|
||||
{ prefix: '/admin/graphic-designers', label: 'Graphic Designer Management' },
|
||||
{ prefix: '/admin/social-media-managers', label: 'Social Media Manager Management' },
|
||||
{ prefix: '/admin/jobs', label: 'Jobs Management' },
|
||||
{ prefix: '/admin/leads', label: 'Leads Management' },
|
||||
{ prefix: '/admin/applications', label: 'Applications Management' },
|
||||
{ prefix: '/admin/responses', label: 'Responses Management' },
|
||||
{ prefix: '/admin/pricing', label: 'Pricing Management' },
|
||||
{ prefix: '/admin/credit', label: 'Credit Management' },
|
||||
{ prefix: '/admin/coupon', label: 'Coupon Management' },
|
||||
{ prefix: '/admin/discount', label: 'Discount Management' },
|
||||
{ prefix: '/admin/tax', label: 'Tax Management' },
|
||||
{ prefix: '/admin/order', label: 'Order Management' },
|
||||
{ prefix: '/admin/invoice', label: 'Invoice Management' },
|
||||
{ prefix: '/admin/payment-gateway', label: 'Payment Gateway Management' },
|
||||
{ prefix: '/admin/smtp', label: 'SMTP Management' },
|
||||
{ prefix: '/admin/kb', label: 'Knowledge Base Management' },
|
||||
{ prefix: '/admin/notifications', label: 'Notifications' },
|
||||
{ prefix: '/admin/review', label: 'Review Management' },
|
||||
{ prefix: '/admin/support', label: 'Support Management' },
|
||||
{ prefix: '/admin/report', label: 'Report Management' },
|
||||
{ prefix: '/admin/ledger', label: 'Ledger Management' },
|
||||
{ prefix: "/admin", label: "Dashboard", exact: true },
|
||||
{ prefix: "/admin/department", label: "Department Management" },
|
||||
{ prefix: "/admin/designation", label: "Designation Management" },
|
||||
{ prefix: "/admin/roles", label: "Internal Role Management" },
|
||||
{ prefix: "/admin/employees", label: "Employee Management" },
|
||||
{ prefix: "/admin/external-roles", label: "External Role Management" },
|
||||
{ prefix: "/admin/internal-dashboard-management", label: "Internal Dashboard Management" },
|
||||
{ prefix: "/admin/external-dashboard-management", label: "External Dashboard Management" },
|
||||
{ prefix: "/admin/role-ui-configs", label: "External Dashboard Management" },
|
||||
{ prefix: "/admin/verification", label: "Verification Management" },
|
||||
{ prefix: "/admin/verification-status", label: "Verification Management" },
|
||||
{ prefix: "/admin/approval", label: "Approval Management" },
|
||||
{ prefix: "/admin/approvals", label: "Approval Management" },
|
||||
{ prefix: "/admin/approval-management", label: "Approval Management" },
|
||||
{ prefix: "/admin/users", label: "Users Management" },
|
||||
{ prefix: "/admin/company", label: "Company Management" },
|
||||
{ prefix: "/admin/candidate", label: "Candidate Management" },
|
||||
{ prefix: "/admin/customer", label: "Customer Management" },
|
||||
{ prefix: "/admin/photographer", label: "Photographer Management" },
|
||||
{ prefix: "/admin/makeup-artist", label: "Makeup Artist Management" },
|
||||
{ prefix: "/admin/tutors", label: "Tutors Management" },
|
||||
{ prefix: "/admin/developers", label: "Developers Management" },
|
||||
{ prefix: "/admin/video-editors", label: "Video Editor Management" },
|
||||
{ prefix: "/admin/fitness-trainers", label: "Fitness Trainer Management" },
|
||||
{ prefix: "/admin/catering-services", label: "Catering Services Management" },
|
||||
{ prefix: "/admin/ugc-content-creators", label: "UGC Content Creator Management" },
|
||||
{ prefix: "/admin/graphic-designers", label: "Graphic Designer Management" },
|
||||
{ prefix: "/admin/social-media-managers", label: "Social Media Manager Management" },
|
||||
{ prefix: "/admin/jobs", label: "Jobs Management" },
|
||||
{ prefix: "/admin/leads", label: "Leads Management" },
|
||||
{ prefix: "/admin/applications", label: "Applications Management" },
|
||||
{ prefix: "/admin/responses", label: "Responses Management" },
|
||||
{ prefix: "/admin/pricing", label: "Pricing Management" },
|
||||
{ prefix: "/admin/credit", label: "Credit Management" },
|
||||
{ prefix: "/admin/coupon", label: "Coupon Management" },
|
||||
{ prefix: "/admin/discount", label: "Discount Management" },
|
||||
{ prefix: "/admin/tax", label: "Tax Management" },
|
||||
{ prefix: "/admin/order", label: "Order Management" },
|
||||
{ prefix: "/admin/invoice", label: "Invoice Management" },
|
||||
{ prefix: "/admin/payment-gateway", label: "Payment Gateway Management" },
|
||||
{ prefix: "/admin/smtp", label: "SMTP Management" },
|
||||
{ prefix: "/admin/kb", label: "Knowledge Base Management" },
|
||||
{ prefix: "/admin/notifications", label: "Notifications" },
|
||||
{ prefix: "/admin/review", label: "Review Management" },
|
||||
{ prefix: "/admin/support", label: "Support Management" },
|
||||
{ prefix: "/admin/report", label: "Report Management" },
|
||||
{ prefix: "/admin/ledger", label: "Ledger Management" },
|
||||
];
|
||||
|
||||
const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [];
|
||||
const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [
|
||||
{ prefix: '/admin', keys: ['ADMIN_DASHBOARD', 'DASHBOARD'] },
|
||||
{ prefix: '/admin/department', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] },
|
||||
{ prefix: '/admin/department-management', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] },
|
||||
{ prefix: '/admin/designation', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] },
|
||||
{ prefix: '/admin/designation-management', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] },
|
||||
{ prefix: '/admin/roles', keys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] },
|
||||
{ prefix: '/admin/employees', keys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] },
|
||||
{ prefix: '/admin/external-roles', keys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] },
|
||||
{ prefix: '/admin/internal-dashboard-management', keys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS', 'INTERNAL_DASHBOARD_CONFIG'] },
|
||||
{ prefix: '/admin/external-dashboard-management', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] },
|
||||
{ prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'EXTERNAL_DASHBOARD_CONFIG', 'RUNTIME_ROLES'] },
|
||||
{ prefix: '/admin/verification', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] },
|
||||
{ prefix: '/admin/verification-status', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] },
|
||||
{ prefix: '/admin/approval', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] },
|
||||
{ prefix: '/admin/approvals', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] },
|
||||
{ prefix: '/admin/approval-management', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] },
|
||||
{ prefix: '/admin/users', keys: ['USER_MANAGEMENT', 'USERS'] },
|
||||
{ prefix: '/admin/company', keys: ['COMPANY_MANAGEMENT', 'COMPANIES'] },
|
||||
{ prefix: '/admin/candidate', keys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] },
|
||||
{ prefix: '/admin/customer', keys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] },
|
||||
{ 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/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] },
|
||||
{ prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] },
|
||||
{ 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'] },
|
||||
{ prefix: "/admin", keys: ["ADMIN_DASHBOARD", "DASHBOARD"] },
|
||||
{ prefix: "/admin/department", keys: ["DEPARTMENT_MANAGEMENT", "DEPARTMENTS"] },
|
||||
{ prefix: "/admin/department-management", keys: ["DEPARTMENT_MANAGEMENT", "DEPARTMENTS"] },
|
||||
{ prefix: "/admin/designation", keys: ["DESIGNATION_MANAGEMENT", "DESIGNATIONS"] },
|
||||
{ prefix: "/admin/designation-management", keys: ["DESIGNATION_MANAGEMENT", "DESIGNATIONS"] },
|
||||
{ prefix: "/admin/roles", keys: ["INTERNAL_ROLE_MANAGEMENT", "ROLES"] },
|
||||
{ prefix: "/admin/employees", keys: ["EMPLOYEE_MANAGEMENT", "EMPLOYEES"] },
|
||||
{ prefix: "/admin/external-roles", keys: ["EXTERNAL_ROLE_MANAGEMENT", "EXTERNAL_ROLES"] },
|
||||
{
|
||||
prefix: "/admin/internal-dashboard-management",
|
||||
keys: ["INTERNAL_DASHBOARD_MANAGEMENT", "INTERNAL_DASHBOARDS", "INTERNAL_DASHBOARD_CONFIG"],
|
||||
},
|
||||
{
|
||||
prefix: "/admin/external-dashboard-management",
|
||||
keys: [
|
||||
"DASHBOARD_CONFIG_MANAGEMENT",
|
||||
"EXTERNAL_DASHBOARD_MANAGEMENT",
|
||||
"EXTERNAL_DASHBOARDS",
|
||||
"EXTERNAL_DASHBOARD_CONFIG",
|
||||
"RUNTIME_ROLES",
|
||||
],
|
||||
},
|
||||
{
|
||||
prefix: "/admin/role-ui-configs",
|
||||
keys: [
|
||||
"DASHBOARD_CONFIG_MANAGEMENT",
|
||||
"EXTERNAL_DASHBOARD_MANAGEMENT",
|
||||
"EXTERNAL_DASHBOARDS",
|
||||
"EXTERNAL_DASHBOARD_CONFIG",
|
||||
"RUNTIME_ROLES",
|
||||
],
|
||||
},
|
||||
{ prefix: "/admin/verification", keys: ["VERIFICATION_MANAGEMENT", "VERIFICATIONS"] },
|
||||
{ prefix: "/admin/verification-status", keys: ["VERIFICATION_MANAGEMENT", "VERIFICATIONS"] },
|
||||
{ prefix: "/admin/approval", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
|
||||
{ prefix: "/admin/approvals", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
|
||||
{ prefix: "/admin/approval-management", keys: ["APPROVAL_MANAGEMENT", "APPROVALS"] },
|
||||
{ prefix: "/admin/users", keys: ["USER_MANAGEMENT", "USERS"] },
|
||||
{ prefix: "/admin/company", keys: ["COMPANY_MANAGEMENT", "COMPANIES"] },
|
||||
{ prefix: "/admin/candidate", keys: ["CANDIDATE_MANAGEMENT", "CANDIDATES"] },
|
||||
{ prefix: "/admin/customer", keys: ["CUSTOMER_MANAGEMENT", "CUSTOMERS"] },
|
||||
{ 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/video-editors", keys: ["VIDEO_EDITOR_MANAGEMENT", "VIDEO_EDITORS"] },
|
||||
{ prefix: "/admin/fitness-trainers", keys: ["FITNESS_TRAINER_MANAGEMENT", "FITNESS_TRAINERS"] },
|
||||
{
|
||||
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 = [
|
||||
{
|
||||
label: 'Users',
|
||||
viewAllHref: '/admin/users',
|
||||
api: '/api/admin/users',
|
||||
listKeys: ['users', 'items'],
|
||||
titleKeys: ['full_name', 'name'],
|
||||
subtitleKeys: ['email', 'phone'],
|
||||
detailBase: '/admin/users',
|
||||
label: "Users",
|
||||
viewAllHref: "/admin/users",
|
||||
api: "/api/admin/users",
|
||||
listKeys: ["users", "items"],
|
||||
titleKeys: ["full_name", "name"],
|
||||
subtitleKeys: ["email", "phone"],
|
||||
detailBase: "/admin/users",
|
||||
},
|
||||
{
|
||||
label: 'Companies',
|
||||
viewAllHref: '/admin/company',
|
||||
api: '/api/admin/companies',
|
||||
listKeys: ['companies', 'items'],
|
||||
titleKeys: ['name', 'companyName'],
|
||||
subtitleKeys: ['email', 'phone'],
|
||||
detailBase: '/admin/company',
|
||||
label: "Companies",
|
||||
viewAllHref: "/admin/company",
|
||||
api: "/api/admin/companies",
|
||||
listKeys: ["companies", "items"],
|
||||
titleKeys: ["name", "companyName"],
|
||||
subtitleKeys: ["email", "phone"],
|
||||
detailBase: "/admin/company",
|
||||
},
|
||||
{
|
||||
label: 'Employees',
|
||||
viewAllHref: '/admin/employees',
|
||||
api: '/api/admin/employees',
|
||||
listKeys: ['employees', 'items'],
|
||||
titleKeys: ['full_name', 'name'],
|
||||
subtitleKeys: ['email', 'department_name'],
|
||||
detailBase: '/admin/employees',
|
||||
label: "Employees",
|
||||
viewAllHref: "/admin/employees",
|
||||
api: "/api/admin/employees",
|
||||
listKeys: ["employees", "items"],
|
||||
titleKeys: ["full_name", "name"],
|
||||
subtitleKeys: ["email", "department_name"],
|
||||
detailBase: "/admin/employees",
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
viewAllHref: '/admin/jobs',
|
||||
api: '/api/admin/jobs',
|
||||
listKeys: ['jobs', 'items'],
|
||||
titleKeys: ['title', 'name'],
|
||||
subtitleKeys: ['status', 'company_name'],
|
||||
detailBase: '/admin/jobs',
|
||||
label: "Jobs",
|
||||
viewAllHref: "/admin/jobs",
|
||||
api: "/api/admin/jobs",
|
||||
listKeys: ["jobs", "items"],
|
||||
titleKeys: ["title", "name"],
|
||||
subtitleKeys: ["status", "company_name"],
|
||||
detailBase: "/admin/jobs",
|
||||
},
|
||||
{
|
||||
label: 'Leads',
|
||||
viewAllHref: '/admin/leads',
|
||||
api: '/api/admin/leads',
|
||||
listKeys: ['leads', 'items'],
|
||||
titleKeys: ['name', 'full_name'],
|
||||
subtitleKeys: ['email', 'status'],
|
||||
detailBase: '/admin/leads',
|
||||
label: "Leads",
|
||||
viewAllHref: "/admin/leads",
|
||||
api: "/api/admin/leads",
|
||||
listKeys: ["leads", "items"],
|
||||
titleKeys: ["name", "full_name"],
|
||||
subtitleKeys: ["email", "status"],
|
||||
detailBase: "/admin/leads",
|
||||
},
|
||||
];
|
||||
|
||||
function pickStr(obj: Record<string, any>, keys: string[]): string {
|
||||
for (const k of keys) if (obj[k]) return String(obj[k]);
|
||||
return '—';
|
||||
return "—";
|
||||
}
|
||||
|
||||
function extractList(data: any, keys: string[]): any[] {
|
||||
|
|
@ -176,7 +218,7 @@ function extractList(data: any, keys: string[]): any[] {
|
|||
}
|
||||
|
||||
function GlobalSearch() {
|
||||
const [query, setQuery] = createSignal('');
|
||||
const [query, setQuery] = createSignal("");
|
||||
const [open, setOpen] = createSignal(false);
|
||||
const [groups, setGroups] = createSignal<SearchGroup[]>([]);
|
||||
const [searching, setSearching] = createSignal(false);
|
||||
|
|
@ -185,11 +227,17 @@ function GlobalSearch() {
|
|||
|
||||
const doSearch = async (q: string) => {
|
||||
const trimmed = q.trim();
|
||||
if (trimmed.length < 2) { setGroups([]); setOpen(false); return; }
|
||||
if (trimmed.length < 2) {
|
||||
setGroups([]);
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const settled = await Promise.allSettled(
|
||||
SEARCH_MODULES.map(async (mod) => {
|
||||
const res = await fetch(`${mod.api}?search=${encodeURIComponent(trimmed)}&limit=4`).catch(() => null);
|
||||
const res = await fetch(`${mod.api}?search=${encodeURIComponent(trimmed)}&limit=4`).catch(
|
||||
() => null
|
||||
);
|
||||
if (!res?.ok) return null;
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!data) return null;
|
||||
|
|
@ -205,9 +253,9 @@ function GlobalSearch() {
|
|||
href: `${mod.detailBase}/${item.id}`,
|
||||
})),
|
||||
} 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);
|
||||
setSearching(false);
|
||||
};
|
||||
|
|
@ -215,26 +263,39 @@ function GlobalSearch() {
|
|||
const handleInput = (val: string) => {
|
||||
setQuery(val);
|
||||
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);
|
||||
};
|
||||
|
||||
const close = () => { setOpen(false); setQuery(''); setGroups([]); };
|
||||
const onOutside = (e: MouseEvent) => { if (!wrapRef.contains(e.target as Node)) setOpen(false); };
|
||||
const close = () => {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
setGroups([]);
|
||||
};
|
||||
const onOutside = (e: MouseEvent) => {
|
||||
if (!wrapRef.contains(e.target as Node)) setOpen(false);
|
||||
};
|
||||
|
||||
onMount(() => document.addEventListener('mousedown', onOutside));
|
||||
onCleanup(() => document.removeEventListener('mousedown', onOutside));
|
||||
onMount(() => document.addEventListener("mousedown", onOutside));
|
||||
onCleanup(() => document.removeEventListener("mousedown", onOutside));
|
||||
|
||||
return (
|
||||
<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
|
||||
type="text"
|
||||
value={query()}
|
||||
placeholder="Search system resources..."
|
||||
onInput={(e) => handleInput(e.currentTarget.value)}
|
||||
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"
|
||||
/>
|
||||
|
||||
|
|
@ -244,19 +305,35 @@ function GlobalSearch() {
|
|||
{(group) => (
|
||||
<div class="border-b border-[#f1f2f5] px-4 py-3 last:border-b-0">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.12em] text-[#9aa0b9]">{group.label}</span>
|
||||
<A href={group.viewAllHref} onClick={close} class="text-[12px] font-semibold text-[#FF5E13]">View all</A>
|
||||
<span class="text-[10px] font-bold uppercase tracking-[0.12em] text-[#9aa0b9]">
|
||||
{group.label}
|
||||
</span>
|
||||
<A
|
||||
href={group.viewAllHref}
|
||||
onClick={close}
|
||||
class="text-[12px] font-semibold text-[#FF5E13]"
|
||||
>
|
||||
View all
|
||||
</A>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<For each={group.results}>
|
||||
{(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]">
|
||||
{item.title.trim().slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-[13px] font-semibold text-[#0D0D2A]">{item.title}</p>
|
||||
<p class="truncate text-[12px] text-[rgba(13,13,42,0.55)]">{item.subtitle}</p>
|
||||
<p class="truncate text-[13px] font-semibold text-[#0D0D2A]">
|
||||
{item.title}
|
||||
</p>
|
||||
<p class="truncate text-[12px] text-[rgba(13,13,42,0.55)]">
|
||||
{item.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
|
|
@ -281,20 +358,27 @@ function ShowTabs(props: {
|
|||
tabs: Tab[];
|
||||
isTabActive: (tab: Tab) => boolean;
|
||||
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 };
|
||||
}) {
|
||||
if (props.tabs.length === 0) return null;
|
||||
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}>
|
||||
{(tab) => (
|
||||
<A
|
||||
href={tab.href}
|
||||
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 ${
|
||||
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}
|
||||
|
|
@ -302,7 +386,7 @@ function ShowTabs(props: {
|
|||
)}
|
||||
</For>
|
||||
<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` }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -314,14 +398,14 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||
const [adminName, setAdminName] = createSignal('Admin User');
|
||||
const [checkedSession, setCheckedSession] = createSignal(true);
|
||||
const [adminName, setAdminName] = createSignal("Admin User");
|
||||
const [allowedModules, setAllowedModules] = createSignal<string[] | null>(null);
|
||||
const [isSuperAdmin, setIsSuperAdmin] = createSignal(false);
|
||||
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
|
||||
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 [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
|
||||
|
|
@ -331,25 +415,26 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
|
||||
const logout = async () => {
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
const accessToken =
|
||||
typeof sessionStorage !== "undefined"
|
||||
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||
: "";
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'x-portal-target': 'admin',
|
||||
Accept: "application/json",
|
||||
"x-portal-target": "admin",
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
}).catch(() => null);
|
||||
} finally {
|
||||
if (typeof sessionStorage !== 'undefined') {
|
||||
sessionStorage.removeItem('nxtgauge_admin_access_token');
|
||||
sessionStorage.removeItem('nxtgauge_admin_preview');
|
||||
if (typeof sessionStorage !== "undefined") {
|
||||
sessionStorage.removeItem("nxtgauge_admin_access_token");
|
||||
sessionStorage.removeItem("nxtgauge_admin_preview");
|
||||
}
|
||||
clearAdminSession();
|
||||
navigate('/login', { replace: true });
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -369,7 +454,10 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const refreshTabIndicator = () => {
|
||||
const activeTab = tabs().find((tab) => isTabActive(tab));
|
||||
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];
|
||||
if (!el) return;
|
||||
setTabIndicator({ left: el.offsetLeft, width: el.offsetWidth, ready: true });
|
||||
|
|
@ -383,56 +471,47 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
|
||||
createEffect(() => {
|
||||
location.pathname;
|
||||
setRouteTransitioning(true);
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => setRouteTransitioning(false));
|
||||
});
|
||||
|
||||
if (!contentScrollRef) return;
|
||||
const prefersReducedMotion = typeof window !== 'undefined'
|
||||
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
: false;
|
||||
contentScrollRef.scrollTo({
|
||||
top: 0,
|
||||
behavior: prefersReducedMotion ? 'auto' : 'smooth',
|
||||
});
|
||||
if (contentScrollRef) {
|
||||
contentScrollRef.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const savedTheme = (typeof localStorage !== 'undefined'
|
||||
? localStorage.getItem('nxtgauge_admin_theme')
|
||||
: null) as 'light' | 'dark' | null;
|
||||
const nextTheme = savedTheme === 'dark' ? 'dark' : 'light';
|
||||
const savedTheme = (
|
||||
typeof localStorage !== "undefined" ? localStorage.getItem("nxtgauge_admin_theme") : null
|
||||
) as "light" | "dark" | null;
|
||||
const nextTheme = savedTheme === "dark" ? "dark" : "light";
|
||||
setTheme(nextTheme);
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', nextTheme);
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.setAttribute("data-theme", nextTheme);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', refreshTabIndicator);
|
||||
onCleanup(() => window.removeEventListener('resize', refreshTabIndicator));
|
||||
window.addEventListener("resize", refreshTabIndicator);
|
||||
onCleanup(() => window.removeEventListener("resize", refreshTabIndicator));
|
||||
|
||||
// Fetch unread notification count and poll every 30 seconds
|
||||
const fetchUnreadCount = async () => {
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const accessToken =
|
||||
typeof sessionStorage !== "undefined"
|
||||
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||
: "";
|
||||
if (!accessToken) return;
|
||||
const res = await fetch('/api/me/notifications/unread-count', {
|
||||
method: 'GET',
|
||||
const res = await fetch("/api/me/notifications/unread-count", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'x-portal-target': 'admin',
|
||||
Accept: "application/json",
|
||||
"x-portal-target": "admin",
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setUnreadCount(data.unread_count || 0);
|
||||
}
|
||||
} 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);
|
||||
onCleanup(() => clearInterval(interval));
|
||||
|
||||
const isPreview = searchParams._preview === '1' ||
|
||||
(typeof sessionStorage !== 'undefined' && sessionStorage.getItem('nxtgauge_admin_preview') === '1');
|
||||
const isPreview =
|
||||
searchParams._preview === "1" ||
|
||||
(typeof sessionStorage !== "undefined" &&
|
||||
sessionStorage.getItem("nxtgauge_admin_preview") === "1");
|
||||
|
||||
if (isPreview) {
|
||||
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem('nxtgauge_admin_preview', '1');
|
||||
if (typeof sessionStorage !== "undefined")
|
||||
sessionStorage.setItem("nxtgauge_admin_preview", "1");
|
||||
setAdminSession();
|
||||
setCheckedSession(true);
|
||||
return;
|
||||
|
|
@ -452,52 +534,57 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
|
||||
const verify = async () => {
|
||||
if (!hasAdminSession()) {
|
||||
navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, { replace: true });
|
||||
navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const response = await fetch('/api/auth/session', {
|
||||
method: 'GET',
|
||||
const accessToken =
|
||||
typeof sessionStorage !== "undefined"
|
||||
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
|
||||
: "";
|
||||
const response = await fetch("/api/auth/session", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'x-portal-target': 'admin',
|
||||
Accept: "application/json",
|
||||
"x-portal-target": "admin",
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
});
|
||||
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);
|
||||
|
||||
const roleKey = String(
|
||||
payload?.active_role
|
||||
|| payload?.role
|
||||
|| payload?.user?.active_role
|
||||
|| payload?.user?.active_role_key
|
||||
|| payload?.user?.role
|
||||
|| payload?.user?.role_key
|
||||
|| '',
|
||||
payload?.active_role ||
|
||||
payload?.role ||
|
||||
payload?.user?.active_role ||
|
||||
payload?.user?.active_role_key ||
|
||||
payload?.user?.role ||
|
||||
payload?.user?.role_key ||
|
||||
""
|
||||
).toUpperCase();
|
||||
setIsSuperAdmin(roleKey === 'SUPER_ADMIN');
|
||||
setIsSuperAdmin(roleKey === "SUPER_ADMIN");
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/runtime-config', {
|
||||
method: 'GET',
|
||||
const res = await fetch("/api/runtime-config", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'x-portal-target': 'admin',
|
||||
Accept: "application/json",
|
||||
"x-portal-target": "admin",
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
});
|
||||
const runtime = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
setAllowedModules(normalizeAllowedModules(runtime));
|
||||
const activeRole = String(runtime?.active_role || runtime?.user?.active_role || roleKey || '').toUpperCase();
|
||||
if (activeRole) setIsSuperAdmin(activeRole === 'SUPER_ADMIN');
|
||||
const activeRole = String(
|
||||
runtime?.active_role || runtime?.user?.active_role || roleKey || ""
|
||||
).toUpperCase();
|
||||
if (activeRole) setIsSuperAdmin(activeRole === "SUPER_ADMIN");
|
||||
} else {
|
||||
setAllowedModules(null);
|
||||
}
|
||||
|
|
@ -508,7 +595,9 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
setCheckedSession(true);
|
||||
} catch {
|
||||
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 path = location.pathname;
|
||||
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 'Admin';
|
||||
return "Admin";
|
||||
});
|
||||
|
||||
const adminInitials = createMemo(() => {
|
||||
if (adminName().trim().toLowerCase() === 'admin user') return 'AD';
|
||||
const parts = adminName().split(' ').map((s) => s.trim()).filter(Boolean);
|
||||
if (parts.length === 0) return 'U';
|
||||
if (adminName().trim().toLowerCase() === "admin user") return "AD";
|
||||
const parts = adminName()
|
||||
.split(" ")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
if (parts.length === 0) return "U";
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const t = theme();
|
||||
if (typeof localStorage !== 'undefined') localStorage.setItem('nxtgauge_admin_theme', t);
|
||||
if (typeof document !== 'undefined') document.documentElement.setAttribute('data-theme', t);
|
||||
if (typeof localStorage !== "undefined") localStorage.setItem("nxtgauge_admin_theme", t);
|
||||
if (typeof document !== "undefined") document.documentElement.setAttribute("data-theme", t);
|
||||
});
|
||||
|
||||
const toggleTheme = () => setTheme((v) => (v === 'dark' ? 'light' : 'dark'));
|
||||
const isDark = () => theme() === 'dark';
|
||||
const toggleTheme = () => setTheme((v) => (v === "dark" ? "light" : "dark"));
|
||||
const isDark = () => theme() === "dark";
|
||||
|
||||
createEffect(() => {
|
||||
if (!checkedSession()) return;
|
||||
|
|
@ -550,29 +646,53 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
if (!modules || modules.length === 0) return;
|
||||
|
||||
const path = location.pathname;
|
||||
if (path === '/admin') return;
|
||||
if (path === "/admin") return;
|
||||
|
||||
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];
|
||||
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()));
|
||||
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 (
|
||||
<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
|
||||
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 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">
|
||||
<AdminSidebar
|
||||
|
|
@ -588,24 +708,43 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
</div>
|
||||
|
||||
<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;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} />}>
|
||||
<Sun size={18} />
|
||||
</Show>
|
||||
</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">
|
||||
<Bell size={18} />
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<Bell size={18} />
|
||||
<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} />
|
||||
</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
|
||||
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"
|
||||
|
|
@ -615,14 +754,22 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
{adminInitials()}
|
||||
</div>
|
||||
<div style="text-align:left">
|
||||
<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>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
|
@ -631,18 +778,20 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
</header>
|
||||
|
||||
<div
|
||||
ref={(el) => { contentScrollRef = el; }}
|
||||
ref={(el) => {
|
||||
contentScrollRef = el;
|
||||
}}
|
||||
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
|
||||
class="admin-main"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '28px 24px 36px 24px',
|
||||
filter: isDark() ? 'brightness(0.96)' : 'none',
|
||||
transition: 'opacity 150ms ease',
|
||||
opacity: routeTransitioning() ? '0.92' : '1',
|
||||
width: "100%",
|
||||
padding: "28px 24px 36px 24px",
|
||||
filter: isDark() ? "brightness(0.96)" : "none",
|
||||
transition: "opacity 150ms ease",
|
||||
opacity: routeTransitioning() ? "0.92" : "1",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,36 +1,37 @@
|
|||
import { A, useParams } from '@solidjs/router';
|
||||
import { createMemo } from 'solid-js';
|
||||
import ApprovalManagementPage from './approval';
|
||||
import VerificationManagementPage from './verification';
|
||||
import UsersManagementPage from './users';
|
||||
import ExternalDashboardManagementPage from './external-dashboard-management';
|
||||
import InternalDashboardManagementPage from './internal-dashboard-management';
|
||||
import { A, useParams } from "@solidjs/router";
|
||||
import { createMemo, lazy } from "solid-js";
|
||||
|
||||
const ApprovalManagementPage = lazy(() => import("./approval"));
|
||||
const VerificationManagementPage = lazy(() => import("./verification"));
|
||||
const UsersManagementPage = lazy(() => import("./users"));
|
||||
const ExternalDashboardManagementPage = lazy(() => import("./external-dashboard-management"));
|
||||
const InternalDashboardManagementPage = lazy(() => import("./internal-dashboard-management"));
|
||||
|
||||
function toTitle(value: string): string {
|
||||
return value
|
||||
.split(/[-_/]/g)
|
||||
.filter(Boolean)
|
||||
.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 {
|
||||
switch (modulePath) {
|
||||
case 'roles':
|
||||
return '/roles?scope=internal';
|
||||
case 'approval-management':
|
||||
case 'approvals':
|
||||
return '/approval';
|
||||
case 'onboarding-management':
|
||||
return '/external-dashboard-management';
|
||||
case 'internal-dashboard-management':
|
||||
return '/internal-dashboard-management';
|
||||
case 'external-dashboard-management':
|
||||
return '/external-dashboard-management';
|
||||
case 'support':
|
||||
return '/help';
|
||||
case "roles":
|
||||
return "/roles?scope=internal";
|
||||
case "approval-management":
|
||||
case "approvals":
|
||||
return "/approval";
|
||||
case "onboarding-management":
|
||||
return "/external-dashboard-management";
|
||||
case "internal-dashboard-management":
|
||||
return "/internal-dashboard-management";
|
||||
case "external-dashboard-management":
|
||||
return "/external-dashboard-management";
|
||||
case "support":
|
||||
return "/help";
|
||||
default:
|
||||
return `/${modulePath}`;
|
||||
}
|
||||
|
|
@ -38,29 +39,42 @@ function resolveLegacyPath(modulePath: string): string {
|
|||
|
||||
export default function LegacyModuleShellPage() {
|
||||
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 />;
|
||||
}
|
||||
|
||||
if (modulePath === 'verification' || modulePath === 'verification-status' || modulePath === 'verification-management') {
|
||||
if (
|
||||
modulePath === "verification" ||
|
||||
modulePath === "verification-status" ||
|
||||
modulePath === "verification-management"
|
||||
) {
|
||||
return <VerificationManagementPage />;
|
||||
}
|
||||
|
||||
if (modulePath === 'users' || modulePath === 'users-management' || modulePath === 'user-management') {
|
||||
if (
|
||||
modulePath === "users" ||
|
||||
modulePath === "users-management" ||
|
||||
modulePath === "user-management"
|
||||
) {
|
||||
return <UsersManagementPage />;
|
||||
}
|
||||
|
||||
if (modulePath === 'external-dashboard-management' || modulePath === 'onboarding-management') {
|
||||
if (modulePath === "external-dashboard-management" || modulePath === "onboarding-management") {
|
||||
return <ExternalDashboardManagementPage />;
|
||||
}
|
||||
|
||||
if (modulePath === 'internal-dashboard-management') {
|
||||
if (modulePath === "internal-dashboard-management") {
|
||||
return <InternalDashboardManagementPage />;
|
||||
}
|
||||
|
||||
const moduleName = createMemo(() => toTitle(modulePath || 'Management'));
|
||||
const moduleName = createMemo(() => toTitle(modulePath || "Management"));
|
||||
const legacyPath = createMemo(() => resolveLegacyPath(modulePath));
|
||||
const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`);
|
||||
|
||||
|
|
@ -72,12 +86,24 @@ export default function LegacyModuleShellPage() {
|
|||
</p>
|
||||
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
|
||||
<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>
|
||||
<iframe
|
||||
src={legacyUrl()}
|
||||
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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,23 +5,11 @@ import type { CrudRecord } from "~/lib/admin/types";
|
|||
const API = "";
|
||||
|
||||
type DepartmentRecord = CrudRecord & {
|
||||
code?: string;
|
||||
description?: string;
|
||||
totalEmployees?: number;
|
||||
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 = {
|
||||
departments?: any[];
|
||||
data?: any[];
|
||||
|
|
@ -36,11 +24,8 @@ function normalizeDepartment(item: any, idx: number): DepartmentRecord {
|
|||
return {
|
||||
id: String(item.id ?? `dep-${idx + 1}`),
|
||||
name: String(item.name ?? ""),
|
||||
code: item.code ? String(item.code) : undefined,
|
||||
description: item.description ? String(item.description) : undefined,
|
||||
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",
|
||||
updatedAt: String(item.updated_at ?? ""),
|
||||
createdDate: String(item.created_at ?? ""),
|
||||
|
|
@ -92,7 +77,7 @@ export default function DepartmentManagementPage() {
|
|||
const isPreview = () => searchParams._preview === "1";
|
||||
|
||||
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 [search, setSearch] = createSignal("");
|
||||
const [statusFilter, setStatusFilter] = createSignal("all");
|
||||
|
|
@ -110,10 +95,7 @@ export default function DepartmentManagementPage() {
|
|||
const [isDeleting, setIsDeleting] = createSignal(false);
|
||||
|
||||
const [name, setName] = createSignal("");
|
||||
const [code, setCode] = createSignal("");
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [departmentHead, setDepartmentHead] = createSignal("");
|
||||
const [departmentEmail, setDepartmentEmail] = createSignal("");
|
||||
const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE");
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
|
|
@ -172,9 +154,6 @@ export default function DepartmentManagementPage() {
|
|||
r = r.filter(
|
||||
(d) =>
|
||||
d.name.toLowerCase().includes(q) ||
|
||||
String(d.code ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q) ||
|
||||
String(d.description ?? "")
|
||||
.toLowerCase()
|
||||
.includes(q)
|
||||
|
|
@ -197,10 +176,7 @@ export default function DepartmentManagementPage() {
|
|||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setName("");
|
||||
setCode("");
|
||||
setDescription("");
|
||||
setDepartmentHead("");
|
||||
setDepartmentEmail("");
|
||||
setStatus("ACTIVE");
|
||||
setFormTab("general");
|
||||
setError("");
|
||||
|
|
@ -214,10 +190,7 @@ export default function DepartmentManagementPage() {
|
|||
const openEdit = (row: DepartmentRecord) => {
|
||||
setEditingId(row.id);
|
||||
setName(row.name || "");
|
||||
setCode(String(row.code || ""));
|
||||
setDescription(String(row.description || ""));
|
||||
setDepartmentHead(String(row.departmentHead || ""));
|
||||
setDepartmentEmail(String(row.departmentEmail || ""));
|
||||
setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE");
|
||||
setFormTab("general");
|
||||
setView("form");
|
||||
|
|
@ -230,20 +203,12 @@ export default function DepartmentManagementPage() {
|
|||
setFormTab("general");
|
||||
return;
|
||||
}
|
||||
if (!code().trim()) {
|
||||
setError("Department code is required.");
|
||||
setFormTab("general");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
setError("");
|
||||
const payload = {
|
||||
name: name().trim(),
|
||||
code: code().trim() || null,
|
||||
description: description().trim() || null,
|
||||
department_head: departmentHead().trim() || null,
|
||||
department_email: departmentEmail().trim() || null,
|
||||
status: status(),
|
||||
};
|
||||
|
||||
|
|
@ -358,34 +323,8 @@ export default function DepartmentManagementPage() {
|
|||
</div>
|
||||
<StatusBadge status={viewingDept()!.status} />
|
||||
</div>
|
||||
{/* Details grid — 3 cols using flex rows */}
|
||||
{/* Details grid — 2 cols using flex rows */}
|
||||
<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="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">
|
||||
|
|
@ -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">
|
||||
Department Name
|
||||
</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">
|
||||
Description
|
||||
</th>
|
||||
|
|
@ -586,7 +522,7 @@ export default function DepartmentManagementPage() {
|
|||
when={filteredRows().length > 0}
|
||||
fallback={
|
||||
<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]">
|
||||
No departments found
|
||||
</p>
|
||||
|
|
@ -613,11 +549,6 @@ export default function DepartmentManagementPage() {
|
|||
<td style="padding:12px 20px">
|
||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
||||
</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">
|
||||
<p style="font-size:13px;color:#6B7280;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{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">
|
||||
{/* Sub-tabs */}
|
||||
<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) => {
|
||||
const labels = ["General Information", "Department Settings", "Permissions"];
|
||||
{(["general", "settings"] as const).map((tab, i) => {
|
||||
const labels = ["General Information", "Department Settings"];
|
||||
const active = () => formTab() === tab;
|
||||
return (
|
||||
<button
|
||||
|
|
@ -919,22 +850,13 @@ export default function DepartmentManagementPage() {
|
|||
{/* General Information */}
|
||||
<Show when={formTab() === "general"}>
|
||||
<div style="display:flex;flex-direction:column;gap:20px">
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
<FormInput
|
||||
label="Department Name"
|
||||
required
|
||||
value={name()}
|
||||
onInput={setName}
|
||||
placeholder="e.g. Engineering"
|
||||
/>
|
||||
<FormInput
|
||||
label="Department Code"
|
||||
required
|
||||
value={code()}
|
||||
onInput={setCode}
|
||||
placeholder="e.g. ENG-001"
|
||||
/>
|
||||
</div>
|
||||
<FormInput
|
||||
label="Department Name"
|
||||
required
|
||||
value={name()}
|
||||
onInput={setName}
|
||||
placeholder="e.g. Engineering"
|
||||
/>
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
||||
<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"
|
||||
/>
|
||||
</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>
|
||||
</Show>
|
||||
|
||||
|
|
@ -983,98 +890,6 @@ export default function DepartmentManagementPage() {
|
|||
))}
|
||||
</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>
|
||||
</Show>
|
||||
</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 = "";
|
||||
|
||||
type DesignationRecord = CrudRecord & {
|
||||
code?: string;
|
||||
department?: string;
|
||||
departmentId?: string;
|
||||
level?: string;
|
||||
description?: string;
|
||||
totalEmployees?: number;
|
||||
createdDate?: string;
|
||||
canManageTeam?: boolean;
|
||||
canApprove?: boolean;
|
||||
};
|
||||
|
||||
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 = {
|
||||
designations?: any[];
|
||||
data?: any[];
|
||||
|
|
@ -41,14 +28,10 @@ function normalizeDesignation(item: any, idx: number): DesignationRecord {
|
|||
return {
|
||||
id: String(item.id ?? `des-${idx + 1}`),
|
||||
name: String(item.name ?? ""),
|
||||
code: item.code ? String(item.code) : undefined,
|
||||
department: item.department_name ? String(item.department_name) : undefined,
|
||||
departmentId: item.department_id ? String(item.department_id) : undefined,
|
||||
level: item.level ? String(item.level) : undefined,
|
||||
description: item.description ? String(item.description) : undefined,
|
||||
totalEmployees: Number(item.total_employees ?? 0),
|
||||
canManageTeam: Boolean(item.can_manage_team ?? false),
|
||||
canApprove: Boolean(item.can_approve ?? false),
|
||||
status: isActive ? "ACTIVE" : "INACTIVE",
|
||||
updatedAt: String(item.updated_at ?? ""),
|
||||
createdDate: String(item.created_at ?? ""),
|
||||
|
|
@ -123,7 +106,7 @@ export default function DesignationManagementPage() {
|
|||
const isPreview = () => searchParams._preview === "1";
|
||||
|
||||
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 [search, setSearch] = createSignal("");
|
||||
const [deptFilter, setDeptFilter] = createSignal("all");
|
||||
|
|
@ -142,13 +125,9 @@ export default function DesignationManagementPage() {
|
|||
const [isDeleting, setIsDeleting] = createSignal(false);
|
||||
|
||||
const [name, setName] = createSignal("");
|
||||
const [code, setCode] = createSignal("");
|
||||
const [departmentId, setDepartmentId] = createSignal("");
|
||||
const [level, setLevel] = createSignal("");
|
||||
const [description, setDescription] = createSignal("");
|
||||
const [status, setStatus] = createSignal<"ACTIVE" | "INACTIVE">("ACTIVE");
|
||||
const [canManageTeam, setCanManageTeam] = createSignal(false);
|
||||
const [canApprove, setCanApprove] = createSignal(false);
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
const [isSaving, setIsSaving] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
|
|
@ -278,12 +257,10 @@ export default function DesignationManagementPage() {
|
|||
});
|
||||
|
||||
const exportCsv = () => {
|
||||
const headers = ["Designation Name", "Code", "Department", "Level", "Employees", "Status"];
|
||||
const headers = ["Designation Name", "Department", "Employees", "Status"];
|
||||
const rowsData = filteredRows().map((row) => [
|
||||
row.name || "",
|
||||
row.code || "",
|
||||
row.department || "",
|
||||
row.level || "",
|
||||
String(row.totalEmployees ?? 0),
|
||||
row.status || "",
|
||||
]);
|
||||
|
|
@ -305,13 +282,9 @@ export default function DesignationManagementPage() {
|
|||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setName("");
|
||||
setCode("");
|
||||
setDepartmentId("");
|
||||
setLevel("");
|
||||
setDescription("");
|
||||
setStatus("ACTIVE");
|
||||
setCanManageTeam(false);
|
||||
setCanApprove(false);
|
||||
setFormTab("general");
|
||||
setError("");
|
||||
};
|
||||
|
|
@ -324,13 +297,9 @@ export default function DesignationManagementPage() {
|
|||
const openEdit = (row: DesignationRecord) => {
|
||||
setEditingId(row.id);
|
||||
setName(row.name || "");
|
||||
setCode(row.code || "");
|
||||
setDepartmentId(row.departmentId || "");
|
||||
setLevel(row.level || "");
|
||||
setDescription(row.description || "");
|
||||
setStatus(row.status === "INACTIVE" ? "INACTIVE" : "ACTIVE");
|
||||
setCanManageTeam(Boolean(row.canManageTeam));
|
||||
setCanApprove(Boolean(row.canApprove));
|
||||
setFormTab("general");
|
||||
setView("form");
|
||||
setOpenMenuId(null);
|
||||
|
|
@ -348,12 +317,8 @@ export default function DesignationManagementPage() {
|
|||
setError("");
|
||||
const payload: Record<string, unknown> = {
|
||||
name: name().trim(),
|
||||
code: code().trim() || null,
|
||||
level: level().trim() || null,
|
||||
description: description().trim() || null,
|
||||
status: status(),
|
||||
can_manage_team: canManageTeam(),
|
||||
can_approve: canApprove(),
|
||||
};
|
||||
if (departmentId().trim()) {
|
||||
payload.department_id = departmentId().trim();
|
||||
|
|
@ -481,14 +446,6 @@ export default function DesignationManagementPage() {
|
|||
{/* Details grid */}
|
||||
<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">
|
||||
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">
|
||||
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
|
||||
Department
|
||||
|
|
@ -497,14 +454,6 @@ export default function DesignationManagementPage() {
|
|||
{viewingRecord()!.department || "—"}
|
||||
</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">
|
||||
Level
|
||||
</p>
|
||||
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">
|
||||
{viewingRecord()!.level || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;border-bottom: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)}
|
||||
</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">
|
||||
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">
|
||||
<p style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">
|
||||
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">
|
||||
Designation Name
|
||||
</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">
|
||||
Department
|
||||
</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">
|
||||
Employees
|
||||
</th>
|
||||
|
|
@ -747,7 +682,7 @@ export default function DesignationManagementPage() {
|
|||
when={filteredRows().length > 0}
|
||||
fallback={
|
||||
<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]">
|
||||
No designations found
|
||||
</p>
|
||||
|
|
@ -774,19 +709,9 @@ export default function DesignationManagementPage() {
|
|||
<td style="padding:12px 20px">
|
||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.name}</p>
|
||||
</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">
|
||||
{String(row.department || "—")}
|
||||
</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">
|
||||
{Number(row.totalEmployees || 0)}
|
||||
</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">
|
||||
{/* Sub-tabs */}
|
||||
<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) => {
|
||||
const labels = ["General Information", "Designation Settings", "Permissions"];
|
||||
{(["general", "settings"] as const).map((tab, i) => {
|
||||
const labels = ["General Information", "Designation Settings"];
|
||||
const active = () => formTab() === tab;
|
||||
return (
|
||||
<button
|
||||
|
|
@ -1082,34 +1007,10 @@ export default function DesignationManagementPage() {
|
|||
onInput={setName}
|
||||
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}>
|
||||
<option value="">Select department</option>
|
||||
<For each={departments()}>{(d) => <option value={d.id}>{d.name}</option>}</For>
|
||||
</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>
|
||||
<label style="display:block">
|
||||
<span style="font-size:13px;font-weight:600;color:#374151">Description</span>
|
||||
|
|
@ -1144,75 +1045,6 @@ export default function DesignationManagementPage() {
|
|||
))}
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,12 +90,15 @@ export default function CreateEmployeePage() {
|
|||
const [depts] = createResource(fetchDepts);
|
||||
const [desigs] = createResource(fetchDesigs);
|
||||
|
||||
const [fullName, setFullName] = createSignal("");
|
||||
const [firstName, setFirstName] = createSignal("");
|
||||
const [lastName, setLastName] = createSignal("");
|
||||
const [email, setEmail] = createSignal("");
|
||||
const [employeeCode, setEmployeeCode] = createSignal("");
|
||||
const [createLoginCreds, setCreateLoginCreds] = createSignal(true);
|
||||
const [loginPassword, setLoginPassword] = createSignal("");
|
||||
const [confirmLoginPassword, setConfirmLoginPassword] = createSignal("");
|
||||
const [showLoginPassword, setShowLoginPassword] = createSignal(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = createSignal(false);
|
||||
const [roleId, setRoleId] = createSignal("");
|
||||
const [deptId, setDeptId] = createSignal("");
|
||||
const [desigId, setDesigId] = createSignal("");
|
||||
|
|
@ -157,8 +160,12 @@ export default function CreateEmployeePage() {
|
|||
|
||||
const handleSave = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
if (!fullName().trim()) {
|
||||
setError("Full name is required");
|
||||
if (!firstName().trim()) {
|
||||
setError("First name is required");
|
||||
return;
|
||||
}
|
||||
if (!lastName().trim()) {
|
||||
setError("Last name is required");
|
||||
return;
|
||||
}
|
||||
if (!email().trim()) {
|
||||
|
|
@ -204,8 +211,8 @@ export default function CreateEmployeePage() {
|
|||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
email: email().trim(),
|
||||
first_name: fullName().trim().split(" ")[0] || "",
|
||||
last_name: fullName().trim().split(" ").slice(1).join(" ") || "",
|
||||
first_name: firstName().trim(),
|
||||
last_name: lastName().trim(),
|
||||
role_code: roleId(),
|
||||
department_id: deptId().trim(),
|
||||
designation_id: desigId().trim(),
|
||||
|
|
@ -260,17 +267,33 @@ export default function CreateEmployeePage() {
|
|||
|
||||
<form onSubmit={handleSave} class="p-6 space-y-5">
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
{/* Full Name */}
|
||||
{/* First Name */}
|
||||
<div>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="e.g. Arjun Sharma"
|
||||
value={fullName()}
|
||||
onInput={(e) => setFullName(e.currentTarget.value)}
|
||||
placeholder="e.g. Arjun"
|
||||
value={firstName()}
|
||||
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"
|
||||
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">
|
||||
Login Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={loginPassword()}
|
||||
onInput={(e) => setLoginPassword(e.currentTarget.value)}
|
||||
placeholder="Minimum 8 characters"
|
||||
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 style="position:relative">
|
||||
<input
|
||||
type={showLoginPassword() ? "text" : "password"}
|
||||
value={loginPassword()}
|
||||
onInput={(e) => setLoginPassword(e.currentTarget.value)}
|
||||
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>
|
||||
<label class="block text-[13px] font-medium text-[#111827] mb-1.5">
|
||||
Confirm Password <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmLoginPassword()}
|
||||
onInput={(e) => setConfirmLoginPassword(e.currentTarget.value)}
|
||||
placeholder="Repeat password"
|
||||
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 style="position:relative">
|
||||
<input
|
||||
type={showConfirmPassword() ? "text" : "password"}
|
||||
value={confirmLoginPassword()}
|
||||
onInput={(e) => setConfirmLoginPassword(e.currentTarget.value)}
|
||||
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>
|
||||
</>
|
||||
</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]"
|
||||
>
|
||||
<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>
|
||||
</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 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 = {
|
||||
id: string;
|
||||
roleId: string;
|
||||
|
|
@ -18,19 +39,99 @@ type ExternalDashboard = {
|
|||
previewPath: string;
|
||||
status: "ACTIVE" | "INACTIVE" | "DRAFT";
|
||||
updatedAt: string;
|
||||
pkgCardColors?: Record<string, string>;
|
||||
};
|
||||
|
||||
const AVAILABLE_WIDGETS = [
|
||||
"kpi_summary",
|
||||
"pending_approvals",
|
||||
"user_growth",
|
||||
"active_sessions",
|
||||
"system_health",
|
||||
"recent_activity",
|
||||
"quick_actions",
|
||||
"team_performance",
|
||||
const ROLE_WIDGETS: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER", string[]> = {
|
||||
CUSTOMER: [
|
||||
"total_requirements",
|
||||
"open_requirements",
|
||||
"closed_requirements",
|
||||
"responses_received",
|
||||
"shortlisted_responses",
|
||||
"credits",
|
||||
],
|
||||
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[]> =
|
||||
{
|
||||
PROFESSIONAL: [
|
||||
|
|
@ -44,7 +145,6 @@ const ROLE_BASED_SIDEBAR: Record<"PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CU
|
|||
"Verification",
|
||||
"Help Center",
|
||||
"Settings",
|
||||
"Switch Services",
|
||||
"Logout",
|
||||
],
|
||||
COMPANY: [
|
||||
|
|
@ -193,6 +293,23 @@ function rolePreviewPath(roleKey: string): string {
|
|||
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[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
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 fields = asStringArray(cfg?.fields);
|
||||
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 =
|
||||
item?.is_active === false || String(item?.status || "").toUpperCase() === "INACTIVE";
|
||||
|
|
@ -227,6 +345,7 @@ function normalizeDashboard(item: any): ExternalDashboard {
|
|||
previewPath,
|
||||
status: isInactive ? "INACTIVE" : isDraft ? "DRAFT" : "ACTIVE",
|
||||
updatedAt: String(item?.updated_at || ""),
|
||||
pkgCardColors,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -263,7 +382,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
const [view, setView] = createSignal<"list" | "form">("list");
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [formTab, setFormTab] = createSignal<
|
||||
"general" | "tabs" | "sidebar" | "fields" | "preview" | "full_preview"
|
||||
"general" | "tabs" | "sidebar" | "fields" | "theme" | "preview" | "full_preview"
|
||||
>("general");
|
||||
const [isFullscreenPreview, setIsFullscreenPreview] = createSignal(false);
|
||||
const [listTab, setListTab] = createSignal<"all" | "create">("all");
|
||||
|
|
@ -290,6 +409,22 @@ export default function ExternalDashboardManagementPage() {
|
|||
const [activePreviewSidebar, setActivePreviewSidebar] = 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 map: Record<string, "PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER"> = {};
|
||||
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.
|
||||
};
|
||||
|
||||
const availableWidgetOptions = createMemo(() => {
|
||||
const persona = personaFromKey(formRoleKey()) || "PROFESSIONAL";
|
||||
return ROLE_WIDGETS[persona] || [];
|
||||
});
|
||||
|
||||
const sidebarLooksCustomer = createMemo(() => {
|
||||
const joined = sidebarItems().join(" ").toLowerCase();
|
||||
return (
|
||||
|
|
@ -340,10 +480,17 @@ export default function ExternalDashboardManagementPage() {
|
|||
: sidebarLooksCustomer()
|
||||
? ROLE_BASED_SIDEBAR.CUSTOMER
|
||||
: ["My Dashboard", "My Profile", "Switch Services", "Logout"];
|
||||
return mergeSidebarForPersona(
|
||||
const merged = mergeSidebarForPersona(
|
||||
baseItems,
|
||||
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"]));
|
||||
|
||||
|
|
@ -400,120 +547,111 @@ export default function ExternalDashboardManagementPage() {
|
|||
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 () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const [dashRes, rolesRes] = await Promise.all([
|
||||
fetch(`${API}/api/admin/dashboard-config`, {
|
||||
const [rolesRes, modulesRes] = await Promise.all([
|
||||
fetch(`${API}/api/admin/external-roles?per_page=200`, {
|
||||
headers: authHeaders(),
|
||||
credentials: "include",
|
||||
}),
|
||||
fetch(`${API}/api/admin/roles?audience=EXTERNAL&per_page=200`, {
|
||||
fetch(`${API}/api/admin/modules`, {
|
||||
headers: authHeaders(),
|
||||
credentials: "include",
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!dashRes.ok) throw new Error(`Failed to load dashboard config (${dashRes.status})`);
|
||||
if (!rolesRes.ok) throw new Error(`Failed to load external roles (${rolesRes.status})`);
|
||||
if (!rolesRes.ok) throw new Error(`Failed to load 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(() => []);
|
||||
if (looksLikeGateway404(dashData) || looksLikeGateway404(roleData)) {
|
||||
throw new Error("Required admin runtime endpoints are unavailable.");
|
||||
}
|
||||
const modulesData = await modulesRes.json().catch(() => []);
|
||||
|
||||
const dashRows = Array.isArray(dashData)
|
||||
? dashData
|
||||
: dashData?.items || dashData?.configs || [];
|
||||
const roleRows = Array.isArray(roleData)
|
||||
? roleData
|
||||
: roleData?.roles || roleData?.items || [];
|
||||
const roleRows = roleData?.roles || roleData?.items || roleData || [];
|
||||
const modules: Module[] = modulesData?.items || modulesData || [];
|
||||
|
||||
const moduleByKey = new Map(modules.map((m: Module) => [m.module_key, m]));
|
||||
|
||||
setRoles(
|
||||
roleRows
|
||||
.filter((r: any) => {
|
||||
const hasRoleShape = Boolean(r?.id && (r?.key || r?.name));
|
||||
if (hasRoleShape && r?.audience == null) return true;
|
||||
return String(r?.audience || "").toUpperCase() === "EXTERNAL";
|
||||
return r?.status === "ACTIVE" || r?.status === "INACTIVE";
|
||||
})
|
||||
.map((r: any) => ({
|
||||
id: String(r?.id || ""),
|
||||
key: String(r?.key || "").toUpperCase(),
|
||||
key: String(r?.code || r?.key || "").toUpperCase(),
|
||||
name: String(r?.name || r?.key || "External Role"),
|
||||
}))
|
||||
.filter((r: RoleOption) => r.id)
|
||||
);
|
||||
|
||||
const roleKeyById = new Map<string, string>();
|
||||
roleRows.forEach((r: any) => {
|
||||
const id = String(r?.id || "").trim();
|
||||
const key = String(r?.key || "")
|
||||
.toUpperCase()
|
||||
.trim();
|
||||
if (id) roleKeyById.set(id, key);
|
||||
});
|
||||
const permissionMap: Record<string, RolePermission[]> = {};
|
||||
for (const role of roleRows) {
|
||||
try {
|
||||
const permRes = await fetch(`${API}/api/admin/roles/${role.id}/permissions`, {
|
||||
headers: authHeaders(),
|
||||
credentials: "include",
|
||||
});
|
||||
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 {
|
||||
...row,
|
||||
roleKey: resolvedRoleKey || row.roleKey,
|
||||
sidebarItems: mergeSidebarForPersona(row.sidebarItems, persona),
|
||||
id: `dashboard-${roleId}`,
|
||||
roleId,
|
||||
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.
|
||||
const fallbackRows = externalDashRows.length
|
||||
? 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)
|
||||
)
|
||||
);
|
||||
setRows(dashboardRows.sort((a: ExternalDashboard, b: ExternalDashboard) =>
|
||||
b.updatedAt.localeCompare(a.updatedAt)
|
||||
));
|
||||
} catch (e: any) {
|
||||
setRows([]);
|
||||
setRoles([]);
|
||||
|
|
@ -562,6 +700,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
setIsActive(true);
|
||||
setActivePreviewSidebar("My Profile");
|
||||
setActivePreviewTab("basic info");
|
||||
setPkgCardColors({});
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
|
|
@ -586,6 +725,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
setIsActive(row.status === "ACTIVE");
|
||||
setActivePreviewSidebar("My Profile");
|
||||
setActivePreviewTab("basic info");
|
||||
setPkgCardColors(row.pkgCardColors || {});
|
||||
setListTab("create");
|
||||
setView("form");
|
||||
};
|
||||
|
|
@ -597,6 +737,8 @@ export default function ExternalDashboardManagementPage() {
|
|||
const matchedRole = roles().find((r) => r.id === selected);
|
||||
if (matchedRole) setFormRoleKey(matchedRole.key);
|
||||
applySidebarPresetForRole(selected, false);
|
||||
applyTabsPresetForRole(selected, false);
|
||||
applyFieldsPresetForRole(selected, false);
|
||||
applyPreviewPathForRole(selected, false);
|
||||
});
|
||||
|
||||
|
|
@ -616,6 +758,14 @@ export default function ExternalDashboardManagementPage() {
|
|||
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(() => {
|
||||
const list = tabs(); // use configured tabs only — not the fallback ['overview']
|
||||
if (!list.length) return; // sidebar-driven tabs (profile, portfolio, etc.) manage validation internally
|
||||
|
|
@ -667,6 +817,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
enabled: true,
|
||||
intent: "role_marketplace_and_profile_verification",
|
||||
},
|
||||
pkg_card: pkgCardColors(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
@ -721,7 +872,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
</div>
|
||||
|
||||
<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) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -787,7 +938,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
Available Widgets
|
||||
</p>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
|
||||
<For each={AVAILABLE_WIDGETS}>
|
||||
<For each={availableWidgetOptions()}>
|
||||
{(key) => (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -919,6 +1070,160 @@ export default function ExternalDashboardManagementPage() {
|
|||
</div>
|
||||
</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"}>
|
||||
<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">
|
||||
|
|
@ -952,6 +1257,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
roleKey={selectedRoleKey() || formRoleKey()}
|
||||
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
|
||||
onOpenFullscreen={() => setFormTab("full_preview")}
|
||||
dashboardConfig={{ pkg_card: pkgCardColors() }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
|
@ -998,6 +1304,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
mode={"customer_external"}
|
||||
roleKey={selectedRoleKey() || formRoleKey()}
|
||||
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
|
||||
dashboardConfig={{ pkg_card: pkgCardColors() }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1307,6 +1614,7 @@ export default function ExternalDashboardManagementPage() {
|
|||
mode={"customer_external"}
|
||||
roleKey={selectedRoleKey() || formRoleKey()}
|
||||
exploreRoles={roles().map((r) => ({ key: r.key, name: r.name }))}
|
||||
dashboardConfig={{ pkg_card: pkgCardColors() }}
|
||||
/>
|
||||
</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 { loadAdminDashboardLayout, saveAdminDashboardLayout } from "~/lib/runtime/storage";
|
||||
|
||||
const API = "/api";
|
||||
const API = "";
|
||||
|
||||
async function fetchMetrics() {
|
||||
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