feat: unify API paths and upgrade table UIs
- Replace all /api/gateway/* with /api/* to match gateway routing - Fix AdminShell.tsx: update UGC route to singular and fix logout URL - Remove Applications and Responses from sidebar (unused) - Move conflicting route files into folders (company, approval, verification, users, jobs, kb, leads, photographer) as index.tsx to avoid catch-all interference - Upgrade ProfessionAdminListPage to match Department Management UI: • Dark headers with white text • Icons on Sort/Filters/Export buttons • Pagination UI • Improved empty state with Create button • Hover effects and consistent spacing - Update all pages using ProfessionAdminListPage to benefit from new UI - Fix jobs admin endpoint to use /api/admin/companies/jobs with auth - Add authentication headers to jobs and leads fetch calls These changes unify the API architecture and bring a consistent, professional look to all management tables.
This commit is contained in:
parent
ead325a3e3
commit
0ec64be905
70 changed files with 2947 additions and 1962 deletions
961
admin.log
Normal file
961
admin.log
Normal file
|
|
@ -0,0 +1,961 @@
|
||||||
|
|
||||||
|
> dev
|
||||||
|
> vinxi dev
|
||||||
|
|
||||||
|
vinxi v0.5.11
|
||||||
|
vinxi found vinxi app config in vite.config.ts
|
||||||
|
vinxi starting dev server
|
||||||
|
|
||||||
|
➜ Local: http://localhost:3000/
|
||||||
|
➜ Network: use --host to expose
|
||||||
|
|
||||||
|
3:56:34 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:56:34 PM [vite] (client) page reload src/routes/admin/company.tsx
|
||||||
|
3:56:34 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:56:34 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:56:34 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/routes/admin/approval.tsx, /src/app.css, /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/approval.tsx?pick=default&pick=$css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/jobs.tsx
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/kb.tsx
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/leads.tsx
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/photographer.tsx
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/routes/admin/users.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/verification-status.tsx
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/routes/admin/verification.tsx, /src/app.css, /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/verification.tsx?pick=default&pick=$css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
3:57:27 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/approval/index.tsx
|
||||||
|
3:57:27 PM [vite] (client) page reload src/routes/admin/verification/index.tsx
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/components/AdminShell.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/components/AdminSidebar.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/ExternalRoleForm.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/ProfessionAdminListPage.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/UserListPage.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/OnboardingManagementTabs.tsx
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/components/admin/DashboardDesignPreview.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/OnboardingFlowBuilder.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/RoleUserManagementTablePage.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/AdminUi.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/components/admin/ExternalRoleTabs.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Button.stories.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Page.stories.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/admin/AdminPages.stories.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/AdminDashboard.stories.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Header.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Button.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Page.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/stories/Header.stories.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/entry-server.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload src/entry-server.tsx
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/runtime/storage.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/runtime/types.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin/dashboard.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin/types.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin/client.ts
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin/module-access.test.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/server/gateway.ts
|
||||||
|
4:03:29 PM [vite] (ssr) page reload src/lib/server/gateway.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin-auth.test.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/admin-modules.ts
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
4:03:29 PM [vite] (client) page reload src/lib/sidebar-state.ts
|
||||||
|
4:03:29 PM [vite] (client) page reload src/global.d.ts
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/approval-management.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/graphic-designers.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/pricing.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/external-roles.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/tutors.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/order.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/discount.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/developers.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/articles/new.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/articles/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/articles/[id]/edit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/articles.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/kb/categories.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/financial/reconcile.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/financial/adjust-credit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/support.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/designation.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/approvals.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/leads/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/leads/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/workspace/[menuId].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/ugc-content-creator.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/department-management.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/review.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/roles/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/roles/templates.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/roles/create.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/roles/[id]/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/roles/[id]/edit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/requirements/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/runtime-roles/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/runtime-roles/new.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/runtime-roles/[roleKey].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/modules.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/designation-management.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/external-dashboard-management/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/department.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/verification/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/notifications.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/role-ui-configs/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/role-ui-configs/new.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/role-ui-configs/[roleKey].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/credit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/profile/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/catering-services.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/customer.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/users/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/users/details/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/users/[id]/edit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/tax.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/invoice.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/photographer/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/photographer/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/verification-status/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/verification-status/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/video-editors.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/report.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/coupon.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/ledger.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/jobs/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/jobs/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/new.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/[schemaId].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/makeup-artist.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/social-media-managers.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/applications.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/responses.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/fitness-trainers.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/help/support-bridge.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/help/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/approval/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/company/create.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/company/[id].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/employees/index.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/employees/create.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/admin/employees/[id]/edit.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/[...404].tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/api/gateway/[...path].ts
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
4:03:29 PM [vite] (client) page reload src/routes/about.tsx
|
||||||
|
4:03:29 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
4:03:29 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/company/index.tsx?pick=default&pick=$css
|
||||||
|
4:03:29 PM [vite] (client) hmr invalidate /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx
|
||||||
|
4:03:29 PM [vite] (client) hmr update /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/components/AdminShell.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/components/AdminSidebar.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/ExternalRoleForm.tsx
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/components/admin/ProfessionAdminListPage.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/UserListPage.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/OnboardingManagementTabs.tsx
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/components/admin/DashboardDesignPreview.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/OnboardingFlowBuilder.tsx
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/components/admin/RoleUserManagementTablePage.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/AdminUi.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/components/admin/ExternalRoleTabs.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Button.stories.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Page.stories.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/admin/AdminPages.stories.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/AdminDashboard.stories.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Header.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Button.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Page.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/stories/Header.stories.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/entry-server.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload src/entry-server.tsx
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/runtime/storage.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/runtime/types.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin/dashboard.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin/types.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin/client.ts
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin/module-access.test.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/server/gateway.ts
|
||||||
|
6:06:53 PM [vite] (ssr) page reload src/lib/server/gateway.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin-auth.test.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/admin-modules.ts
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:06:53 PM [vite] (client) page reload src/lib/sidebar-state.ts
|
||||||
|
6:06:53 PM [vite] (client) page reload src/global.d.ts
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/approval-management.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/order.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/discount.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/articles/new.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/articles/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/articles/[id]/edit.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/articles.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/kb/categories.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/financial/reconcile.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/financial/adjust-credit.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/support.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/designation.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/approvals.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/workspace/[menuId].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/department-management.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/review.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/roles/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/roles/templates.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/roles/create.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/roles/[id]/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/roles/[id]/edit.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/requirements/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/runtime-roles/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/runtime-roles/new.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/runtime-roles/[roleKey].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/modules.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/designation-management.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/external-dashboard-management/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/department.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/verification/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/notifications.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/role-ui-configs/index.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/role-ui-configs/new.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/role-ui-configs/[roleKey].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/credit.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/profile/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/users/details/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/users/[id]/edit.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/tax.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/invoice.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/photographer/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/verification-status/index.tsx
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/verification-status/[id].tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/report.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/coupon.tsx
|
||||||
|
6:06:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:53 PM [vite] (client) page reload src/routes/admin/ledger.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/jobs/[id].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/index.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/new.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/[schemaId].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/help/support-bridge.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/help/[id].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/approval/[id].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/company/create.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/company/[id].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/employees/index.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/employees/create.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/employees/[id]/edit.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/[...404].tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/api/gateway/[...path].ts
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/about.tsx
|
||||||
|
6:06:54 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/pricing.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/external-roles.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/internal-dashboard-management/index.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/leads/index.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/leads/[id].tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/verification/index.tsx
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/candidate.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/customer.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/jobs/index.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/applications.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/responses.tsx?pick=default&pick=$css
|
||||||
|
6:06:54 PM [vite] (client) page reload src/routes/admin/approval/index.tsx
|
||||||
|
6:06:54 PM [vite] (client) hmr invalidate /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx
|
||||||
|
6:06:54 PM [vite] (client) hmr update /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/components/AdminShell.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/components/AdminSidebar.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/ExternalRoleForm.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/components/admin/ProfessionAdminListPage.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/UserListPage.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/OnboardingManagementTabs.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/components/admin/DashboardDesignPreview.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/OnboardingFlowBuilder.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/components/admin/RoleUserManagementTablePage.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/AdminUi.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/components/admin/ExternalRoleTabs.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Button.stories.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Page.stories.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/admin/AdminPages.stories.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/AdminDashboard.stories.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Header.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Button.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Page.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/stories/Header.stories.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/entry-server.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload src/entry-server.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/runtime/storage.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/runtime/types.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin/dashboard.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin/types.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin/client.ts
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin/module-access.test.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/server/gateway.ts
|
||||||
|
6:07:45 PM [vite] (ssr) page reload src/lib/server/gateway.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin-auth.test.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/admin-modules.ts
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.css, /src/components/AdminShell.tsx
|
||||||
|
6:07:45 PM [vite] (client) page reload src/lib/sidebar-state.ts
|
||||||
|
6:07:45 PM [vite] (client) page reload src/global.d.ts
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/approval-management.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/index.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/order.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/discount.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/articles/new.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/articles/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/articles/[id]/edit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/articles.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/kb/categories.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/financial/reconcile.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/financial/adjust-credit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/support.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/designation.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/approvals.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/workspace/[menuId].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/department-management.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/review.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/roles/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/roles/templates.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/roles/create.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/roles/[id]/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/roles/[id]/edit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/requirements/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/runtime-roles/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/runtime-roles/new.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/runtime-roles/[roleKey].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/modules.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/designation-management.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/external-dashboard-management/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/department.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/verification/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/notifications.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/role-ui-configs/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/role-ui-configs/new.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/role-ui-configs/[roleKey].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/credit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/profile/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/users/details/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/users/[id]/edit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/tax.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/invoice.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/photographer/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/verification-status/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/verification-status/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/report.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/coupon.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/ledger.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/jobs/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/new.tsx
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/onboarding-schemas/[schemaId].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/help/support-bridge.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/help/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/approval/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/company/create.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/company/[id].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/employees/index.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/employees/create.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/admin/employees/[id]/edit.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/[...404].tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/api/gateway/[...path].ts
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:45 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
6:07:45 PM [vite] (client) page reload src/routes/about.tsx
|
||||||
|
6:07:45 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
6:07:46 PM [vite] (client) hmr invalidate /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/entry-client.tsx
|
||||||
|
6:07:46 PM [vite] (client) hmr update /src/app.css
|
||||||
|
7:02:59 PM [vite] (client) hmr update /src/components/AdminSidebar.tsx, /src/app.css
|
||||||
|
7:15:46 PM [vite] (client) hmr update /src/components/admin/ProfessionAdminListPage.tsx, /src/app.css
|
||||||
|
7:18:08 PM [vite] (client) hmr update /src/app.css, /@fs/Users/ashwin/workspace/nxtgauge-admin-solid/src/routes/admin/index.tsx?pick=default&pick=$css
|
||||||
|
7:18:52 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
7:18:52 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
7:18:52 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
7:19:49 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
7:19:49 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
7:19:49 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
7:19:53 PM [vite] (client) hmr update /src/app.tsx, /src/app.css
|
||||||
|
7:19:53 PM [vite] (ssr) page reload vinxi/routes
|
||||||
|
7:19:53 PM [vite] (client) page reload src/routes/login.tsx
|
||||||
|
7:21:11 PM [vite] (client) hmr update /src/components/AdminShell.tsx, /src/app.css
|
||||||
1
admin.pid
Normal file
1
admin.pid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
87293
|
||||||
|
|
@ -90,7 +90,7 @@ const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [
|
||||||
{ prefix: '/admin/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] },
|
{ prefix: '/admin/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] },
|
||||||
{ prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] },
|
{ prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] },
|
||||||
{ prefix: '/admin/catering-services', keys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] },
|
{ prefix: '/admin/catering-services', keys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] },
|
||||||
{ prefix: '/admin/ugc-content-creators', keys: ['UGC_CONTENT_CREATOR_MANAGEMENT', 'UGC_CONTENT_CREATOR'] },
|
{ 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/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/social-media-managers', keys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] },
|
||||||
{ prefix: '/admin/jobs', keys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
{ prefix: '/admin/jobs', keys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
||||||
|
|
@ -116,7 +116,7 @@ const SEARCH_MODULES = [
|
||||||
{
|
{
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
viewAllHref: '/admin/users',
|
viewAllHref: '/admin/users',
|
||||||
api: '/api/gateway/api/admin/users',
|
api: '/api/admin/users',
|
||||||
listKeys: ['users', 'items'],
|
listKeys: ['users', 'items'],
|
||||||
titleKeys: ['full_name', 'name'],
|
titleKeys: ['full_name', 'name'],
|
||||||
subtitleKeys: ['email', 'phone'],
|
subtitleKeys: ['email', 'phone'],
|
||||||
|
|
@ -125,7 +125,7 @@ const SEARCH_MODULES = [
|
||||||
{
|
{
|
||||||
label: 'Companies',
|
label: 'Companies',
|
||||||
viewAllHref: '/admin/company',
|
viewAllHref: '/admin/company',
|
||||||
api: '/api/gateway/api/admin/companies',
|
api: '/api/admin/companies',
|
||||||
listKeys: ['companies', 'items'],
|
listKeys: ['companies', 'items'],
|
||||||
titleKeys: ['name', 'companyName'],
|
titleKeys: ['name', 'companyName'],
|
||||||
subtitleKeys: ['email', 'phone'],
|
subtitleKeys: ['email', 'phone'],
|
||||||
|
|
@ -134,7 +134,7 @@ const SEARCH_MODULES = [
|
||||||
{
|
{
|
||||||
label: 'Employees',
|
label: 'Employees',
|
||||||
viewAllHref: '/admin/employees',
|
viewAllHref: '/admin/employees',
|
||||||
api: '/api/gateway/api/admin/employees',
|
api: '/api/admin/employees',
|
||||||
listKeys: ['employees', 'items'],
|
listKeys: ['employees', 'items'],
|
||||||
titleKeys: ['full_name', 'name'],
|
titleKeys: ['full_name', 'name'],
|
||||||
subtitleKeys: ['email', 'department_name'],
|
subtitleKeys: ['email', 'department_name'],
|
||||||
|
|
@ -143,7 +143,7 @@ const SEARCH_MODULES = [
|
||||||
{
|
{
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
viewAllHref: '/admin/jobs',
|
viewAllHref: '/admin/jobs',
|
||||||
api: '/api/gateway/api/admin/jobs',
|
api: '/api/admin/jobs',
|
||||||
listKeys: ['jobs', 'items'],
|
listKeys: ['jobs', 'items'],
|
||||||
titleKeys: ['title', 'name'],
|
titleKeys: ['title', 'name'],
|
||||||
subtitleKeys: ['status', 'company_name'],
|
subtitleKeys: ['status', 'company_name'],
|
||||||
|
|
@ -152,7 +152,7 @@ const SEARCH_MODULES = [
|
||||||
{
|
{
|
||||||
label: 'Leads',
|
label: 'Leads',
|
||||||
viewAllHref: '/admin/leads',
|
viewAllHref: '/admin/leads',
|
||||||
api: '/api/gateway/api/admin/leads',
|
api: '/api/admin/leads',
|
||||||
listKeys: ['leads', 'items'],
|
listKeys: ['leads', 'items'],
|
||||||
titleKeys: ['name', 'full_name'],
|
titleKeys: ['name', 'full_name'],
|
||||||
subtitleKeys: ['email', 'status'],
|
subtitleKeys: ['email', 'status'],
|
||||||
|
|
@ -330,7 +330,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
: '';
|
: '';
|
||||||
await fetch('/api/gateway/auth/logout', {
|
await fetch('/api/auth/logout', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -414,7 +414,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
: '';
|
: '';
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
const res = await fetch('/api/gateway/api/me/notifications/unread-count', {
|
const res = await fetch('/api/me/notifications/unread-count', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -455,7 +455,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
: '';
|
: '';
|
||||||
const response = await fetch('/api/gateway/api/auth/session', {
|
const response = await fetch('/api/auth/session', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
@ -480,7 +480,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
||||||
setIsSuperAdmin(roleKey === 'SUPER_ADMIN');
|
setIsSuperAdmin(roleKey === 'SUPER_ADMIN');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/gateway/api/runtime-config', {
|
const res = await fetch('/api/runtime-config', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,6 @@ const GROUPS: NavItem[][] = [
|
||||||
[
|
[
|
||||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness, moduleKeys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness, moduleKeys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
||||||
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping, moduleKeys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] },
|
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping, moduleKeys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] },
|
||||||
{ href: '/admin/applications', label: 'Applications Management', icon: FileText, moduleKeys: ['APPLICATIONS_MANAGEMENT', 'APPLICATIONS'] },
|
|
||||||
{ href: '/admin/responses', label: 'Responses Management', icon: FileText, moduleKeys: ['RESPONSES_MANAGEMENT', 'RESPONSES'] },
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, moduleKeys: ['PRICING_MANAGEMENT', 'PRICING'] },
|
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, moduleKeys: ['PRICING_MANAGEMENT', 'PRICING'] },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type SortMode = 'newest' | 'oldest' | 'name_asc' | 'name_desc';
|
type SortMode = 'newest' | 'oldest' | 'name_asc' | 'name_desc';
|
||||||
|
|
||||||
|
|
@ -90,140 +90,169 @@ export default function ProfessionAdminListPage(props: {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
|
||||||
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
<div class="bg-white border-b border-gray-200 px-6 py-4">
|
||||||
<h1 class="text-xl font-semibold text-gray-900">{props.title}</h1>
|
<h1 class="text-xl font-semibold text-gray-900">{props.title}</h1>
|
||||||
<p class="text-sm text-gray-500 mt-0.5">{props.subtitle}</p>
|
<p class="text-sm text-gray-500 mt-0.5">{props.subtitle}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 p-6">
|
<div class="flex-1 p-6">
|
||||||
<div class="mb-4 flex flex-wrap items-center gap-2" style="position:relative;z-index:20;">
|
<div class="mb-4 flex flex-wrap items-center gap-2" style="position:relative;z-index:20;">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name or email..."
|
placeholder="Search by name or email..."
|
||||||
value={search()}
|
value={search()}
|
||||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#FF5E13] w-72"
|
class="rounded-lg border border-gray-200 px-3 py-2 text-sm outline-none focus:border-[#FF5E13] w-72"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style="position:relative;">
|
<div style="position:relative;">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
>
|
>
|
||||||
Sort
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
||||||
</button>
|
Sort
|
||||||
<Show when={sortMenuOpen()}>
|
</button>
|
||||||
<div style="position:absolute;right:0;top:38px;z-index:40;min-width:180px;border:1px solid #E5E7EB;border-radius:12px;background:#fff;box-shadow:0 12px 24px rgba(2,6,23,0.12);padding:8px">
|
<Show when={sortMenuOpen()}>
|
||||||
<For each={[
|
<div style="position:absolute;right:0;top:38px;z-index:40;min-width:180px;border:1px solid #E5E7EB;border-radius:12px;background:#fff;box-shadow:0 12px 24px rgba(2,6,23,0.12);padding:8px">
|
||||||
{ key: 'newest', label: 'Newest First' },
|
<For each={[
|
||||||
{ key: 'oldest', label: 'Oldest First' },
|
{ key: 'newest', label: 'Newest First' },
|
||||||
{ key: 'name_asc', label: 'Name A-Z' },
|
{ key: 'oldest', label: 'Oldest First' },
|
||||||
{ key: 'name_desc', label: 'Name Z-A' },
|
{ key: 'name_asc', label: 'Name A-Z' },
|
||||||
] as { key: SortMode; label: string }[]}>
|
{ key: 'name_desc', label: 'Name Z-A' },
|
||||||
{(item) => (
|
] as { key: SortMode; label: string }[]}>
|
||||||
<button
|
{(item) => (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setSortBy(item.key); setSortMenuOpen(false); }}
|
type="button"
|
||||||
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
|
onClick={() => { setSortBy(item.key); setSortMenuOpen(false); }}
|
||||||
>
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
|
||||||
{item.label}
|
>
|
||||||
</button>
|
{item.label}
|
||||||
)}
|
</button>
|
||||||
</For>
|
)}
|
||||||
</div>
|
</For>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="position:relative;">
|
<div style="position:relative;">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
>
|
>
|
||||||
Filters
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||||
</button>
|
Filters
|
||||||
<Show when={filterMenuOpen()}>
|
</button>
|
||||||
<div style="position:absolute;right:0;top:38px;z-index:40;min-width:180px;border:1px solid #E5E7EB;border-radius:12px;background:#fff;box-shadow:0 12px 24px rgba(2,6,23,0.12);padding:8px">
|
<Show when={filterMenuOpen()}>
|
||||||
<For each={[
|
<div style="position:absolute;right:0;top:38px;z-index:40;min-width:180px;border:1px solid #E5E7EB;border-radius:12px;background:#fff;box-shadow:0 12px 24px rgba(2,6,23,0.12);padding:8px">
|
||||||
{ key: 'all', label: 'All Status' },
|
<For each={[
|
||||||
{ key: 'ACTIVE', label: 'Active' },
|
{ key: 'all', label: 'All Status' },
|
||||||
{ key: 'INACTIVE', label: 'Inactive' },
|
{ key: 'ACTIVE', label: 'Active' },
|
||||||
{ key: 'PENDING', label: 'Pending' },
|
{ key: 'INACTIVE', label: 'Inactive' },
|
||||||
] as { key: 'all' | 'ACTIVE' | 'INACTIVE' | 'PENDING'; label: string }[]}>
|
{ key: 'PENDING', label: 'Pending' },
|
||||||
{(item) => (
|
] as { key: 'all' | 'ACTIVE' | 'INACTIVE' | 'PENDING'; label: string }[]}>
|
||||||
<button
|
{(item) => (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => { setStatusFilter(item.key); setFilterMenuOpen(false); }}
|
type="button"
|
||||||
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
|
onClick={() => { setStatusFilter(item.key); setFilterMenuOpen(false); }}
|
||||||
>
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
|
||||||
{item.label}
|
>
|
||||||
</button>
|
{item.label}
|
||||||
)}
|
</button>
|
||||||
</For>
|
)}
|
||||||
</div>
|
</For>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={exportCsv}
|
onClick={exportCsv}
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #D1D5DB;background:#fff;padding:0 12px;font-size:12px;font-weight:600;color:#0f172a;cursor:pointer"
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #D1D5DB;background:#fff;padding:0 12px;font-size:12px;font-weight:600;color:#0f172a;cursor:pointer"
|
||||||
>
|
>
|
||||||
Export
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
</button>
|
Export
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-card">
|
<div class="table-card">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="data-table w-full text-sm">
|
<table class="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr style="background:#0D0D2A;text-align:left">
|
||||||
<th>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">Name</th>
|
||||||
<th>Email</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Email</th>
|
||||||
<th>Phone</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Phone</th>
|
||||||
<th>Status</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
|
||||||
<th>Registered</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Registered</th>
|
||||||
<th class="text-right">Actions</th>
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap;text-align:right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<Show when={items.loading}>
|
<Show when={items.loading}>
|
||||||
<tr><td colspan="6" class="text-center py-8 text-slate-500">Loading...</td></tr>
|
<tr><td colspan="6" class="px-6 py-16 text-center text-slate-500">Loading...</td></tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!items.loading && items.error}>
|
<Show when={!items.loading && items.error}>
|
||||||
<tr><td colspan="6" class="text-center py-8 text-red-700">Failed to load. Is the backend running?</td></tr>
|
<tr><td colspan="6" class="px-6 py-16 text-center text-red-700">Failed to load. Is the backend running?</td></tr>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!items.loading && !items.error && filtered().length === 0}>
|
<Show when={!items.loading && !items.error && filtered().length === 0}>
|
||||||
<tr><td colspan="6" class="text-center py-8 text-slate-400">{props.emptyLabel}</td></tr>
|
<tr>
|
||||||
</Show>
|
<td colspan="6" class="px-6 py-16 text-center">
|
||||||
<Show when={!items.loading && !items.error && filtered().length > 0}>
|
<p class="text-[15px] font-semibold text-[#111827]">No {props.title} found</p>
|
||||||
<For each={filtered()}>
|
<p class="mt-1 text-[13px] text-[#6B7280]">{props.emptyLabel}</p>
|
||||||
{(item) => (
|
{props.createHref && (
|
||||||
<tr class="hover:bg-slate-50">
|
<A class="mt-4 inline-flex items-center gap-2 rounded-xl bg-[#0D0D2A] px-4 py-2 text-[13px] font-semibold text-white" href={props.createHref}>Create {props.title}</A>
|
||||||
<td class="font-semibold text-slate-900">{item.first_name || ''} {item.last_name || ''}</td>
|
)}
|
||||||
<td class="text-slate-500">{item.email}</td>
|
</td>
|
||||||
<td class="text-slate-500">{item.phone || '—'}</td>
|
</tr>
|
||||||
<td>
|
</Show>
|
||||||
<span class={statusBadge(item.status)}>{item.status?.toUpperCase() || '—'}</span>
|
<Show when={!items.loading && !items.error && filtered().length > 0}>
|
||||||
</td>
|
<For each={filtered()}>
|
||||||
<td class="text-slate-500">{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}</td>
|
{(item) => (
|
||||||
<td>
|
<tr class="hover:bg-[#FAFAFA] transition-colors">
|
||||||
<div class="flex items-center justify-end gap-1">
|
<td class="px-6 py-4">
|
||||||
<A class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50" href={props.viewHref(String(item.id))}>View</A>
|
<p class="font-semibold text-slate-900">{item.first_name || ''} {item.last_name || ''}</p>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
<td class="px-6 py-4 text-slate-500">{item.email}</td>
|
||||||
</tr>
|
<td class="px-6 py-4 text-slate-500">{item.phone || '—'}</td>
|
||||||
)}
|
<td class="px-6 py-4">
|
||||||
</For>
|
<span class={statusBadge(item.status)}>{item.status?.toUpperCase() || '—'}</span>
|
||||||
</Show>
|
</td>
|
||||||
</tbody>
|
<td class="px-6 py-4 text-slate-500">{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}</td>
|
||||||
</table>
|
<td class="px-6 py-4 text-right">
|
||||||
</div>
|
<div class="flex items-center justify-end gap-1">
|
||||||
</div>
|
<A class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm hover:bg-gray-50" href={props.viewHref(String(item.id))}>View</A>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
);
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination (matches department style, non-functional placeholder) */}
|
||||||
|
<Show when={!items.loading && !items.error && filtered().length > 0}>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px;margin-top:1px">
|
||||||
|
<p style="font-size:13px;color:#6B7280">
|
||||||
|
Showing <strong style="font-weight:600;color:#111827">1–{filtered().length}</strong> of <strong style="font-weight:600;color:#111827">{filtered().length}</strong> {props.title.toLowerCase()}
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type SortMode = 'newest' | 'oldest' | 'name_asc' | 'name_desc';
|
type SortMode = 'newest' | 'oldest' | 'name_asc' | 'name_desc';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
import { createMemo, createResource, createSignal, For, Show, type JSX } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show, type JSX } from 'solid-js';
|
||||||
import { Search, MoreVertical, Users, UserCheck, UserX, Clock } from 'lucide-solid';
|
import { Search, MoreVertical, Users, UserCheck, UserX, Clock } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
export interface UserListPageConfig {
|
export interface UserListPageConfig {
|
||||||
/** e.g. "photographer" */
|
/** e.g. "photographer" */
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { RuntimeDashboardLayout } from './types';
|
import type { RuntimeDashboardLayout } from './types';
|
||||||
|
|
||||||
// All API calls go through the server route gateway proxy
|
// All API calls go through the gateway proxy
|
||||||
const API_GATEWAY = '/api/gateway';
|
const API_GATEWAY = '';
|
||||||
|
|
||||||
export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding' | 'knowledge_base';
|
export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding' | 'knowledge_base';
|
||||||
export type RuntimeRecordStatus = 'draft' | 'published';
|
export type RuntimeRecordStatus = 'draft' | 'published';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type ApplicationRow = {
|
type ApplicationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,761 +0,0 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
|
||||||
|
|
||||||
const API = '/api/gateway';
|
|
||||||
const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue';
|
|
||||||
|
|
||||||
type ApprovalSubmittedField = { label: string; value: string };
|
|
||||||
type ApprovalDocument = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: 'IMAGE' | 'PDF';
|
|
||||||
url: string;
|
|
||||||
status: 'SUBMITTED' | 'MISSING' | 'INVALID';
|
|
||||||
};
|
|
||||||
|
|
||||||
type ApprovalRecord = CrudRecord & {
|
|
||||||
applicantName?: string;
|
|
||||||
approvalType: 'PROFILE' | 'BUSINESS' | 'JOB' | 'ORDER' | 'INVOICE' | 'COUPON' | 'DISCOUNT' | 'TAX' | 'ROLE' | 'REQUIREMENT' | 'PORTFOLIO';
|
|
||||||
userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER';
|
|
||||||
roleTags?: string[];
|
|
||||||
primaryService?: string;
|
|
||||||
area?: string;
|
|
||||||
submittedDate?: string;
|
|
||||||
verificationStatus: 'PENDING' | 'VERIFIED' | 'FLAGGED';
|
|
||||||
assignedApprover?: string;
|
|
||||||
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
||||||
status: 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ON_HOLD' | 'ESCALATED';
|
|
||||||
sourceKey?: string;
|
|
||||||
submittedFields?: ApprovalSubmittedField[];
|
|
||||||
documents?: ApprovalDocument[];
|
|
||||||
payload?: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ApprovalQueueItem = {
|
|
||||||
id: string;
|
|
||||||
requestType:
|
|
||||||
| 'Profile Approval'
|
|
||||||
| 'Portfolio Approval'
|
|
||||||
| 'Company Approval'
|
|
||||||
| 'Job Seeker Approval'
|
|
||||||
| 'Service Seeker Profile Approval'
|
|
||||||
| 'Service Seeker Requirement'
|
|
||||||
| 'Job Approval';
|
|
||||||
applicantName: string;
|
|
||||||
roleLabel: string;
|
|
||||||
userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER';
|
|
||||||
roleKey: string;
|
|
||||||
area: string;
|
|
||||||
submittedOn: string;
|
|
||||||
documents: ApprovalDocument[];
|
|
||||||
submittedFields: ApprovalSubmittedField[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const toTitle = (value: string) => String(value || '')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
|
|
||||||
const normalizeUserType = (value: unknown): ApprovalRecord['userType'] => {
|
|
||||||
const key = String(value || '').toUpperCase();
|
|
||||||
if (key.includes('COMPANY')) return 'COMPANY';
|
|
||||||
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOBSEEKER';
|
|
||||||
if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
|
|
||||||
return 'PROFESSIONAL';
|
|
||||||
};
|
|
||||||
|
|
||||||
function extractRoleTags(source: any): string[] {
|
|
||||||
const values: string[] = [];
|
|
||||||
const pushValue = (value: unknown) => {
|
|
||||||
if (!value) return;
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((v) => pushValue(v));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const text = String(value || '').trim();
|
|
||||||
if (!text) return;
|
|
||||||
values.push(text);
|
|
||||||
};
|
|
||||||
pushValue(source?.role_key);
|
|
||||||
pushValue(source?.roleKey);
|
|
||||||
pushValue(source?.role_keys);
|
|
||||||
pushValue(source?.roleKeys);
|
|
||||||
pushValue(source?.roles);
|
|
||||||
pushValue(source?.categories);
|
|
||||||
pushValue(source?.category);
|
|
||||||
pushValue(source?.service_category);
|
|
||||||
pushValue(source?.serviceCategory);
|
|
||||||
pushValue(source?.profession);
|
|
||||||
pushValue(source?.service_type);
|
|
||||||
return Array.from(new Set(values.map((v) => toTitle(v)))).slice(0, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractSubmittedFields(source: any): ApprovalSubmittedField[] {
|
|
||||||
const payload = source || {};
|
|
||||||
const fullName = String(
|
|
||||||
payload.full_name
|
|
||||||
|| payload.fullName
|
|
||||||
|| [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ')
|
|
||||||
|| payload.company_name
|
|
||||||
|| payload.title
|
|
||||||
|| '—',
|
|
||||||
).trim();
|
|
||||||
|
|
||||||
const candidates: ApprovalSubmittedField[] = [
|
|
||||||
{ label: 'Name / Title', value: fullName || '—' },
|
|
||||||
{ label: 'Email', value: String(payload.email || payload.email_address || payload.emailAddress || '—') },
|
|
||||||
{ label: 'Mobile', value: String(payload.mobile || payload.mobile_number || payload.phone || payload.contact_number || '—') },
|
|
||||||
{ label: 'Role / Category', value: String(payload.role_key || payload.role || payload.category || payload.profession || payload.service_category || '—') },
|
|
||||||
{ label: 'Area', value: String(payload.area || payload.location || payload.city || 'Chennai') },
|
|
||||||
{ label: 'Place', value: String(payload.place || payload.locality || payload.city || 'Chennai') },
|
|
||||||
{ label: 'Description', value: String(payload.description || payload.about || payload.bio || '—') },
|
|
||||||
];
|
|
||||||
|
|
||||||
return candidates.filter((item) => item.value && item.value !== '—');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractDocuments(source: any): ApprovalDocument[] {
|
|
||||||
const raw = Array.isArray(source?.documents) ? source.documents : [];
|
|
||||||
if (!raw.length) {
|
|
||||||
const portfolioImages = Array.isArray(source?.portfolio_images)
|
|
||||||
? source.portfolio_images
|
|
||||||
: Array.isArray(source?.images)
|
|
||||||
? source.images
|
|
||||||
: Array.isArray(source?.gallery)
|
|
||||||
? source.gallery
|
|
||||||
: [];
|
|
||||||
if (!portfolioImages.length) return [];
|
|
||||||
return portfolioImages.slice(0, 6).map((asset: any, idx: number) => ({
|
|
||||||
id: String(asset.id || `portfolio-${idx + 1}`),
|
|
||||||
title: String(asset.title || asset.name || `Portfolio Image ${idx + 1}`),
|
|
||||||
type: 'IMAGE',
|
|
||||||
url: String(asset.url || '/nxtgauge-logo.png'),
|
|
||||||
status: 'SUBMITTED',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return raw.slice(0, 12).map((doc: any, idx: number) => {
|
|
||||||
const statusRaw = String(doc.status || '').toUpperCase();
|
|
||||||
return {
|
|
||||||
id: String(doc.id || `doc-${idx + 1}`),
|
|
||||||
title: String(doc.title || doc.name || `Document ${idx + 1}`),
|
|
||||||
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE',
|
|
||||||
url: String(doc.url || '/nxtgauge-logo.png'),
|
|
||||||
status: statusRaw === 'MISSING'
|
|
||||||
? 'MISSING'
|
|
||||||
: statusRaw === 'INVALID'
|
|
||||||
? 'INVALID'
|
|
||||||
: 'SUBMITTED',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function verificationToApprovalType(requestType: ApprovalQueueItem['requestType']): ApprovalRecord['approvalType'] {
|
|
||||||
if (requestType === 'Portfolio Approval') return 'PORTFOLIO';
|
|
||||||
if (requestType === 'Service Seeker Requirement') return 'REQUIREMENT';
|
|
||||||
if (requestType === 'Job Approval') return 'JOB';
|
|
||||||
if (requestType === 'Company Approval') return 'BUSINESS';
|
|
||||||
return 'PROFILE';
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapQueueItemsToApprovals(items: ApprovalQueueItem[]): ApprovalRecord[] {
|
|
||||||
return items.map((item) => ({
|
|
||||||
id: String(item.id),
|
|
||||||
name: `${item.requestType} - ${item.applicantName}`,
|
|
||||||
applicantName: item.applicantName,
|
|
||||||
approvalType: verificationToApprovalType(item.requestType),
|
|
||||||
userType: normalizeUserType(item.userType),
|
|
||||||
roleTags: [toTitle(item.roleLabel), toTitle(item.roleKey)].filter((v, i, arr) => v && arr.indexOf(v) === i),
|
|
||||||
primaryService: toTitle(item.roleLabel || item.roleKey || item.requestType),
|
|
||||||
area: item.area || 'Chennai',
|
|
||||||
submittedDate: item.submittedOn || '',
|
|
||||||
verificationStatus: 'VERIFIED',
|
|
||||||
assignedApprover: 'Unassigned',
|
|
||||||
priority: item.requestType === 'Portfolio Approval' || item.requestType === 'Job Approval' ? 'HIGH' : 'MEDIUM',
|
|
||||||
status: 'PENDING',
|
|
||||||
updatedAt: item.submittedOn || '',
|
|
||||||
sourceKey: `verification:${item.id}`,
|
|
||||||
submittedFields: Array.isArray(item.submittedFields) ? item.submittedFields : [],
|
|
||||||
documents: Array.isArray(item.documents) ? item.documents : [],
|
|
||||||
payload: { queueRequestType: item.requestType },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBadge(props: { status: string }) {
|
|
||||||
const getColors = () => {
|
|
||||||
switch (props.status) {
|
|
||||||
case 'APPROVED': return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
|
|
||||||
case 'IN_REVIEW': return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
|
|
||||||
case 'PENDING': return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
|
||||||
case 'ON_HOLD': return { border: '#FDE68A', bg: '#FEF3C7', text: '#D97706', dot: '#D97706' };
|
|
||||||
case 'ESCALATED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
|
||||||
case 'REJECTED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
|
||||||
default: return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const colors = getColors();
|
|
||||||
const label = props.status.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
|
||||||
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PriorityBadge(props: { priority: string }) {
|
|
||||||
const color = props.priority === 'CRITICAL' ? '#7F1D1D' : props.priority === 'HIGH' ? '#DC2626' : props.priority === 'MEDIUM' ? '#F59E0B' : '#16A34A';
|
|
||||||
return (
|
|
||||||
<span style={`display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:${color}`}>
|
|
||||||
<span style={`width:6px;height:6px;border-radius:50%;background:${color}`} />
|
|
||||||
{props.priority}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function VerificationBadge(props: { status: ApprovalRecord['verificationStatus'] }) {
|
|
||||||
const getColors = () => {
|
|
||||||
if (props.status === 'VERIFIED') return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
|
|
||||||
if (props.status === 'FLAGGED') return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
|
||||||
return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
|
|
||||||
};
|
|
||||||
const colors = getColors();
|
|
||||||
return (
|
|
||||||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
|
||||||
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
|
|
||||||
{props.status}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ApprovalManagementPage() {
|
|
||||||
const [listTab, setListTab] = createSignal<'all' | 'view' | 'escalated'>('all');
|
|
||||||
const [detailTab, setDetailTab] = createSignal<'overview' | 'verification' | 'checklist' | 'logs'>('overview');
|
|
||||||
|
|
||||||
const [search, setSearch] = createSignal('');
|
|
||||||
const [rows, setRows] = createSignal<ApprovalRecord[]>([]);
|
|
||||||
const [viewingCase, setViewingCase] = createSignal<ApprovalRecord | null>(null);
|
|
||||||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
|
||||||
|
|
||||||
const [statusFilter, setStatusFilter] = createSignal<'all' | 'pending' | 'escalated'>('all');
|
|
||||||
const [sortBy, setSortBy] = createSignal<'submitted_desc' | 'submitted_asc' | 'priority_desc' | 'priority_asc'>('submitted_desc');
|
|
||||||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
|
||||||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal('');
|
|
||||||
const [isActing, setIsActing] = createSignal(false);
|
|
||||||
|
|
||||||
const selectedDocuments = createMemo<ApprovalDocument[]>(() => {
|
|
||||||
const row = viewingCase();
|
|
||||||
if (!row) return [];
|
|
||||||
if (Array.isArray(row.documents) && row.documents.length) return row.documents;
|
|
||||||
return extractDocuments(row.payload || {});
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedFields = createMemo<ApprovalSubmittedField[]>(() => {
|
|
||||||
const row = viewingCase();
|
|
||||||
if (!row) return [];
|
|
||||||
if (Array.isArray(row.submittedFields) && row.submittedFields.length) return row.submittedFields;
|
|
||||||
return extractSubmittedFields(row.payload || row);
|
|
||||||
});
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
setError('');
|
|
||||||
try {
|
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
||||||
: '';
|
|
||||||
const res = await fetch(`${API}/api/admin/verifications?page=1&limit=100`, {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
|
||||||
const payload = await res.json().catch(() => ({} as any));
|
|
||||||
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
||||||
|
|
||||||
const mappedItems: ApprovalRecord[] = items.map((v: any) => {
|
|
||||||
const p = v.payload || {};
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
name: `${toTitle(v.type)} - ${v.user_name || 'Applicant'}`,
|
|
||||||
applicantName: v.user_name || 'Applicant',
|
|
||||||
approvalType: verificationToApprovalType(v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')),
|
|
||||||
userType: normalizeUserType(v.role_key),
|
|
||||||
roleTags: [toTitle(v.role_key)],
|
|
||||||
primaryService: toTitle(v.role_key || 'User'),
|
|
||||||
area: p.city || p.area || 'Unknown',
|
|
||||||
submittedDate: v.created_at,
|
|
||||||
verificationStatus: v.status === 'APPROVED' ? 'VERIFIED' : 'PENDING',
|
|
||||||
assignedApprover: 'Unassigned',
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
status: v.status === 'APPROVED' ? 'APPROVED' : (v.status === 'REJECTED' ? 'REJECTED' : 'PENDING'),
|
|
||||||
updatedAt: v.updated_at,
|
|
||||||
sourceKey: `v:${v.id}`,
|
|
||||||
submittedFields: extractSubmittedFields(p),
|
|
||||||
documents: extractDocuments(p),
|
|
||||||
payload: v,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
setRows(mappedItems);
|
|
||||||
} catch (e: any) {
|
|
||||||
setRows([]);
|
|
||||||
setError(e?.message || 'Could not reach approvals API.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => void load());
|
|
||||||
|
|
||||||
const formatDate = (v?: string) => {
|
|
||||||
const s = v || '';
|
|
||||||
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
|
||||||
return s.slice(0, 10) || '—';
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredRows = createMemo(() => {
|
|
||||||
let list = rows();
|
|
||||||
const f = statusFilter();
|
|
||||||
if (f === 'pending') list = list.filter((r) => r.status === 'PENDING' || r.status === 'IN_REVIEW');
|
|
||||||
if (f === 'escalated') list = list.filter((r) => r.status === 'ESCALATED');
|
|
||||||
|
|
||||||
const q = search().trim().toLowerCase();
|
|
||||||
if (q) {
|
|
||||||
list = list.filter((r) =>
|
|
||||||
String(r.applicantName || '').toLowerCase().includes(q)
|
|
||||||
|| String(r.id || '').toLowerCase().includes(q)
|
|
||||||
|| String(r.approvalType || '').toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sorted = [...list];
|
|
||||||
const mode = sortBy();
|
|
||||||
const priorityRank = (p: ApprovalRecord['priority']) => (p === 'CRITICAL' ? 4 : p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
const ad = Date.parse(String(a.submittedDate || a.updatedAt || '')) || 0;
|
|
||||||
const bd = Date.parse(String(b.submittedDate || b.updatedAt || '')) || 0;
|
|
||||||
if (mode === 'submitted_asc') return ad - bd;
|
|
||||||
if (mode === 'priority_desc') return priorityRank(b.priority) - priorityRank(a.priority);
|
|
||||||
if (mode === 'priority_asc') return priorityRank(a.priority) - priorityRank(b.priority);
|
|
||||||
return bd - ad;
|
|
||||||
});
|
|
||||||
return sorted;
|
|
||||||
});
|
|
||||||
|
|
||||||
const escalatedCount = createMemo(() => rows().filter((r) => r.status === 'ESCALATED').length);
|
|
||||||
|
|
||||||
const exportCsv = () => {
|
|
||||||
const headers = ['Approval ID', 'Applicant', 'Type', 'Verification', 'Priority', 'Status', 'Submitted Date'];
|
|
||||||
const rowsData = filteredRows().map((row) => [
|
|
||||||
row.id || '',
|
|
||||||
row.applicantName || '',
|
|
||||||
row.approvalType || '',
|
|
||||||
row.verificationStatus || '',
|
|
||||||
row.priority || '',
|
|
||||||
row.status || '',
|
|
||||||
formatDate(row.submittedDate || row.updatedAt),
|
|
||||||
]);
|
|
||||||
const csv = [headers, ...rowsData]
|
|
||||||
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
||||||
.join('\n');
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `approval-management-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openView = (row: ApprovalRecord) => {
|
|
||||||
setViewingCase(row);
|
|
||||||
setDetailTab('overview');
|
|
||||||
setListTab('view');
|
|
||||||
setOpenMenuId(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setLocalStatus = (row: ApprovalRecord, nextStatus: ApprovalRecord['status']) => {
|
|
||||||
setRows((prev) => prev.map((item) => ((item.sourceKey || item.id) === (row.sourceKey || row.id) ? { ...item, status: nextStatus } : item)));
|
|
||||||
setViewingCase((current) => {
|
|
||||||
if (!current) return current;
|
|
||||||
return (current.sourceKey || current.id) === (row.sourceKey || row.id) ? { ...current, status: nextStatus } : current;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const runApprovalAction = async (row: ApprovalRecord, action: 'approve' | 'reject') => {
|
|
||||||
const type = row.approvalType;
|
|
||||||
const nextStatus: ApprovalRecord['status'] = action === 'approve' ? 'APPROVED' : 'REJECTED';
|
|
||||||
|
|
||||||
if (type !== 'JOB' && type !== 'REQUIREMENT') {
|
|
||||||
setLocalStatus(row, nextStatus);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsActing(true);
|
|
||||||
setError('');
|
|
||||||
try {
|
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
||||||
: '';
|
|
||||||
const endpoint = `${API}/api/admin/verifications/${row.id}/${action}`;
|
|
||||||
const res = await fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
||||||
},
|
|
||||||
credentials: 'include',
|
|
||||||
body: action === 'reject'
|
|
||||||
? JSON.stringify({ reason: 'Rejected by admin from approval management' })
|
|
||||||
: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
throw new Error((data as any).message || `Request failed (${res.status})`);
|
|
||||||
}
|
|
||||||
await load();
|
|
||||||
setViewingCase(null);
|
|
||||||
setListTab('all');
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(e?.message || 'Approval action failed.');
|
|
||||||
} finally {
|
|
||||||
setIsActing(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="w-full space-y-6 pb-8">
|
|
||||||
|
|
||||||
<div style="margin-bottom: 1.5rem">
|
|
||||||
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Approval Management</h1>
|
|
||||||
<p class="mt-1 text-[14px] text-[#6B7280]">Manage final approval decisions for all platform entities and requests</p>
|
|
||||||
</div>
|
|
||||||
<Show when={error()}>
|
|
||||||
<div style="margin-bottom:10px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={true}>
|
|
||||||
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB">
|
|
||||||
{([
|
|
||||||
{ key: 'all', label: 'All Approvals', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
|
||||||
{ key: 'escalated', label: `Escalated (${escalatedCount()})`, action: () => { setListTab('escalated'); setStatusFilter('escalated'); } },
|
|
||||||
{ key: 'view', label: 'View Approval', action: () => { setListTab('view'); } },
|
|
||||||
] as const).map((tab) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={tab.action}
|
|
||||||
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={listTab() === 'view'}>
|
|
||||||
<Show when={!viewingCase()}>
|
|
||||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
|
||||||
<p style="font-size:15px;font-weight:600;color:#111827">No approval selected</p>
|
|
||||||
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong>⋮</strong> menu on any row and choose <strong>View Approval</strong>.</p>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={viewingCase()}>
|
|
||||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
||||||
<div>
|
|
||||||
<div style="display:flex;align-items:center;gap:12px">
|
|
||||||
<h2 style="font-size:18px;font-weight:700;color:#111827">{viewingCase()!.applicantName}</h2>
|
|
||||||
<StatusBadge status={viewingCase()!.status} />
|
|
||||||
<PriorityBadge priority={viewingCase()!.priority} />
|
|
||||||
</div>
|
|
||||||
<p style="margin-top:2px;font-size:13px;color:#6B7280">ID: {viewingCase()!.id} • {viewingCase()!.approvalType} • Submitted: {formatDate(viewingCase()!.submittedDate)}</p>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:10px">
|
|
||||||
<button type="button" onClick={() => void runApprovalAction(viewingCase()!, 'approve')} disabled={isActing()} style={`height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:${isActing() ? 0.7 : 1}`}>Approve</button>
|
|
||||||
<button type="button" onClick={() => void runApprovalAction(viewingCase()!, 'reject')} disabled={isActing()} style={`height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer;opacity:${isActing() ? 0.7 : 1}`}>Reject</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:4px;min-height:44px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
|
||||||
{(['overview', 'verification', 'checklist', 'logs'] as const).map((tab, i) => {
|
|
||||||
const labels = ['Overview', 'Verification Summary', 'Approval Checklist', 'Activity Logs'];
|
|
||||||
const active = () => detailTab() === tab;
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
|
||||||
{labels[i]}
|
|
||||||
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="padding:24px">
|
|
||||||
<Show when={detailTab() === 'overview'}>
|
|
||||||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:24px">
|
|
||||||
<div style="display:flex;flex-direction:column;gap:24px">
|
|
||||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
||||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Approval Summary</h3>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Entity Name</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.applicantName}</p></div>
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Approval Type</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.approvalType}</p></div>
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Primary Service</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.primaryService || '—'}</p></div>
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Area / Place</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.area || '—'}</p></div>
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Verification Status</p><VerificationBadge status={viewingCase()!.verificationStatus} /></div>
|
|
||||||
<div><p style="font-size:11px;color:#9CA3AF">Assigned Approver</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.assignedApprover}</p></div>
|
|
||||||
</div>
|
|
||||||
<Show when={viewingCase()!.roleTags?.length}>
|
|
||||||
<div style="margin-top:12px">
|
|
||||||
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF">Registered Roles / Services</p>
|
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
|
||||||
<For each={viewingCase()!.roleTags || []}>
|
|
||||||
{(tag) => (
|
|
||||||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:600;color:#374151">{tag}</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={selectedFields().length > 0}>
|
|
||||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
|
||||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:12px">Submitted Details</h3>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
|
||||||
<For each={selectedFields()}>
|
|
||||||
{(field) => (
|
|
||||||
<div style="border:1px solid #F3F4F6;border-radius:10px;padding:10px;background:#FAFAFA">
|
|
||||||
<p style="font-size:11px;color:#9CA3AF">{field.label}</p>
|
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827;line-height:1.4;word-break:break-word">{field.value || '—'}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px;background:#F9FAFB">
|
|
||||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Decision Notes</h3>
|
|
||||||
<textarea placeholder="Add decision note..." style="width:100%;height:100px;border-radius:8px;border:1px solid #E5E7EB;padding:10px;font-size:13px;resize:none;margin-bottom:12px" />
|
|
||||||
<button type="button" style="width:100%;height:34px;background:#0D0D2A;color:white;border-radius:8px;font-size:12px;font-weight:600;border:none">Add Note</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={detailTab() === 'verification'}>
|
|
||||||
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px;background:#FAFAFA">
|
|
||||||
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Submitted Documents</h3>
|
|
||||||
<Show when={selectedDocuments().length > 0} fallback={<p style="font-size:13px;color:#6B7280">No documents submitted for this request.</p>}>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:8px">
|
|
||||||
<For each={selectedDocuments()}>
|
|
||||||
{(doc) => (
|
|
||||||
<div style="display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;padding:10px;border-radius:10px;border:1px solid #E5E7EB;background:white">
|
|
||||||
<div>
|
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827">{doc.title}</p>
|
|
||||||
<p style="font-size:12px;color:#6B7280">{doc.type} • {doc.status}</p>
|
|
||||||
</div>
|
|
||||||
<a href={doc.url} target="_blank" rel="noreferrer" style="display:inline-flex;height:30px;align-items:center;justify-content:center;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:12px;font-weight:600;color:#374151;background:white;text-decoration:none">View</a>
|
|
||||||
<span style="font-size:12px;color:#6B7280">{doc.status}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={detailTab() === 'checklist'}>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:12px">
|
|
||||||
{[
|
|
||||||
'Verification report is complete and reviewed',
|
|
||||||
'All required entity data is present',
|
|
||||||
'No active risk flags on applicant',
|
|
||||||
'Applicant meets all policy requirements',
|
|
||||||
'Final approval readiness confirmed',
|
|
||||||
].map((item) => (
|
|
||||||
<label style="display:flex;align-items:center;gap:12px;padding:12px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer">
|
|
||||||
<input type="checkbox" style="width:16px;height:16px;accent-color:#FF5E13" />
|
|
||||||
<span style="font-size:13px;color:#111827;font-weight:500">{item}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={detailTab() === 'logs'}>
|
|
||||||
<div style="display:flex;flex-direction:column;gap:16px">
|
|
||||||
<div style="display:flex;gap:12px">
|
|
||||||
<div style="width:8px;height:8px;border-radius:50%;background:#FF5E13;margin-top:4px" />
|
|
||||||
<div>
|
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827">Approval Review Started</p>
|
|
||||||
<p style="font-size:12px;color:#6B7280">Approver started final review • 1 hour ago</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex;gap:12px">
|
|
||||||
<div style="width:8px;height:8px;border-radius:50%;background:#E5E7EB;margin-top:4px" />
|
|
||||||
<div>
|
|
||||||
<p style="font-size:13px;font-weight:600;color:#111827">Verification Completed</p>
|
|
||||||
<p style="font-size:12px;color:#6B7280">Case handed over from verification team • 1 day ago</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
|
|
||||||
<button type="button" onClick={() => { setViewingCase(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div style={`display:${listTab() === 'view' ? 'none' : 'block'}`}>
|
|
||||||
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
||||||
<input
|
|
||||||
value={search()}
|
|
||||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
placeholder="Search approvals..."
|
|
||||||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
|
||||||
/>
|
|
||||||
<div style="position:relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
|
||||||
Sort
|
|
||||||
</button>
|
|
||||||
<Show when={sortMenuOpen()}>
|
|
||||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
||||||
{(['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const).map((s, i) => (
|
|
||||||
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
||||||
{['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div style="position:relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
|
||||||
Filters
|
|
||||||
</button>
|
|
||||||
<Show when={filterMenuOpen()}>
|
|
||||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
||||||
{(['all', 'pending', 'escalated'] as const).map((s) => (
|
|
||||||
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
|
||||||
{s === 'all' ? 'All Cases' : s === 'pending' ? 'Pending Decisions' : 'Escalated'}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto overflow-y-visible">
|
|
||||||
<table class="min-w-full">
|
|
||||||
<thead>
|
|
||||||
<tr style="background:#0D0D2A;text-align:left">
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Approval ID</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Applicant / Entity</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Type</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Verification</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Priority</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Submitted</th>
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<Show
|
|
||||||
when={filteredRows().length > 0}
|
|
||||||
fallback={
|
|
||||||
<tr>
|
|
||||||
<td colSpan={8} style="padding:32px;text-align:center">
|
|
||||||
<p style="font-size:15px;font-weight:600;color:#111827">No approvals found</p>
|
|
||||||
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={filteredRows()}>
|
|
||||||
{(row) => (
|
|
||||||
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
||||||
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.id}</td>
|
|
||||||
<td style="padding:12px 20px">
|
|
||||||
<p style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</p>
|
|
||||||
<p style="font-size:11px;color:#6B7280">{row.userType}{row.area ? ` • ${row.area}` : ''}</p>
|
|
||||||
<Show when={row.roleTags?.length}>
|
|
||||||
<p style="margin-top:2px;font-size:11px;color:#9CA3AF">{(row.roleTags || []).join(', ')}</p>
|
|
||||||
</Show>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.approvalType}</td>
|
|
||||||
<td style="padding:12px 20px"><VerificationBadge status={row.verificationStatus} /></td>
|
|
||||||
<td style="padding:12px 20px"><PriorityBadge priority={row.priority} /></td>
|
|
||||||
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
|
||||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(row.submittedDate || row.updatedAt)}</td>
|
|
||||||
<td style="padding:12px 20px;position:relative">
|
|
||||||
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
|
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
|
||||||
</button>
|
|
||||||
<Show when={openMenuId() === row.id}>
|
|
||||||
<div style="position:absolute;right:20px;top:44px;z-index:20;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
|
||||||
<button type="button" onClick={() => openView(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
|
||||||
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" 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>
|
|
||||||
View Approval
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => void runApprovalAction(row, 'approve')} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
|
||||||
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m8 12 2.5 2.5L16 9"/></svg>
|
|
||||||
Approve
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => void runApprovalAction(row, 'reject')} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">
|
|
||||||
<svg style="width:16px;height:16px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={filteredRows().length > 0}>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
|
||||||
<p style="font-size:13px;color:#6B7280">
|
|
||||||
Showing <strong style="font-weight:600;color:#111827">1–{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> approvals
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;align-items:center;gap:4px">
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams, useSearchParams } from '@solidjs/router';
|
import { A, useParams, useSearchParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, For, Show, createEffect } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show, createEffect } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
// ── Types ──────────────────────────────────────────────────────────
|
// ── Types ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,761 @@
|
||||||
export { default } from '../approval';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
|
const API = '';
|
||||||
|
const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue';
|
||||||
|
|
||||||
|
type ApprovalSubmittedField = { label: string; value: string };
|
||||||
|
type ApprovalDocument = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'IMAGE' | 'PDF';
|
||||||
|
url: string;
|
||||||
|
status: 'SUBMITTED' | 'MISSING' | 'INVALID';
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalRecord = CrudRecord & {
|
||||||
|
applicantName?: string;
|
||||||
|
approvalType: 'PROFILE' | 'BUSINESS' | 'JOB' | 'ORDER' | 'INVOICE' | 'COUPON' | 'DISCOUNT' | 'TAX' | 'ROLE' | 'REQUIREMENT' | 'PORTFOLIO';
|
||||||
|
userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER';
|
||||||
|
roleTags?: string[];
|
||||||
|
primaryService?: string;
|
||||||
|
area?: string;
|
||||||
|
submittedDate?: string;
|
||||||
|
verificationStatus: 'PENDING' | 'VERIFIED' | 'FLAGGED';
|
||||||
|
assignedApprover?: string;
|
||||||
|
priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||||
|
status: 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ON_HOLD' | 'ESCALATED';
|
||||||
|
sourceKey?: string;
|
||||||
|
submittedFields?: ApprovalSubmittedField[];
|
||||||
|
documents?: ApprovalDocument[];
|
||||||
|
payload?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalQueueItem = {
|
||||||
|
id: string;
|
||||||
|
requestType:
|
||||||
|
| 'Profile Approval'
|
||||||
|
| 'Portfolio Approval'
|
||||||
|
| 'Company Approval'
|
||||||
|
| 'Job Seeker Approval'
|
||||||
|
| 'Service Seeker Profile Approval'
|
||||||
|
| 'Service Seeker Requirement'
|
||||||
|
| 'Job Approval';
|
||||||
|
applicantName: string;
|
||||||
|
roleLabel: string;
|
||||||
|
userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER';
|
||||||
|
roleKey: string;
|
||||||
|
area: string;
|
||||||
|
submittedOn: string;
|
||||||
|
documents: ApprovalDocument[];
|
||||||
|
submittedFields: ApprovalSubmittedField[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTitle = (value: string) => String(value || '')
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
const normalizeUserType = (value: unknown): ApprovalRecord['userType'] => {
|
||||||
|
const key = String(value || '').toUpperCase();
|
||||||
|
if (key.includes('COMPANY')) return 'COMPANY';
|
||||||
|
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOBSEEKER';
|
||||||
|
if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
|
||||||
|
return 'PROFESSIONAL';
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractRoleTags(source: any): string[] {
|
||||||
|
const values: string[] = [];
|
||||||
|
const pushValue = (value: unknown) => {
|
||||||
|
if (!value) return;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => pushValue(v));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const text = String(value || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
values.push(text);
|
||||||
|
};
|
||||||
|
pushValue(source?.role_key);
|
||||||
|
pushValue(source?.roleKey);
|
||||||
|
pushValue(source?.role_keys);
|
||||||
|
pushValue(source?.roleKeys);
|
||||||
|
pushValue(source?.roles);
|
||||||
|
pushValue(source?.categories);
|
||||||
|
pushValue(source?.category);
|
||||||
|
pushValue(source?.service_category);
|
||||||
|
pushValue(source?.serviceCategory);
|
||||||
|
pushValue(source?.profession);
|
||||||
|
pushValue(source?.service_type);
|
||||||
|
return Array.from(new Set(values.map((v) => toTitle(v)))).slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractSubmittedFields(source: any): ApprovalSubmittedField[] {
|
||||||
|
const payload = source || {};
|
||||||
|
const fullName = String(
|
||||||
|
payload.full_name
|
||||||
|
|| payload.fullName
|
||||||
|
|| [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ')
|
||||||
|
|| payload.company_name
|
||||||
|
|| payload.title
|
||||||
|
|| '—',
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
const candidates: ApprovalSubmittedField[] = [
|
||||||
|
{ label: 'Name / Title', value: fullName || '—' },
|
||||||
|
{ label: 'Email', value: String(payload.email || payload.email_address || payload.emailAddress || '—') },
|
||||||
|
{ label: 'Mobile', value: String(payload.mobile || payload.mobile_number || payload.phone || payload.contact_number || '—') },
|
||||||
|
{ label: 'Role / Category', value: String(payload.role_key || payload.role || payload.category || payload.profession || payload.service_category || '—') },
|
||||||
|
{ label: 'Area', value: String(payload.area || payload.location || payload.city || 'Chennai') },
|
||||||
|
{ label: 'Place', value: String(payload.place || payload.locality || payload.city || 'Chennai') },
|
||||||
|
{ label: 'Description', value: String(payload.description || payload.about || payload.bio || '—') },
|
||||||
|
];
|
||||||
|
|
||||||
|
return candidates.filter((item) => item.value && item.value !== '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDocuments(source: any): ApprovalDocument[] {
|
||||||
|
const raw = Array.isArray(source?.documents) ? source.documents : [];
|
||||||
|
if (!raw.length) {
|
||||||
|
const portfolioImages = Array.isArray(source?.portfolio_images)
|
||||||
|
? source.portfolio_images
|
||||||
|
: Array.isArray(source?.images)
|
||||||
|
? source.images
|
||||||
|
: Array.isArray(source?.gallery)
|
||||||
|
? source.gallery
|
||||||
|
: [];
|
||||||
|
if (!portfolioImages.length) return [];
|
||||||
|
return portfolioImages.slice(0, 6).map((asset: any, idx: number) => ({
|
||||||
|
id: String(asset.id || `portfolio-${idx + 1}`),
|
||||||
|
title: String(asset.title || asset.name || `Portfolio Image ${idx + 1}`),
|
||||||
|
type: 'IMAGE',
|
||||||
|
url: String(asset.url || '/nxtgauge-logo.png'),
|
||||||
|
status: 'SUBMITTED',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return raw.slice(0, 12).map((doc: any, idx: number) => {
|
||||||
|
const statusRaw = String(doc.status || '').toUpperCase();
|
||||||
|
return {
|
||||||
|
id: String(doc.id || `doc-${idx + 1}`),
|
||||||
|
title: String(doc.title || doc.name || `Document ${idx + 1}`),
|
||||||
|
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE',
|
||||||
|
url: String(doc.url || '/nxtgauge-logo.png'),
|
||||||
|
status: statusRaw === 'MISSING'
|
||||||
|
? 'MISSING'
|
||||||
|
: statusRaw === 'INVALID'
|
||||||
|
? 'INVALID'
|
||||||
|
: 'SUBMITTED',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function verificationToApprovalType(requestType: ApprovalQueueItem['requestType']): ApprovalRecord['approvalType'] {
|
||||||
|
if (requestType === 'Portfolio Approval') return 'PORTFOLIO';
|
||||||
|
if (requestType === 'Service Seeker Requirement') return 'REQUIREMENT';
|
||||||
|
if (requestType === 'Job Approval') return 'JOB';
|
||||||
|
if (requestType === 'Company Approval') return 'BUSINESS';
|
||||||
|
return 'PROFILE';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapQueueItemsToApprovals(items: ApprovalQueueItem[]): ApprovalRecord[] {
|
||||||
|
return items.map((item) => ({
|
||||||
|
id: String(item.id),
|
||||||
|
name: `${item.requestType} - ${item.applicantName}`,
|
||||||
|
applicantName: item.applicantName,
|
||||||
|
approvalType: verificationToApprovalType(item.requestType),
|
||||||
|
userType: normalizeUserType(item.userType),
|
||||||
|
roleTags: [toTitle(item.roleLabel), toTitle(item.roleKey)].filter((v, i, arr) => v && arr.indexOf(v) === i),
|
||||||
|
primaryService: toTitle(item.roleLabel || item.roleKey || item.requestType),
|
||||||
|
area: item.area || 'Chennai',
|
||||||
|
submittedDate: item.submittedOn || '',
|
||||||
|
verificationStatus: 'VERIFIED',
|
||||||
|
assignedApprover: 'Unassigned',
|
||||||
|
priority: item.requestType === 'Portfolio Approval' || item.requestType === 'Job Approval' ? 'HIGH' : 'MEDIUM',
|
||||||
|
status: 'PENDING',
|
||||||
|
updatedAt: item.submittedOn || '',
|
||||||
|
sourceKey: `verification:${item.id}`,
|
||||||
|
submittedFields: Array.isArray(item.submittedFields) ? item.submittedFields : [],
|
||||||
|
documents: Array.isArray(item.documents) ? item.documents : [],
|
||||||
|
payload: { queueRequestType: item.requestType },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusBadge(props: { status: string }) {
|
||||||
|
const getColors = () => {
|
||||||
|
switch (props.status) {
|
||||||
|
case 'APPROVED': return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
|
||||||
|
case 'IN_REVIEW': return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
|
||||||
|
case 'PENDING': return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
||||||
|
case 'ON_HOLD': return { border: '#FDE68A', bg: '#FEF3C7', text: '#D97706', dot: '#D97706' };
|
||||||
|
case 'ESCALATED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
||||||
|
case 'REJECTED': return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
||||||
|
default: return { border: '#D1D5DB', bg: '#F3F4F6', text: '#4B5563', dot: '#9CA3AF' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const colors = getColors();
|
||||||
|
const label = props.status.split('_').map(w => w.charAt(0) + w.slice(1).toLowerCase()).join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
|
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PriorityBadge(props: { priority: string }) {
|
||||||
|
const color = props.priority === 'CRITICAL' ? '#7F1D1D' : props.priority === 'HIGH' ? '#DC2626' : props.priority === 'MEDIUM' ? '#F59E0B' : '#16A34A';
|
||||||
|
return (
|
||||||
|
<span style={`display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:${color}`}>
|
||||||
|
<span style={`width:6px;height:6px;border-radius:50%;background:${color}`} />
|
||||||
|
{props.priority}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerificationBadge(props: { status: ApprovalRecord['verificationStatus'] }) {
|
||||||
|
const getColors = () => {
|
||||||
|
if (props.status === 'VERIFIED') return { border: '#B7E4C7', bg: '#DEF7E8', text: '#0B8A4A', dot: '#0B8A4A' };
|
||||||
|
if (props.status === 'FLAGGED') return { border: '#FECACA', bg: '#FEF2F2', text: '#DC2626', dot: '#DC2626' };
|
||||||
|
return { border: '#F6D78F', bg: '#FFF3D6', text: '#B7791F', dot: '#B7791F' };
|
||||||
|
};
|
||||||
|
const colors = getColors();
|
||||||
|
return (
|
||||||
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${colors.border};background:${colors.bg};color:${colors.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
|
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${colors.dot};margin-right:5px;flex-shrink:0`} />
|
||||||
|
{props.status}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ApprovalManagementPage() {
|
||||||
|
const [listTab, setListTab] = createSignal<'all' | 'view' | 'escalated'>('all');
|
||||||
|
const [detailTab, setDetailTab] = createSignal<'overview' | 'verification' | 'checklist' | 'logs'>('overview');
|
||||||
|
|
||||||
|
const [search, setSearch] = createSignal('');
|
||||||
|
const [rows, setRows] = createSignal<ApprovalRecord[]>([]);
|
||||||
|
const [viewingCase, setViewingCase] = createSignal<ApprovalRecord | null>(null);
|
||||||
|
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = createSignal<'all' | 'pending' | 'escalated'>('all');
|
||||||
|
const [sortBy, setSortBy] = createSignal<'submitted_desc' | 'submitted_asc' | 'priority_desc' | 'priority_asc'>('submitted_desc');
|
||||||
|
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
||||||
|
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
const [isActing, setIsActing] = createSignal(false);
|
||||||
|
|
||||||
|
const selectedDocuments = createMemo<ApprovalDocument[]>(() => {
|
||||||
|
const row = viewingCase();
|
||||||
|
if (!row) return [];
|
||||||
|
if (Array.isArray(row.documents) && row.documents.length) return row.documents;
|
||||||
|
return extractDocuments(row.payload || {});
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedFields = createMemo<ApprovalSubmittedField[]>(() => {
|
||||||
|
const row = viewingCase();
|
||||||
|
if (!row) return [];
|
||||||
|
if (Array.isArray(row.submittedFields) && row.submittedFields.length) return row.submittedFields;
|
||||||
|
return extractSubmittedFields(row.payload || row);
|
||||||
|
});
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
|
: '';
|
||||||
|
const res = await fetch(`${API}/api/admin/verifications?page=1&limit=100`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||||
|
const payload = await res.json().catch(() => ({} as any));
|
||||||
|
const items = Array.isArray(payload?.items) ? payload.items : [];
|
||||||
|
|
||||||
|
const mappedItems: ApprovalRecord[] = items.map((v: any) => {
|
||||||
|
const p = v.payload || {};
|
||||||
|
return {
|
||||||
|
id: v.id,
|
||||||
|
name: `${toTitle(v.type)} - ${v.user_name || 'Applicant'}`,
|
||||||
|
applicantName: v.user_name || 'Applicant',
|
||||||
|
approvalType: verificationToApprovalType(v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')),
|
||||||
|
userType: normalizeUserType(v.role_key),
|
||||||
|
roleTags: [toTitle(v.role_key)],
|
||||||
|
primaryService: toTitle(v.role_key || 'User'),
|
||||||
|
area: p.city || p.area || 'Unknown',
|
||||||
|
submittedDate: v.created_at,
|
||||||
|
verificationStatus: v.status === 'APPROVED' ? 'VERIFIED' : 'PENDING',
|
||||||
|
assignedApprover: 'Unassigned',
|
||||||
|
priority: 'MEDIUM',
|
||||||
|
status: v.status === 'APPROVED' ? 'APPROVED' : (v.status === 'REJECTED' ? 'REJECTED' : 'PENDING'),
|
||||||
|
updatedAt: v.updated_at,
|
||||||
|
sourceKey: `v:${v.id}`,
|
||||||
|
submittedFields: extractSubmittedFields(p),
|
||||||
|
documents: extractDocuments(p),
|
||||||
|
payload: v,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setRows(mappedItems);
|
||||||
|
} catch (e: any) {
|
||||||
|
setRows([]);
|
||||||
|
setError(e?.message || 'Could not reach approvals API.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => void load());
|
||||||
|
|
||||||
|
const formatDate = (v?: string) => {
|
||||||
|
const s = v || '';
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
|
||||||
|
return s.slice(0, 10) || '—';
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredRows = createMemo(() => {
|
||||||
|
let list = rows();
|
||||||
|
const f = statusFilter();
|
||||||
|
if (f === 'pending') list = list.filter((r) => r.status === 'PENDING' || r.status === 'IN_REVIEW');
|
||||||
|
if (f === 'escalated') list = list.filter((r) => r.status === 'ESCALATED');
|
||||||
|
|
||||||
|
const q = search().trim().toLowerCase();
|
||||||
|
if (q) {
|
||||||
|
list = list.filter((r) =>
|
||||||
|
String(r.applicantName || '').toLowerCase().includes(q)
|
||||||
|
|| String(r.id || '').toLowerCase().includes(q)
|
||||||
|
|| String(r.approvalType || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...list];
|
||||||
|
const mode = sortBy();
|
||||||
|
const priorityRank = (p: ApprovalRecord['priority']) => (p === 'CRITICAL' ? 4 : p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const ad = Date.parse(String(a.submittedDate || a.updatedAt || '')) || 0;
|
||||||
|
const bd = Date.parse(String(b.submittedDate || b.updatedAt || '')) || 0;
|
||||||
|
if (mode === 'submitted_asc') return ad - bd;
|
||||||
|
if (mode === 'priority_desc') return priorityRank(b.priority) - priorityRank(a.priority);
|
||||||
|
if (mode === 'priority_asc') return priorityRank(a.priority) - priorityRank(b.priority);
|
||||||
|
return bd - ad;
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
});
|
||||||
|
|
||||||
|
const escalatedCount = createMemo(() => rows().filter((r) => r.status === 'ESCALATED').length);
|
||||||
|
|
||||||
|
const exportCsv = () => {
|
||||||
|
const headers = ['Approval ID', 'Applicant', 'Type', 'Verification', 'Priority', 'Status', 'Submitted Date'];
|
||||||
|
const rowsData = filteredRows().map((row) => [
|
||||||
|
row.id || '',
|
||||||
|
row.applicantName || '',
|
||||||
|
row.approvalType || '',
|
||||||
|
row.verificationStatus || '',
|
||||||
|
row.priority || '',
|
||||||
|
row.status || '',
|
||||||
|
formatDate(row.submittedDate || row.updatedAt),
|
||||||
|
]);
|
||||||
|
const csv = [headers, ...rowsData]
|
||||||
|
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `approval-management-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openView = (row: ApprovalRecord) => {
|
||||||
|
setViewingCase(row);
|
||||||
|
setDetailTab('overview');
|
||||||
|
setListTab('view');
|
||||||
|
setOpenMenuId(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLocalStatus = (row: ApprovalRecord, nextStatus: ApprovalRecord['status']) => {
|
||||||
|
setRows((prev) => prev.map((item) => ((item.sourceKey || item.id) === (row.sourceKey || row.id) ? { ...item, status: nextStatus } : item)));
|
||||||
|
setViewingCase((current) => {
|
||||||
|
if (!current) return current;
|
||||||
|
return (current.sourceKey || current.id) === (row.sourceKey || row.id) ? { ...current, status: nextStatus } : current;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const runApprovalAction = async (row: ApprovalRecord, action: 'approve' | 'reject') => {
|
||||||
|
const type = row.approvalType;
|
||||||
|
const nextStatus: ApprovalRecord['status'] = action === 'approve' ? 'APPROVED' : 'REJECTED';
|
||||||
|
|
||||||
|
if (type !== 'JOB' && type !== 'REQUIREMENT') {
|
||||||
|
setLocalStatus(row, nextStatus);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsActing(true);
|
||||||
|
setError('');
|
||||||
|
try {
|
||||||
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
|
: '';
|
||||||
|
const endpoint = `${API}/api/admin/verifications/${row.id}/${action}`;
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: action === 'reject'
|
||||||
|
? JSON.stringify({ reason: 'Rejected by admin from approval management' })
|
||||||
|
: JSON.stringify({}),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
throw new Error((data as any).message || `Request failed (${res.status})`);
|
||||||
|
}
|
||||||
|
await load();
|
||||||
|
setViewingCase(null);
|
||||||
|
setListTab('all');
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e?.message || 'Approval action failed.');
|
||||||
|
} finally {
|
||||||
|
setIsActing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="w-full space-y-6 pb-8">
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1.5rem">
|
||||||
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Approval Management</h1>
|
||||||
|
<p class="mt-1 text-[14px] text-[#6B7280]">Manage final approval decisions for all platform entities and requests</p>
|
||||||
|
</div>
|
||||||
|
<Show when={error()}>
|
||||||
|
<div style="margin-bottom:10px;border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">
|
||||||
|
{error()}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={true}>
|
||||||
|
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB">
|
||||||
|
{([
|
||||||
|
{ key: 'all', label: 'All Approvals', action: () => { setListTab('all'); setStatusFilter('all'); } },
|
||||||
|
{ key: 'escalated', label: `Escalated (${escalatedCount()})`, action: () => { setListTab('escalated'); setStatusFilter('escalated'); } },
|
||||||
|
{ key: 'view', label: 'View Approval', action: () => { setListTab('view'); } },
|
||||||
|
] as const).map((tab) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={tab.action}
|
||||||
|
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={listTab() === 'view'}>
|
||||||
|
<Show when={!viewingCase()}>
|
||||||
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
||||||
|
<p style="font-size:15px;font-weight:600;color:#111827">No approval selected</p>
|
||||||
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click the <strong>⋮</strong> menu on any row and choose <strong>View Approval</strong>.</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={viewingCase()}>
|
||||||
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px">
|
||||||
|
<h2 style="font-size:18px;font-weight:700;color:#111827">{viewingCase()!.applicantName}</h2>
|
||||||
|
<StatusBadge status={viewingCase()!.status} />
|
||||||
|
<PriorityBadge priority={viewingCase()!.priority} />
|
||||||
|
</div>
|
||||||
|
<p style="margin-top:2px;font-size:13px;color:#6B7280">ID: {viewingCase()!.id} • {viewingCase()!.approvalType} • Submitted: {formatDate(viewingCase()!.submittedDate)}</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px">
|
||||||
|
<button type="button" onClick={() => void runApprovalAction(viewingCase()!, 'approve')} disabled={isActing()} style={`height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;border:none;cursor:pointer;opacity:${isActing() ? 0.7 : 1}`}>Approve</button>
|
||||||
|
<button type="button" onClick={() => void runApprovalAction(viewingCase()!, 'reject')} disabled={isActing()} style={`height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer;opacity:${isActing() ? 0.7 : 1}`}>Reject</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:4px;min-height:44px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
|
||||||
|
{(['overview', 'verification', 'checklist', 'logs'] as const).map((tab, i) => {
|
||||||
|
const labels = ['Overview', 'Verification Summary', 'Approval Checklist', 'Activity Logs'];
|
||||||
|
const active = () => detailTab() === tab;
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
|
||||||
|
{labels[i]}
|
||||||
|
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:24px">
|
||||||
|
<Show when={detailTab() === 'overview'}>
|
||||||
|
<div style="display:grid;grid-template-columns:2fr 1fr;gap:24px">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:24px">
|
||||||
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
||||||
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Approval Summary</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Entity Name</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.applicantName}</p></div>
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Approval Type</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.approvalType}</p></div>
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Primary Service</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.primaryService || '—'}</p></div>
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Area / Place</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.area || '—'}</p></div>
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Verification Status</p><VerificationBadge status={viewingCase()!.verificationStatus} /></div>
|
||||||
|
<div><p style="font-size:11px;color:#9CA3AF">Assigned Approver</p><p style="font-size:14px;font-weight:600;color:#111827">{viewingCase()!.assignedApprover}</p></div>
|
||||||
|
</div>
|
||||||
|
<Show when={viewingCase()!.roleTags?.length}>
|
||||||
|
<div style="margin-top:12px">
|
||||||
|
<p style="margin:0 0 8px;font-size:11px;color:#9CA3AF">Registered Roles / Services</p>
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||||||
|
<For each={viewingCase()!.roleTags || []}>
|
||||||
|
{(tag) => (
|
||||||
|
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:600;color:#374151">{tag}</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={selectedFields().length > 0}>
|
||||||
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
|
||||||
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:12px">Submitted Details</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||||||
|
<For each={selectedFields()}>
|
||||||
|
{(field) => (
|
||||||
|
<div style="border:1px solid #F3F4F6;border-radius:10px;padding:10px;background:#FAFAFA">
|
||||||
|
<p style="font-size:11px;color:#9CA3AF">{field.label}</p>
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#111827;line-height:1.4;word-break:break-word">{field.value || '—'}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px;background:#F9FAFB">
|
||||||
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Decision Notes</h3>
|
||||||
|
<textarea placeholder="Add decision note..." style="width:100%;height:100px;border-radius:8px;border:1px solid #E5E7EB;padding:10px;font-size:13px;resize:none;margin-bottom:12px" />
|
||||||
|
<button type="button" style="width:100%;height:34px;background:#0D0D2A;color:white;border-radius:8px;font-size:12px;font-weight:600;border:none">Add Note</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={detailTab() === 'verification'}>
|
||||||
|
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px;background:#FAFAFA">
|
||||||
|
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Submitted Documents</h3>
|
||||||
|
<Show when={selectedDocuments().length > 0} fallback={<p style="font-size:13px;color:#6B7280">No documents submitted for this request.</p>}>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
<For each={selectedDocuments()}>
|
||||||
|
{(doc) => (
|
||||||
|
<div style="display:grid;grid-template-columns:1fr auto auto;gap:12px;align-items:center;padding:10px;border-radius:10px;border:1px solid #E5E7EB;background:white">
|
||||||
|
<div>
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#111827">{doc.title}</p>
|
||||||
|
<p style="font-size:12px;color:#6B7280">{doc.type} • {doc.status}</p>
|
||||||
|
</div>
|
||||||
|
<a href={doc.url} target="_blank" rel="noreferrer" style="display:inline-flex;height:30px;align-items:center;justify-content:center;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:12px;font-weight:600;color:#374151;background:white;text-decoration:none">View</a>
|
||||||
|
<span style="font-size:12px;color:#6B7280">{doc.status}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={detailTab() === 'checklist'}>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:12px">
|
||||||
|
{[
|
||||||
|
'Verification report is complete and reviewed',
|
||||||
|
'All required entity data is present',
|
||||||
|
'No active risk flags on applicant',
|
||||||
|
'Applicant meets all policy requirements',
|
||||||
|
'Final approval readiness confirmed',
|
||||||
|
].map((item) => (
|
||||||
|
<label style="display:flex;align-items:center;gap:12px;padding:12px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer">
|
||||||
|
<input type="checkbox" style="width:16px;height:16px;accent-color:#FF5E13" />
|
||||||
|
<span style="font-size:13px;color:#111827;font-weight:500">{item}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={detailTab() === 'logs'}>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:16px">
|
||||||
|
<div style="display:flex;gap:12px">
|
||||||
|
<div style="width:8px;height:8px;border-radius:50%;background:#FF5E13;margin-top:4px" />
|
||||||
|
<div>
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#111827">Approval Review Started</p>
|
||||||
|
<p style="font-size:12px;color:#6B7280">Approver started final review • 1 hour ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:12px">
|
||||||
|
<div style="width:8px;height:8px;border-radius:50%;background:#E5E7EB;margin-top:4px" />
|
||||||
|
<div>
|
||||||
|
<p style="font-size:13px;font-weight:600;color:#111827">Verification Completed</p>
|
||||||
|
<p style="font-size:12px;color:#6B7280">Case handed over from verification team • 1 day ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;padding:14px 24px;border-top:1px solid #E5E7EB">
|
||||||
|
<button type="button" onClick={() => { setViewingCase(null); setListTab('all'); }} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div style={`display:${listTab() === 'view' ? 'none' : 'block'}`}>
|
||||||
|
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
|
<input
|
||||||
|
value={search()}
|
||||||
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
placeholder="Search approvals..."
|
||||||
|
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
||||||
|
/>
|
||||||
|
<div style="position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
||||||
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
||||||
|
Sort
|
||||||
|
</button>
|
||||||
|
<Show when={sortMenuOpen()}>
|
||||||
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||||
|
{(['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const).map((s, i) => (
|
||||||
|
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||||
|
{['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
||||||
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||||
|
Filters
|
||||||
|
</button>
|
||||||
|
<Show when={filterMenuOpen()}>
|
||||||
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||||
|
{(['all', 'pending', 'escalated'] as const).map((s) => (
|
||||||
|
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||||
|
{s === 'all' ? 'All Cases' : s === 'pending' ? 'Pending Decisions' : 'Escalated'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={exportCsv} style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A;text-align:left">
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Approval ID</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Applicant / Entity</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Type</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Verification</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Priority</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Status</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Submitted</th>
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Show
|
||||||
|
when={filteredRows().length > 0}
|
||||||
|
fallback={
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} style="padding:32px;text-align:center">
|
||||||
|
<p style="font-size:15px;font-weight:600;color:#111827">No approvals found</p>
|
||||||
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={filteredRows()}>
|
||||||
|
{(row) => (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
||||||
|
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">{row.id}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<p style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</p>
|
||||||
|
<p style="font-size:11px;color:#6B7280">{row.userType}{row.area ? ` • ${row.area}` : ''}</p>
|
||||||
|
<Show when={row.roleTags?.length}>
|
||||||
|
<p style="margin-top:2px;font-size:11px;color:#9CA3AF">{(row.roleTags || []).join(', ')}</p>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{row.approvalType}</td>
|
||||||
|
<td style="padding:12px 20px"><VerificationBadge status={row.verificationStatus} /></td>
|
||||||
|
<td style="padding:12px 20px"><PriorityBadge priority={row.priority} /></td>
|
||||||
|
<td style="padding:12px 20px"><StatusBadge status={row.status} /></td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{formatDate(row.submittedDate || row.updatedAt)}</td>
|
||||||
|
<td style="padding:12px 20px;position:relative">
|
||||||
|
<button type="button" onClick={() => setOpenMenuId(openMenuId() === row.id ? null : row.id)} style="display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;border-radius:8px;color:#9CA3AF;background:none;border:none;cursor:pointer">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
|
||||||
|
</button>
|
||||||
|
<Show when={openMenuId() === row.id}>
|
||||||
|
<div style="position:absolute;right:20px;top:44px;z-index:20;width:210px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 20px rgba(0,0,0,0.12)">
|
||||||
|
<button type="button" onClick={() => openView(row)} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" 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>
|
||||||
|
View Approval
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => void runApprovalAction(row, 'approve')} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#374151;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg style="width:16px;height:16px;color:#FF5E13;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m8 12 2.5 2.5L16 9"/></svg>
|
||||||
|
Approve
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => void runApprovalAction(row, 'reject')} style="display:flex;width:100%;align-items:center;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;color:#DC2626;background:none;border:none;cursor:pointer;text-align:left">
|
||||||
|
<svg style="width:16px;height:16px;flex-shrink:0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={filteredRows().length > 0}>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||||||
|
<p style="font-size:13px;color:#6B7280">
|
||||||
|
Showing <strong style="font-weight:600;color:#111827">1–{filteredRows().length}</strong> of <strong style="font-weight:600;color:#111827">{filteredRows().length}</strong> approvals
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type CandidateRecord = CrudRecord & {
|
type CandidateRecord = CrudRecord & {
|
||||||
email: string;
|
email: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type CompanyCore = {
|
type CompanyCore = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
import { createSignal, Show } from 'solid-js';
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
export default function CreateCompanyPage() {
|
export default function CreateCompanyPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function CompanyManagementPage() {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
||||||
const r = await fetch('/api/gateway/api/admin/companies', {
|
const r = await fetch('/api/admin/companies', {
|
||||||
headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
|
headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, Show, For } from 'solid-js';
|
import { createResource, createSignal, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
const ROLE_OPTIONS = [
|
||||||
'company',
|
'company',
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, createResource, Show, For } from 'solid-js';
|
import { createSignal, createResource, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type LedgerEntry = {
|
type LedgerEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type CustomerRecord = CrudRecord & {
|
type CustomerRecord = CrudRecord & {
|
||||||
email: string;
|
email: string;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import { useSearchParams } from '@solidjs/router';
|
import { useSearchParams } from '@solidjs/router';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type DepartmentRecord = CrudRecord & {
|
type DepartmentRecord = CrudRecord & {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import { useSearchParams } from '@solidjs/router';
|
import { useSearchParams } from '@solidjs/router';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type DesignationRecord = CrudRecord & {
|
type DesignationRecord = CrudRecord & {
|
||||||
code?: string;
|
code?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, Show, For } from 'solid-js';
|
import { createResource, createSignal, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
const ROLE_OPTIONS = [
|
||||||
'company',
|
'company',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate, useParams } from '@solidjs/router';
|
import { A, useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Role = { id: string; name: string };
|
type Role = { id: string; name: string };
|
||||||
type Employee = { id: string; name?: string; full_name?: string; email: string; role_id?: string; role_name?: string; role?: { id?: string } };
|
type Employee = { id: string; name?: string; full_name?: string; email: string; role_id?: string; role_name?: string; role?: { id?: string } };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
import { createResource, createSignal, For, onMount, Show } from 'solid-js';
|
import { createResource, createSignal, For, onMount, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Role = { id: string; name: string };
|
type Role = { id: string; name: string };
|
||||||
type Dept = { id: string; name: string };
|
type Dept = { id: string; name: string };
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import { useSearchParams } from '@solidjs/router';
|
import { useSearchParams } from '@solidjs/router';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type EmployeeRecord = CrudRecord & {
|
type EmployeeRecord = CrudRecord & {
|
||||||
employeeId?: string;
|
employeeId?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
|
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type RoleOption = { id: string; key: string; name: string };
|
type RoleOption = { id: string; key: string; name: string };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { For, Show, createMemo, createSignal, onMount, createEffect } from 'soli
|
||||||
import { useSearchParams } from '@solidjs/router';
|
import { useSearchParams } from '@solidjs/router';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type ExternalRoleRecord = {
|
type ExternalRoleRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import {
|
||||||
import type { RuntimeDashboardLayout } from '~/lib/runtime/types';
|
import type { RuntimeDashboardLayout } from '~/lib/runtime/types';
|
||||||
import { loadAdminDashboardLayout, saveAdminDashboardLayout } from '~/lib/runtime/storage';
|
import { loadAdminDashboardLayout, saveAdminDashboardLayout } from '~/lib/runtime/storage';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
async function fetchMetrics() {
|
async function fetchMetrics() {
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
|
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type RoleOption = { id: string; key: string; name: string };
|
type RoleOption = { id: string; key: string; name: string };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, createMemo, Show } from 'solid-js';
|
import { createResource, createSignal, createMemo, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
async function loadInvoices(): Promise<any[]> {
|
async function loadInvoices(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Job = {
|
type Job = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type JobRecord = CrudRecord & {
|
type JobRecord = CrudRecord & {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createResource, Show } from 'solid-js';
|
import { createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type KbArticle = {
|
type KbArticle = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate, useParams } from '@solidjs/router';
|
import { A, useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createEffect, createResource, createSignal, Show } from 'solid-js';
|
import { createEffect, createResource, createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type KbArticle = {
|
type KbArticle = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
Clock, Globe, AlertCircle
|
Clock, Globe, AlertCircle
|
||||||
} from 'lucide-solid';
|
} from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
interface Category {
|
interface Category {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Lead = {
|
type Lead = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
const ROLE_OPTIONS = [
|
const ROLE_OPTIONS = [
|
||||||
'company', 'job_seeker', 'customer', 'photographer', 'video_editor',
|
'company', 'job_seeker', 'customer', 'photographer', 'video_editor',
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, Show, For } from 'solid-js';
|
import { createResource, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type LedgerEntry = {
|
type LedgerEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type ModuleRecord = {
|
type ModuleRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, createMemo, Show, For } from 'solid-js';
|
import { createSignal, createMemo, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type NotificationRow = {
|
type NotificationRow = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import OnboardingFlowBuilder, {
|
||||||
type OnboardingStep,
|
type OnboardingStep,
|
||||||
} from '~/components/admin/OnboardingFlowBuilder';
|
} from '~/components/admin/OnboardingFlowBuilder';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
const FRONTEND_PREVIEW_BASE = String(import.meta.env.VITE_FRONTEND_PREVIEW_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
const FRONTEND_PREVIEW_BASE = String(import.meta.env.VITE_FRONTEND_PREVIEW_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
||||||
|
|
||||||
function normalizeRoleKey(value: string): string {
|
function normalizeRoleKey(value: string): string {
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ export default function OnboardingManagementPage() {
|
||||||
|
|
||||||
const [auditSearch, setAuditSearch] = createSignal('');
|
const [auditSearch, setAuditSearch] = createSignal('');
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import OnboardingFlowBuilder, {
|
||||||
type OnboardingField,
|
type OnboardingField,
|
||||||
} from '~/components/admin/OnboardingFlowBuilder';
|
} from '~/components/admin/OnboardingFlowBuilder';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
const FRONTEND_PREVIEW_BASE = String(import.meta.env.VITE_FRONTEND_PREVIEW_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
const FRONTEND_PREVIEW_BASE = String(import.meta.env.VITE_FRONTEND_PREVIEW_URL || 'http://localhost:3001').replace(/\/+$/, '');
|
||||||
|
|
||||||
function normalizeRoleKey(value: string): string {
|
function normalizeRoleKey(value: string): string {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Order = {
|
type Order = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { For, createMemo, createResource, Show } from 'solid-js';
|
import { For, createMemo, createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Photographer = {
|
type Photographer = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, Show, For } from 'solid-js';
|
import { createResource, createSignal, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Package = {
|
type Package = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createSignal, Show } from 'solid-js';
|
import { createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type UserReport = {
|
type UserReport = {
|
||||||
total_users?: number;
|
total_users?: number;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Requirement = {
|
type Requirement = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Requirement = {
|
type Requirement = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Review = {
|
type Review = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { A, useNavigate, useSearchParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, Show } from 'solid-js';
|
import { createMemo, createResource, Show } from 'solid-js';
|
||||||
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type ExternalRole = {
|
type ExternalRole = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate, useParams } from '@solidjs/router';
|
import { A, useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createEffect, createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Permission = { key: string; module: string; action: string };
|
type Permission = { key: string; module: string; action: string };
|
||||||
type Department = { id: string; name: string };
|
type Department = { id: string; name: string };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams } from '@solidjs/router';
|
import { A, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, For, Show } from 'solid-js';
|
import { createMemo, createResource, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Permission = { key: string; module: string; action: string };
|
type Permission = { key: string; module: string; action: string };
|
||||||
type RoleDetail = {
|
type RoleDetail = {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate } from '@solidjs/router';
|
import { A, useNavigate } from '@solidjs/router';
|
||||||
import { createEffect, createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
import { createEffect, createMemo, createResource, createSignal, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Permission = { key: string; module: string; action: string };
|
type Permission = { key: string; module: string; action: string };
|
||||||
type Department = { id: string; name: string };
|
type Department = { id: string; name: string };
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
|
import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
|
||||||
import type { CrudRecord } from '~/lib/admin/types';
|
import type { CrudRecord } from '~/lib/admin/types';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
const ACTIONS = ['View', 'Create', 'Update', 'Delete'] as const;
|
const ACTIONS = ['View', 'Create', 'Update', 'Delete'] as const;
|
||||||
const STATIC_MODULES = [
|
const STATIC_MODULES = [
|
||||||
'Department Management', 'Designation Management', 'Internal Role Management',
|
'Department Management', 'Designation Management', 'Internal Role Management',
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
import { createMemo, createResource, For, Show } from 'solid-js';
|
import { createMemo, createResource, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type RoleTemplate = {
|
type RoleTemplate = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createMemo, createResource, createSignal, Show } from 'solid-js';
|
||||||
import ExternalRoleForm, { type ExternalRoleConfig } from '~/components/admin/ExternalRoleForm';
|
import ExternalRoleForm, { type ExternalRoleConfig } from '~/components/admin/ExternalRoleForm';
|
||||||
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
function normalizeRoleKey(value: string): string {
|
function normalizeRoleKey(value: string): string {
|
||||||
return String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
return String(value || '').trim().toLowerCase().replace(/[-\s]+/g, '_');
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { A } from '@solidjs/router';
|
||||||
import { createResource, createSignal, For, Show } from 'solid-js';
|
import { createResource, createSignal, For, Show } from 'solid-js';
|
||||||
import { Globe, ShieldCheck, ShieldOff, Layers, MoreVertical, Search, Plus } from 'lucide-solid';
|
import { Globe, ShieldCheck, ShieldOff, Layers, MoreVertical, Search, Plus } from 'lucide-solid';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type ExternalRole = {
|
type ExternalRole = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { createResource, createSignal, Show } from 'solid-js';
|
||||||
import ExternalRoleForm, { type ExternalRoleConfig } from '~/components/admin/ExternalRoleForm';
|
import ExternalRoleForm, { type ExternalRoleConfig } from '~/components/admin/ExternalRoleForm';
|
||||||
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
function defaultConfig(): ExternalRoleConfig {
|
function defaultConfig(): ExternalRoleConfig {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
import { createResource, createSignal, createMemo, Show, For } from 'solid-js';
|
||||||
import { A } from '@solidjs/router';
|
import { A } from '@solidjs/router';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type SupportCase = {
|
type SupportCase = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createResource, createSignal, Show } from 'solid-js';
|
import { createResource, createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
async function loadTaxes(): Promise<any[]> {
|
async function loadTaxes(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useNavigate, useParams } from '@solidjs/router';
|
import { A, useNavigate, useParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, createSignal, Show } from 'solid-js';
|
import { createMemo, createResource, createSignal, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type Role = {
|
type Role = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useParams, useSearchParams } from '@solidjs/router';
|
import { A, useParams, useSearchParams } from '@solidjs/router';
|
||||||
import { createMemo, createResource, For, Show } from 'solid-js';
|
import { createMemo, createResource, For, Show } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
|
|
||||||
type UserDetail = {
|
type UserDetail = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export default function UsersManagementPage() {
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
try {
|
try {
|
||||||
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
const accessToken = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
|
||||||
const r = await fetch('/api/gateway/api/admin/users', {
|
const r = await fetch('/api/admin/users', {
|
||||||
headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
|
headers: { Accept: 'application/json', ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) },
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
@ -1,988 +0,0 @@
|
||||||
import { A } from '@solidjs/router';
|
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
|
||||||
|
|
||||||
type VerificationStatus = 'PENDING' | 'UNDER_REVIEW' | 'DOCUMENTS_REQUESTED' | 'REVISION_REQUESTED' | 'APPROVED' | 'REJECTED';
|
|
||||||
type VerificationPriority = 'HIGH' | 'MEDIUM' | 'LOW';
|
|
||||||
|
|
||||||
type VerificationRow = {
|
|
||||||
id: string;
|
|
||||||
applicantName: string;
|
|
||||||
requestType:
|
|
||||||
| 'Profile Approval'
|
|
||||||
| 'Portfolio Approval'
|
|
||||||
| 'Company Approval'
|
|
||||||
| 'Job Seeker Approval'
|
|
||||||
| 'Service Seeker Profile Approval'
|
|
||||||
| 'Service Seeker Requirement'
|
|
||||||
| 'Job Approval';
|
|
||||||
roleLabel: string;
|
|
||||||
submittedOn: string;
|
|
||||||
status: VerificationStatus;
|
|
||||||
priority: VerificationPriority;
|
|
||||||
userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER';
|
|
||||||
area: string;
|
|
||||||
userId: string;
|
|
||||||
roleKey: string;
|
|
||||||
payload?: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SubmittedDocument = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: 'IMAGE' | 'PDF';
|
|
||||||
url: string;
|
|
||||||
status: 'SUBMITTED' | 'MISSING' | 'INVALID';
|
|
||||||
};
|
|
||||||
|
|
||||||
type PortfolioAsset = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ApprovalQueueItem = {
|
|
||||||
id: string;
|
|
||||||
requestType: VerificationRow['requestType'];
|
|
||||||
applicantName: string;
|
|
||||||
roleLabel: string;
|
|
||||||
userType: VerificationRow['userType'];
|
|
||||||
roleKey: string;
|
|
||||||
area: string;
|
|
||||||
submittedOn: string;
|
|
||||||
documents: SubmittedDocument[];
|
|
||||||
submittedFields: Array<{ label: string; value: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROLE_PROFILE_FIELDS: Record<string, string[]> = {
|
|
||||||
CUSTOMER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Area', 'Place', 'PIN Code', 'Service Category'],
|
|
||||||
COMPANY: ['Company Name', 'Company Email', 'Company Phone', 'City', 'Area', 'Place', 'PIN Code', 'Website URL'],
|
|
||||||
JOB_SEEKER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Current Role', 'Total Experience', 'City', 'Area', 'Place'],
|
|
||||||
PROFESSIONAL: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const ROLE_DOCUMENTS: Record<string, string[]> = {
|
|
||||||
CUSTOMER: ['Identity Proof', 'Address Proof'],
|
|
||||||
COMPANY: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'],
|
|
||||||
JOB_SEEKER: ['Identity Proof', 'Address Proof', 'Education Proof'],
|
|
||||||
PHOTOGRAPHER: ['Identity Proof', 'Address Proof', 'Portfolio Ownership Proof'],
|
|
||||||
MAKEUP_ARTIST: ['Identity Proof', 'Address Proof', 'Professional Certifications'],
|
|
||||||
DEVELOPER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
VIDEO_EDITOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
UGC_CONTENT_CREATOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
GRAPHIC_DESIGNER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
SOCIAL_MEDIA_MANAGER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
FITNESS_TRAINER: ['Identity Proof', 'Address Proof', 'Certification Proof'],
|
|
||||||
TUTOR: ['Identity Proof', 'Address Proof', 'Educational Proof'],
|
|
||||||
CATERING_SERVICES: ['Identity Proof', 'Address Proof', 'Food License'],
|
|
||||||
PROFESSIONAL: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const REQUIREMENT_ROLE_FIELDS: Record<string, string[]> = {
|
|
||||||
PHOTOGRAPHER: ['Event Type', 'Shoot Type', 'Event Date & Time', 'Event Duration (Hours)', 'Venue / Location', 'Number of People', 'Delivery Deadline'],
|
|
||||||
MAKEUP_ARTIST: ['Event Type', 'Makeup Category', 'Event Date & Time', 'Artists Required', 'Venue / Location', 'Skin Tone Preference', 'Ready By Time'],
|
|
||||||
TUTOR: ['Subject', 'Class / Grade', 'Mode (Online / Offline)', 'Sessions Per Week', 'Preferred Start Date', 'Student Location', 'Exam Goal'],
|
|
||||||
DEVELOPER: ['Project Type', 'Platform', 'Preferred Stack', 'Project Duration', 'Launch Deadline', 'Team Size Needed', 'Support Duration'],
|
|
||||||
VIDEO_EDITOR: ['Video Category', 'Final Duration', 'Footage Volume', 'Delivery Date', 'Editing Style', 'Platform', 'Revision Rounds'],
|
|
||||||
UGC_CONTENT_CREATOR: ['Campaign Goal', 'Platform', 'Deliverables Needed', 'Brand Category', 'Delivery Deadline', 'Target Audience', 'Usage Rights Duration'],
|
|
||||||
FITNESS_TRAINER: ['Primary Goal', 'Current Activity Level', 'Preferred Mode', 'Training Days Per Week', 'Preferred Timings', 'Health Conditions', 'Goal Timeline'],
|
|
||||||
CATERING_SERVICES: ['Event Type', 'Cuisine Preference', 'Guest Count', 'Service Date', 'Venue / Location', 'Meal Slot', 'Serving Style'],
|
|
||||||
GRAPHIC_DESIGNER: ['Project Type', 'Brand Industry', 'Deliverables Needed', 'Deadline', 'Target Audience', 'Reference Links', 'Output Formats'],
|
|
||||||
SOCIAL_MEDIA_MANAGER: ['Primary Goal', 'Platforms', 'Posting Frequency', 'Campaign Duration', 'Start Date', 'Brand Category', 'Monthly Budget'],
|
|
||||||
};
|
|
||||||
|
|
||||||
const JOB_POSTING_FIELDS = [
|
|
||||||
'Job Title',
|
|
||||||
'Department',
|
|
||||||
'Job Category',
|
|
||||||
'Employment Type',
|
|
||||||
'Seniority',
|
|
||||||
'Openings',
|
|
||||||
'Role & Requirements',
|
|
||||||
'Compensation',
|
|
||||||
'Location',
|
|
||||||
'Description',
|
|
||||||
];
|
|
||||||
|
|
||||||
const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue';
|
|
||||||
|
|
||||||
const API = '/api/gateway';
|
|
||||||
|
|
||||||
const toTitle = (value: string) => String(value || '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
|
|
||||||
const statusUi = (status: VerificationStatus) => {
|
|
||||||
if (status === 'APPROVED') return { bg: '#ECFDF3', border: '#BBF7D0', text: '#166534', label: 'Approved' };
|
|
||||||
if (status === 'UNDER_REVIEW') return { bg: '#EEF2FF', border: '#C7D2FE', text: '#3730A3', label: 'Under Review' };
|
|
||||||
if (status === 'DOCUMENTS_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Document Requested' };
|
|
||||||
if (status === 'REVISION_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Revision Requested' };
|
|
||||||
if (status === 'REJECTED') return { bg: '#FEF2F2', border: '#FECACA', text: '#B91C1C', label: 'Rejected' };
|
|
||||||
return { bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', label: 'Pending' };
|
|
||||||
};
|
|
||||||
|
|
||||||
const priorityUi = (priority: VerificationPriority) => {
|
|
||||||
if (priority === 'HIGH') return { color: '#DC2626', label: 'High' };
|
|
||||||
if (priority === 'MEDIUM') return { color: '#D97706', label: 'Medium' };
|
|
||||||
return { color: '#64748B', label: 'Low' };
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseDate = (value: string) => {
|
|
||||||
const ts = Date.parse(String(value || ''));
|
|
||||||
return Number.isNaN(ts) ? 0 : ts;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeRoleSpecKey = (value: string) => {
|
|
||||||
const key = String(value || '').toUpperCase();
|
|
||||||
if (key.includes('COMPANY')) return 'COMPANY';
|
|
||||||
if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
|
|
||||||
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOB_SEEKER';
|
|
||||||
if (key.includes('PHOTOGRAPHER') || key.includes('PHOTO')) return 'PHOTOGRAPHER';
|
|
||||||
if (key.includes('MAKEUP')) return 'MAKEUP_ARTIST';
|
|
||||||
if (key.includes('DEVELOPER')) return 'DEVELOPER';
|
|
||||||
if (key.includes('VIDEO')) return 'VIDEO_EDITOR';
|
|
||||||
if (key.includes('UGC') || (key.includes('CONTENT') && key.includes('CREATOR'))) return 'UGC_CONTENT_CREATOR';
|
|
||||||
if (key.includes('GRAPHIC')) return 'GRAPHIC_DESIGNER';
|
|
||||||
if (key.includes('SOCIAL')) return 'SOCIAL_MEDIA_MANAGER';
|
|
||||||
if (key.includes('FITNESS')) return 'FITNESS_TRAINER';
|
|
||||||
if (key.includes('TUTOR')) return 'TUTOR';
|
|
||||||
if (key.includes('CATER')) return 'CATERING_SERVICES';
|
|
||||||
return 'PROFESSIONAL';
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function VerificationManagementPage() {
|
|
||||||
const [rows, setRows] = createSignal<VerificationRow[]>([]);
|
|
||||||
const [search, setSearch] = createSignal('');
|
|
||||||
const [statusFilter, setStatusFilter] = createSignal<'ALL' | VerificationStatus>('ALL');
|
|
||||||
const [sortBy, setSortBy] = createSignal<'latest' | 'oldest' | 'priority'>('latest');
|
|
||||||
const [sortOpen, setSortOpen] = createSignal(false);
|
|
||||||
const [filterOpen, setFilterOpen] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal('');
|
|
||||||
|
|
||||||
const [categoryTab, setCategoryTab] = createSignal<'all' | 'profile' | 'portfolio' | 'company' | 'job_seeker' | 'service_profile' | 'service_requirement' | 'job'>('all');
|
|
||||||
const [listTab, setListTab] = createSignal<'all' | 'view'>('all');
|
|
||||||
const [selectedRow, setSelectedRow] = createSignal<VerificationRow | null>(null);
|
|
||||||
const [viewer, setViewer] = createSignal<{ open: boolean; title: string; type: 'IMAGE' | 'PDF'; url: string }>({
|
|
||||||
open: false,
|
|
||||||
title: '',
|
|
||||||
type: 'IMAGE',
|
|
||||||
url: '',
|
|
||||||
});
|
|
||||||
const [docSelection, setDocSelection] = createSignal<Record<string, boolean>>({});
|
|
||||||
const [requestNote, setRequestNote] = createSignal('');
|
|
||||||
const [actionMessage, setActionMessage] = createSignal('');
|
|
||||||
|
|
||||||
const load = async () => {
|
|
||||||
try {
|
|
||||||
setError('');
|
|
||||||
const res = await fetch(`${API}/api/admin/verifications?page=1&limit=200`, {
|
|
||||||
headers: { Accept: 'application/json' },
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error(`Failed to load verification queue (${res.status})`);
|
|
||||||
const data = await res.json().catch(() => ({} as any));
|
|
||||||
const items = Array.isArray(data?.items) ? data.items : [];
|
|
||||||
|
|
||||||
const mergedRows: VerificationRow[] = items.map((v: any) => {
|
|
||||||
const payload = v.payload || {};
|
|
||||||
const userType = (v.type === 'job_approval' ? 'COMPANY' : (v.type === 'requirement_approval' ? 'CUSTOMER' : 'PROFESSIONAL')) as VerificationRow['userType'];
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v.id,
|
|
||||||
applicantName: v.user_name || 'Applicant',
|
|
||||||
requestType: (v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')) as VerificationRow['requestType'],
|
|
||||||
roleLabel: toTitle(v.role_key || 'User'),
|
|
||||||
submittedOn: v.created_at,
|
|
||||||
status: v.status as VerificationStatus,
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
userType,
|
|
||||||
area: payload.city || payload.area || 'Unknown',
|
|
||||||
userId: v.user_id,
|
|
||||||
roleKey: v.role_key,
|
|
||||||
payload,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
setRows(mergedRows);
|
|
||||||
} catch (e: any) {
|
|
||||||
setRows([]);
|
|
||||||
setError(e?.message ? e.message : 'Could not load verification queue.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
void load();
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabCounts = createMemo(() => {
|
|
||||||
const all = rows();
|
|
||||||
return {
|
|
||||||
all: all.length,
|
|
||||||
profile: all.filter((r) => r.requestType === 'Profile Approval').length,
|
|
||||||
portfolio: all.filter((r) => r.requestType === 'Portfolio Approval').length,
|
|
||||||
company: all.filter((r) => r.requestType === 'Company Approval').length,
|
|
||||||
jobSeeker: all.filter((r) => r.requestType === 'Job Seeker Approval').length,
|
|
||||||
serviceProfile: all.filter((r) => r.requestType === 'Service Seeker Profile Approval').length,
|
|
||||||
serviceRequirement: all.filter((r) => r.requestType === 'Service Seeker Requirement').length,
|
|
||||||
job: all.filter((r) => r.requestType === 'Job Approval').length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredRows = createMemo(() => {
|
|
||||||
const query = search().trim().toLowerCase();
|
|
||||||
const activeTab = categoryTab();
|
|
||||||
const status = statusFilter();
|
|
||||||
const sort = sortBy();
|
|
||||||
|
|
||||||
const scoped = rows().filter((row) => {
|
|
||||||
if (activeTab === 'profile' && row.requestType !== 'Profile Approval') return false;
|
|
||||||
if (activeTab === 'portfolio' && row.requestType !== 'Portfolio Approval') return false;
|
|
||||||
if (activeTab === 'company' && row.requestType !== 'Company Approval') return false;
|
|
||||||
if (activeTab === 'job_seeker' && row.requestType !== 'Job Seeker Approval') return false;
|
|
||||||
if (activeTab === 'service_profile' && row.requestType !== 'Service Seeker Profile Approval') return false;
|
|
||||||
if (activeTab === 'service_requirement' && row.requestType !== 'Service Seeker Requirement') return false;
|
|
||||||
if (activeTab === 'job' && row.requestType !== 'Job Approval') return false;
|
|
||||||
if (status !== 'ALL' && row.status !== status) return false;
|
|
||||||
|
|
||||||
if (!query) return true;
|
|
||||||
return [row.id, row.applicantName, row.requestType, row.roleLabel, row.area]
|
|
||||||
.some((value) => String(value || '').toLowerCase().includes(query));
|
|
||||||
});
|
|
||||||
|
|
||||||
const next = [...scoped];
|
|
||||||
if (sort === 'priority') {
|
|
||||||
const rank = (p: VerificationPriority) => (p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
|
|
||||||
next.sort((a, b) => rank(b.priority) - rank(a.priority));
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
next.sort((a, b) => (sort === 'oldest' ? 1 : -1) * (parseDate(a.submittedOn) - parseDate(b.submittedOn)));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayRows = createMemo(() => {
|
|
||||||
return filteredRows();
|
|
||||||
});
|
|
||||||
|
|
||||||
const metrics = createMemo(() => {
|
|
||||||
const all = rows();
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
const submittedToday = all.filter((r) => String(r.submittedOn || '').slice(0, 10) === today);
|
|
||||||
return {
|
|
||||||
totalPending: all.filter((r) => r.status === 'PENDING' || r.status === 'UNDER_REVIEW').length,
|
|
||||||
approvedToday: submittedToday.filter((r) => r.status === 'APPROVED').length,
|
|
||||||
rejectedToday: submittedToday.filter((r) => r.status === 'REJECTED').length,
|
|
||||||
needsRevision: all.filter((r) => r.status === 'DOCUMENTS_REQUESTED' || r.status === 'REVISION_REQUESTED').length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const exportCsv = () => {
|
|
||||||
const headers = ['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Area'];
|
|
||||||
const lines = filteredRows().map((row) => [
|
|
||||||
row.id,
|
|
||||||
row.requestType,
|
|
||||||
row.applicantName,
|
|
||||||
row.roleLabel,
|
|
||||||
String(row.submittedOn || '').slice(0, 10),
|
|
||||||
row.status,
|
|
||||||
row.priority,
|
|
||||||
row.area,
|
|
||||||
]);
|
|
||||||
const csv = [headers, ...lines]
|
|
||||||
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
|
||||||
.join('\n');
|
|
||||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `verification-queue-${new Date().toISOString().slice(0, 10)}.csv`;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectedDocuments = createMemo<SubmittedDocument[]>(() => {
|
|
||||||
const row = selectedRow();
|
|
||||||
if (!row) return [];
|
|
||||||
if (row.requestType === 'Job Approval') {
|
|
||||||
return [
|
|
||||||
{ id: 'job_desc', title: 'Job Description File', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
|
|
||||||
{ id: 'company_proof', title: 'Company Verification Snapshot', type: 'PDF', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (row.requestType === 'Service Seeker Requirement') {
|
|
||||||
return [
|
|
||||||
{ id: 'requirement_brief', title: 'Requirement Brief', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
|
|
||||||
{ id: 'reference', title: 'Reference Attachment', type: 'IMAGE', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
|
|
||||||
const fromPayload = Array.isArray(row.payload?.documents) ? row.payload.documents : [];
|
|
||||||
if (fromPayload.length) {
|
|
||||||
return fromPayload.slice(0, 8).map((doc: any, idx: number) => ({
|
|
||||||
id: String(doc.id || `doc-${idx + 1}`),
|
|
||||||
title: String(doc.title || doc.name || `Document ${idx + 1}`),
|
|
||||||
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE',
|
|
||||||
url: String(doc.url || '/nxtgauge-logo.png'),
|
|
||||||
status: String(doc.status || '').toUpperCase() === 'MISSING'
|
|
||||||
? 'MISSING'
|
|
||||||
: String(doc.status || '').toUpperCase() === 'INVALID'
|
|
||||||
? 'INVALID'
|
|
||||||
: 'SUBMITTED',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
const docs = ROLE_DOCUMENTS[roleSpecKey] || ROLE_DOCUMENTS.PROFESSIONAL;
|
|
||||||
return docs.map((title, idx) => ({
|
|
||||||
id: `${roleSpecKey.toLowerCase()}-doc-${idx + 1}`,
|
|
||||||
title,
|
|
||||||
type: title.toLowerCase().includes('proof') ? 'IMAGE' : 'PDF',
|
|
||||||
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
|
|
||||||
status: idx === docs.length - 1 ? 'MISSING' : 'SUBMITTED',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedPortfolio = createMemo<PortfolioAsset[]>(() => {
|
|
||||||
const row = selectedRow();
|
|
||||||
if (!row || !(row.userType === 'PROFESSIONAL' || row.requestType === 'Portfolio Approval')) return [];
|
|
||||||
const fromPayload = Array.isArray(row.payload?.portfolio_images)
|
|
||||||
? row.payload.portfolio_images
|
|
||||||
: Array.isArray(row.payload?.images)
|
|
||||||
? row.payload.images
|
|
||||||
: Array.isArray(row.payload?.gallery)
|
|
||||||
? row.payload.gallery
|
|
||||||
: [];
|
|
||||||
if (fromPayload.length) {
|
|
||||||
return fromPayload.slice(0, 6).map((asset: any, idx: number) => ({
|
|
||||||
id: String(asset.id || `pf-${idx + 1}`),
|
|
||||||
title: String(asset.title || asset.name || `Portfolio ${idx + 1}`),
|
|
||||||
url: String(asset.url || '/nxtgauge-logo.png'),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return Array.from({ length: 6 }).map((_, idx) => ({
|
|
||||||
id: `pf-${idx + 1}`,
|
|
||||||
title: `Portfolio ${idx + 1}`,
|
|
||||||
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedFieldValues = createMemo<Array<{ label: string; value: string }>>(() => {
|
|
||||||
const row = selectedRow();
|
|
||||||
if (!row) return [];
|
|
||||||
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
|
|
||||||
const payload = row.payload || {};
|
|
||||||
|
|
||||||
const fullName = String(
|
|
||||||
payload.full_name
|
|
||||||
|| payload.fullName
|
|
||||||
|| [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ')
|
|
||||||
|| row.applicantName
|
|
||||||
|| '',
|
|
||||||
).trim();
|
|
||||||
const email = String(payload.email || payload.email_address || payload.emailAddress || 'applicant@nxtgauge.com');
|
|
||||||
const phone = String(payload.phone || payload.mobile || payload.mobile_number || payload.mobileNumber || '+91 90000 00000');
|
|
||||||
const area = String(payload.area || row.area || 'T. Nagar');
|
|
||||||
const place = String(payload.place || payload.locality || 'Chennai');
|
|
||||||
const city = String(payload.city || 'Chennai');
|
|
||||||
const state = String(payload.state || 'Tamil Nadu');
|
|
||||||
const pin = String(payload.pin_code || payload.pinCode || '600001');
|
|
||||||
const gender = String(payload.gender || 'Not specified');
|
|
||||||
|
|
||||||
const byLabel: Record<string, string> = {
|
|
||||||
'First Name': fullName.split(' ')[0] || fullName,
|
|
||||||
'Last Name': fullName.split(' ').slice(1).join(' ') || '—',
|
|
||||||
'Full Name': fullName || '—',
|
|
||||||
'Email Address': email,
|
|
||||||
'Mobile Number': phone,
|
|
||||||
Area: area,
|
|
||||||
Place: place,
|
|
||||||
City: city,
|
|
||||||
State: state,
|
|
||||||
'PIN Code': pin,
|
|
||||||
Gender: gender,
|
|
||||||
'Address Line 1': String(payload.address_line1 || payload.addressLine1 || payload.address || 'No. 12, Main Road'),
|
|
||||||
'Address Line 2 (Optional)': String(payload.address_line2 || payload.addressLine2 || '—'),
|
|
||||||
'Service Category': String(payload.service_category || payload.serviceCategory || row.roleLabel || 'General'),
|
|
||||||
'Company Name': String(payload.company_name || payload.companyName || row.applicantName || '—'),
|
|
||||||
'Company Email': email,
|
|
||||||
'Company Phone': phone,
|
|
||||||
'Website URL': String(payload.website || payload.website_url || payload.websiteUrl || '—'),
|
|
||||||
'Contact Person Name': String(payload.contact_person_name || payload.contactPersonName || fullName || '—'),
|
|
||||||
'Current Role': String(payload.current_role || payload.currentRole || row.roleLabel || '—'),
|
|
||||||
'Total Experience': String(payload.total_experience || payload.totalExperience || payload.experience || '—'),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (row.requestType === 'Job Approval') {
|
|
||||||
return JOB_POSTING_FIELDS.map((label) => ({
|
|
||||||
label,
|
|
||||||
value: ({
|
|
||||||
'Job Title': String(payload.title || payload.job_title || '—'),
|
|
||||||
Department: String(payload.department || payload.company_department || '—'),
|
|
||||||
'Job Category': String(payload.category || payload.job_category || '—'),
|
|
||||||
'Employment Type': String(payload.employment_type || payload.type || '—'),
|
|
||||||
Seniority: String(payload.seniority || payload.level || '—'),
|
|
||||||
Openings: String(payload.openings || payload.positions || '—'),
|
|
||||||
'Role & Requirements': String(payload.requirements || payload.skills || '—'),
|
|
||||||
Compensation: String(payload.salary_range || payload.compensation || '—'),
|
|
||||||
Location: String(payload.location || payload.city || 'Chennai'),
|
|
||||||
Description: String(payload.description || payload.summary || '—'),
|
|
||||||
} as Record<string, string>)[label] || '—',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.requestType === 'Service Seeker Requirement') {
|
|
||||||
const reqRoleKey = normalizeRoleSpecKey(String(payload.role_key || payload.profession || payload.category || row.roleKey));
|
|
||||||
const dynamicReqFields = REQUIREMENT_ROLE_FIELDS[reqRoleKey] || REQUIREMENT_ROLE_FIELDS.PHOTOGRAPHER;
|
|
||||||
const baseReqFields = ['Requirement Title', 'Priority', 'Requirement Description', 'Expected Start Date', 'Service City', 'Contact Number'];
|
|
||||||
const fields = [...baseReqFields, ...dynamicReqFields];
|
|
||||||
return fields.map((label) => ({
|
|
||||||
label,
|
|
||||||
value: ({
|
|
||||||
'Requirement Title': String(payload.title || payload.requirement_title || '—'),
|
|
||||||
Priority: String(payload.priority || '—'),
|
|
||||||
'Requirement Description': String(payload.description || payload.details || '—'),
|
|
||||||
'Expected Start Date': String(payload.start_date || payload.expected_start_date || '—'),
|
|
||||||
'Service City': String(payload.city || 'Chennai'),
|
|
||||||
'Contact Number': String(payload.phone || payload.contact_number || '+91 90000 00000'),
|
|
||||||
} as Record<string, string>)[label]
|
|
||||||
|| String(
|
|
||||||
payload[label]
|
|
||||||
|| payload[label.toLowerCase().replace(/[^a-z0-9]+/g, '_')]
|
|
||||||
|| '—',
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.requestType === 'Portfolio Approval') {
|
|
||||||
const portfolioFields = [
|
|
||||||
'About',
|
|
||||||
'Services & Pricing',
|
|
||||||
'Portfolio Photos',
|
|
||||||
'Experience & Tools',
|
|
||||||
'Specialties',
|
|
||||||
'Languages',
|
|
||||||
'Service Areas',
|
|
||||||
];
|
|
||||||
return portfolioFields.map((label) => ({
|
|
||||||
label,
|
|
||||||
value: ({
|
|
||||||
About: String(payload.about || payload.bio || '—'),
|
|
||||||
'Services & Pricing': Array.isArray(payload.services) ? `${payload.services.length} service(s) added` : String(payload.pricing || '—'),
|
|
||||||
'Portfolio Photos': Array.isArray(payload.images || payload.portfolio_images || payload.gallery) ? `${(payload.images || payload.portfolio_images || payload.gallery).length} image(s)` : '0 image(s)',
|
|
||||||
'Experience & Tools': String(payload.experience || payload.tools || '—'),
|
|
||||||
Specialties: Array.isArray(payload.specialties) ? payload.specialties.join(', ') : String(payload.specialties || '—'),
|
|
||||||
Languages: Array.isArray(payload.languages) ? payload.languages.join(', ') : String(payload.languages || '—'),
|
|
||||||
'Service Areas': Array.isArray(payload.service_areas || payload.travel_areas) ? (payload.service_areas || payload.travel_areas).join(', ') : String(payload.service_areas || '—'),
|
|
||||||
} as Record<string, string>)[label] || '—',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = ROLE_PROFILE_FIELDS[roleSpecKey] || ROLE_PROFILE_FIELDS.PROFESSIONAL;
|
|
||||||
return fields.map((label) => ({ label, value: byLabel[label] || '—' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
const pushToApprovalQueue = (row: VerificationRow) => {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
const item: ApprovalQueueItem = {
|
|
||||||
id: row.id,
|
|
||||||
requestType: row.requestType,
|
|
||||||
applicantName: row.applicantName,
|
|
||||||
roleLabel: row.roleLabel,
|
|
||||||
userType: row.userType,
|
|
||||||
roleKey: row.roleKey,
|
|
||||||
area: row.area,
|
|
||||||
submittedOn: row.submittedOn,
|
|
||||||
documents: selectedDocuments(),
|
|
||||||
submittedFields: selectedFieldValues(),
|
|
||||||
};
|
|
||||||
const raw = window.localStorage.getItem(APPROVAL_QUEUE_STORAGE_KEY);
|
|
||||||
const parsed = raw ? JSON.parse(raw) : [];
|
|
||||||
const current = Array.isArray(parsed) ? parsed as ApprovalQueueItem[] : [];
|
|
||||||
const filtered = current.filter((entry) => entry.id !== item.id);
|
|
||||||
window.localStorage.setItem(APPROVAL_QUEUE_STORAGE_KEY, JSON.stringify([item, ...filtered]));
|
|
||||||
};
|
|
||||||
|
|
||||||
const applySelectedStatus = async (nextStatus: VerificationStatus) => {
|
|
||||||
const current = selectedRow();
|
|
||||||
if (!current) return;
|
|
||||||
|
|
||||||
const isApprove = nextStatus === 'APPROVED';
|
|
||||||
const isReject = nextStatus === 'REJECTED';
|
|
||||||
|
|
||||||
if (!isApprove && !isReject) {
|
|
||||||
// local update only for intermediate states if needed, but usually we skip backend call here
|
|
||||||
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
|
|
||||||
setSelectedRow({ ...current, status: nextStatus });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const accessToken = typeof sessionStorage !== 'undefined'
|
|
||||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
|
||||||
: '';
|
|
||||||
const common = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Accept: 'application/json',
|
|
||||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
|
||||||
},
|
|
||||||
credentials: 'include' as const,
|
|
||||||
body: isReject ? JSON.stringify({ reason: requestNote() }) : undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const endpoint = `/api/admin/verifications/${current.id}/${isApprove ? 'approve' : 'reject'}`;
|
|
||||||
|
|
||||||
const res = await fetch(`${API}${endpoint}`, common);
|
|
||||||
if (!res.ok) {
|
|
||||||
const txt = await res.text();
|
|
||||||
throw new Error(`Failed to update status (${res.status}): ${txt}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
|
|
||||||
setSelectedRow({ ...current, status: nextStatus });
|
|
||||||
|
|
||||||
if (isApprove) {
|
|
||||||
pushToApprovalQueue({ ...current, status: nextStatus });
|
|
||||||
setActionMessage('Successfully verified and sent to Approval Management.');
|
|
||||||
} else {
|
|
||||||
setActionMessage('Successfully rejected submission.');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(e.message || 'Failed to update backend status');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestSelectedDocuments = () => {
|
|
||||||
const selectedIds = Object.entries(docSelection()).filter(([, checked]) => checked).map(([id]) => id);
|
|
||||||
if (selectedIds.length === 0) {
|
|
||||||
setActionMessage('Select at least one document before sending request.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applySelectedStatus('DOCUMENTS_REQUESTED');
|
|
||||||
setActionMessage(`Document request sent for ${selectedIds.length} item(s).`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestProfileChanges = () => {
|
|
||||||
applySelectedStatus('REVISION_REQUESTED');
|
|
||||||
setActionMessage('Revision request sent to applicant.');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="w-full space-y-6 pb-8">
|
|
||||||
<div style="margin-bottom:1.5rem">
|
|
||||||
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Verification Management</h1>
|
|
||||||
<p class="mt-1 text-[14px] text-[#6B7280]">Review and verify all platform submissions before they move to approval management</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={error()}>
|
|
||||||
<div style="border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">{error()}</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
|
|
||||||
{([
|
|
||||||
{ key: 'all', label: 'All Verifications' },
|
|
||||||
{ key: 'view', label: 'View Verification' },
|
|
||||||
] as const).map((tab) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setListTab(tab.key)}
|
|
||||||
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${listTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={listTab() === 'all'}>
|
|
||||||
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
|
|
||||||
{([
|
|
||||||
{ key: 'all', label: `All Verifications (${tabCounts().all})` },
|
|
||||||
{ key: 'profile', label: `Profile Approvals (${tabCounts().profile})` },
|
|
||||||
{ key: 'portfolio', label: `Portfolio Approvals (${tabCounts().portfolio})` },
|
|
||||||
{ key: 'company', label: `Company Approvals (${tabCounts().company})` },
|
|
||||||
{ key: 'job_seeker', label: `Job Seeker Approvals (${tabCounts().jobSeeker})` },
|
|
||||||
{ key: 'service_profile', label: `Service Seeker Profile (${tabCounts().serviceProfile})` },
|
|
||||||
{ key: 'service_requirement', label: `Service Seeker Requirements (${tabCounts().serviceRequirement})` },
|
|
||||||
{ key: 'job', label: `Job Approvals (${tabCounts().job})` },
|
|
||||||
] as const).map((tab) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCategoryTab(tab.key)}
|
|
||||||
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${categoryTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={listTab() === 'all'}>
|
|
||||||
<div style="position:relative;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
|
||||||
<input
|
|
||||||
value={search()}
|
|
||||||
onInput={(e) => setSearch(e.currentTarget.value)}
|
|
||||||
placeholder="Search by ID, name or type..."
|
|
||||||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style="position:relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSortOpen((v) => !v); setFilterOpen(false); }}
|
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
|
||||||
Sort
|
|
||||||
</button>
|
|
||||||
<Show when={sortOpen()}>
|
|
||||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
||||||
{([
|
|
||||||
{ key: 'latest', label: 'Latest Submitted' },
|
|
||||||
{ key: 'oldest', label: 'Oldest Submitted' },
|
|
||||||
{ key: 'priority', label: 'Priority (High-Low)' },
|
|
||||||
] as const).map((item) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSortBy(item.key); setSortOpen(false); }}
|
|
||||||
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="position:relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setFilterOpen((v) => !v); setSortOpen(false); }}
|
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
|
||||||
Filters
|
|
||||||
</button>
|
|
||||||
<Show when={filterOpen()}>
|
|
||||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
|
||||||
{([
|
|
||||||
{ key: 'ALL', label: 'All Status' },
|
|
||||||
{ key: 'PENDING', label: 'Pending' },
|
|
||||||
{ key: 'UNDER_REVIEW', label: 'Under Review' },
|
|
||||||
{ key: 'DOCUMENTS_REQUESTED', label: 'Documents Requested' },
|
|
||||||
{ key: 'REVISION_REQUESTED', label: 'Revision Requested' },
|
|
||||||
{ key: 'APPROVED', label: 'Approved' },
|
|
||||||
{ key: 'REJECTED', label: 'Rejected' },
|
|
||||||
] as const).map((item) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setStatusFilter(item.key); setFilterOpen(false); }}
|
|
||||||
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={exportCsv}
|
|
||||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer"
|
|
||||||
>
|
|
||||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto overflow-y-visible">
|
|
||||||
<table class="min-w-full">
|
|
||||||
<thead>
|
|
||||||
<tr style="background:#0D0D2A;text-align:left">
|
|
||||||
{['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Actions'].map((header) => (
|
|
||||||
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<Show
|
|
||||||
when={displayRows().length > 0}
|
|
||||||
fallback={
|
|
||||||
<tr>
|
|
||||||
<td colSpan={8} style="padding:32px;text-align:center">
|
|
||||||
<p style="font-size:15px;font-weight:600;color:#111827">No verification requests found</p>
|
|
||||||
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<For each={displayRows()}>
|
|
||||||
{(row) => {
|
|
||||||
const s = statusUi(row.status);
|
|
||||||
const p = priorityUi(row.priority);
|
|
||||||
return (
|
|
||||||
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
|
||||||
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">#{row.id}</td>
|
|
||||||
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.requestType}</td>
|
|
||||||
<td style="padding:12px 20px">
|
|
||||||
<div style="display:flex;flex-direction:column;gap:2px">
|
|
||||||
<span style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</span>
|
|
||||||
<span style="font-size:12px;color:#6B7280">{row.area || 'Chennai'}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.roleLabel}</td>
|
|
||||||
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{String(row.submittedOn || '').slice(0, 10) || '—'}</td>
|
|
||||||
<td style="padding:12px 20px">
|
|
||||||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${s.border};background:${s.bg};color:${s.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
|
||||||
{s.label}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 20px">
|
|
||||||
<span style={`display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:${p.color}`}>
|
|
||||||
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${p.color}`} />
|
|
||||||
{p.label}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 20px">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRow(row);
|
|
||||||
setDocSelection({});
|
|
||||||
setRequestNote('');
|
|
||||||
setActionMessage('');
|
|
||||||
setListTab('view');
|
|
||||||
}}
|
|
||||||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;display:inline-flex;align-items:center;cursor:pointer"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</Show>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={displayRows().length > 0}>
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
|
||||||
<p style="font-size:13px;color:#6B7280">
|
|
||||||
Showing <strong style="font-weight:600;color:#111827">1–{displayRows().length}</strong> of <strong style="font-weight:600;color:#111827">{displayRows().length}</strong> verifications
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;align-items:center;gap:4px">
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
|
||||||
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={listTab() === 'view'}>
|
|
||||||
<Show
|
|
||||||
when={selectedRow()}
|
|
||||||
fallback={
|
|
||||||
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
|
||||||
<p style="font-size:15px;font-weight:600;color:#111827">No verification selected</p>
|
|
||||||
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click <strong>View</strong> on a row from <strong>All Verifications</strong>.</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style="margin-top:24px;display:grid;gap:12px">
|
|
||||||
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
||||||
<div>
|
|
||||||
<h2 style="font-size:18px;font-weight:700;color:#111827">#{selectedRow()!.id}</h2>
|
|
||||||
<p style="margin-top:2px;font-size:13px;color:#6B7280">{selectedRow()!.requestType}</p>
|
|
||||||
</div>
|
|
||||||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${statusUi(selectedRow()!.status).border};background:${statusUi(selectedRow()!.status).bg};color:${statusUi(selectedRow()!.status).text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
|
||||||
{statusUi(selectedRow()!.status).label}
|
|
||||||
</span>
|
|
||||||
</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">Applicant Name</p>
|
|
||||||
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.applicantName}</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">Role</p>
|
|
||||||
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.roleLabel}</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">Area</p>
|
|
||||||
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.area || 'Chennai'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
|
|
||||||
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Form Details</h3>
|
|
||||||
<span style="font-size:12px;color:#6B7280">{selectedFieldValues().length} fields</span>
|
|
||||||
</div>
|
|
||||||
<div style="padding:14px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
|
||||||
<For each={selectedFieldValues()}>
|
|
||||||
{(entry) => (
|
|
||||||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
|
||||||
<p style="margin:0;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">{entry.label}</p>
|
|
||||||
<p style="margin:6px 0 0;font-size:13px;font-weight:700;color:#111827;line-height:1.35">{entry.value}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
|
|
||||||
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Documents</h3>
|
|
||||||
<span style="font-size:12px;color:#6B7280">{selectedDocuments().length} files</span>
|
|
||||||
</div>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="min-w-full">
|
|
||||||
<thead>
|
|
||||||
<tr style="background:#0D0D2A;text-align:left">
|
|
||||||
{['Document', 'State', 'View', 'Request'].map((header) => (
|
|
||||||
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<For each={selectedDocuments()}>
|
|
||||||
{(doc) => (
|
|
||||||
<tr style="border-bottom:1px solid #F3F4F6">
|
|
||||||
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827">{doc.title}</td>
|
|
||||||
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{toTitle(doc.status)}</td>
|
|
||||||
<td style="padding:12px 16px">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setViewer({ open: true, title: doc.title, type: doc.type, url: doc.url })}
|
|
||||||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td style="padding:12px 16px">
|
|
||||||
<label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;color:#374151;cursor:pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={Boolean(docSelection()[doc.id])}
|
|
||||||
onChange={(e) => setDocSelection((prev) => ({ ...prev, [doc.id]: e.currentTarget.checked }))}
|
|
||||||
style="accent-color:#FF5E13"
|
|
||||||
/>
|
|
||||||
Request
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
|
||||||
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between;background:#FFFBF0">
|
|
||||||
<h3 style="margin:0;font-size:16px;font-weight:700;color:#854D0E">Verification Requirements Checklist</h3>
|
|
||||||
<span style="font-size:12px;font-weight:600;color:#A16207">System Audit</span>
|
|
||||||
</div>
|
|
||||||
<div style="padding:20px;display:grid;gap:12px">
|
|
||||||
<For each={selectedDocuments()}>
|
|
||||||
{(doc) => (
|
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:8px;background:#F9FAFB;border:1px solid #E5E7EB">
|
|
||||||
<div style="display:flex;align-items:center;gap:10px">
|
|
||||||
<div style={`width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;${doc.status === 'SUBMITTED' ? 'background:#22C55E' : 'background:#EF4444'}`}>
|
|
||||||
<Show when={doc.status === 'SUBMITTED'} fallback={<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>}>
|
|
||||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<span style="font-size:13px;font-weight:600;color:#374151">{doc.title}</span>
|
|
||||||
</div>
|
|
||||||
<span style={`font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;${doc.status === 'SUBMITTED' ? 'background:#DCFCE7;color:#166534' : 'background:#FEE2E2;color:#991B1B'}`}>
|
|
||||||
{doc.status === 'SUBMITTED' ? 'RECEIVED' : 'MISSING'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
|
|
||||||
<div style="margin-top:8px;padding-top:16px;border-top:1px dashed #E5E7EB">
|
|
||||||
<p style="font-size:12px;font-weight:700;color:#6B7280;margin-bottom:8px;text-transform:uppercase">Profile Completeness</p>
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
||||||
<For each={selectedFieldValues().slice(0, 6)}>
|
|
||||||
{(field) => (
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;font-size:12px;color:#4B5563">
|
|
||||||
<div style={`width:6px;height:6px;border-radius:50%;${field.value && field.value !== '—' ? 'background:#22C55E' : 'background:#EF4444'}`} />
|
|
||||||
<span style="opacity:0.8">{field.label}:</span>
|
|
||||||
<span style="font-weight:600">{field.value && field.value !== '—' ? 'Filled' : 'Empty'}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);padding:16px">
|
|
||||||
<h3 style="margin:0 0 8px;font-size:16px;font-weight:700;color:#111827">Reviewer Actions</h3>
|
|
||||||
<textarea
|
|
||||||
value={requestNote()}
|
|
||||||
onInput={(e) => setRequestNote(e.currentTarget.value)}
|
|
||||||
placeholder="Reviewer note for applicant"
|
|
||||||
style="width:100%;min-height:74px;border:1px solid #E5E7EB;border-radius:10px;padding:10px;font-size:13px;color:#374151;resize:vertical"
|
|
||||||
/>
|
|
||||||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
|
||||||
<button type="button" onClick={requestSelectedDocuments} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Selected Documents</button>
|
|
||||||
<button type="button" onClick={requestProfileChanges} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Changes</button>
|
|
||||||
<button type="button" onClick={() => applySelectedStatus('APPROVED')} style="height:34px;border-radius:8px;border:none;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:700;color:white;cursor:pointer">Approve</button>
|
|
||||||
<button type="button" onClick={() => applySelectedStatus('REJECTED')} style="height:34px;border-radius:8px;border:1px solid #FECACA;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#B91C1C;cursor:pointer">Reject</button>
|
|
||||||
</div>
|
|
||||||
<Show when={actionMessage()}>
|
|
||||||
<div style="margin-top:10px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;padding:8px 10px;font-size:12px;color:#374151">{actionMessage()}</div>
|
|
||||||
</Show>
|
|
||||||
<Show when={selectedRow()!.status === 'APPROVED'}>
|
|
||||||
<A href="/admin/approval" style="margin-top:8px;height:32px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:700;display:inline-flex;align-items:center;text-decoration:none">
|
|
||||||
Open Approval Management
|
|
||||||
</A>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:10px">
|
|
||||||
<A
|
|
||||||
href={`/admin/verification/${encodeURIComponent(selectedRow()!.id)}?roleKey=${encodeURIComponent(selectedRow()!.roleKey || '')}&userId=${encodeURIComponent(selectedRow()!.userId || '')}&type=${encodeURIComponent(selectedRow()!.requestType)}`}
|
|
||||||
style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;display:inline-flex;align-items:center;text-decoration:none"
|
|
||||||
>
|
|
||||||
Open Full Review
|
|
||||||
</A>
|
|
||||||
<button type="button" onClick={() => setListTab('all')} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={viewer().open}>
|
|
||||||
<div style="position:fixed;inset:0;z-index:9999;background:rgba(17,24,39,0.72);display:flex;align-items:center;justify-content:center;padding:20px">
|
|
||||||
<div style="width:min(90vw,900px);max-height:88vh;border-radius:14px;overflow:hidden;border:1px solid #E5E7EB;background:white;display:flex;flex-direction:column">
|
|
||||||
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
|
||||||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{viewer().title}</p>
|
|
||||||
<button type="button" onClick={() => setViewer({ open: false, title: '', type: 'IMAGE', url: '' })} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Close</button>
|
|
||||||
</div>
|
|
||||||
<div style="padding:12px;overflow:auto;display:flex;justify-content:center;align-items:center;background:#F8FAFC">
|
|
||||||
<Show
|
|
||||||
when={viewer().type === 'IMAGE'}
|
|
||||||
fallback={<iframe src={viewer().url} style="width:100%;height:70vh;border:none;background:white" title={viewer().title} />}
|
|
||||||
>
|
|
||||||
<img src={viewer().url} alt={viewer().title} style="max-width:100%;max-height:70vh;object-fit:contain" />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { A, useSearchParams, useParams } from '@solidjs/router';
|
import { A, useSearchParams, useParams } from '@solidjs/router';
|
||||||
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
|
|
||||||
const API = '/api/gateway';
|
const API = '';
|
||||||
async function adminFetch(path: string, opts?: RequestInit) {
|
async function adminFetch(path: string, opts?: RequestInit) {
|
||||||
return fetch(`${API}${path}`, {
|
return fetch(`${API}${path}`, {
|
||||||
...opts,
|
...opts,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,988 @@
|
||||||
export { default } from '../verification';
|
import { A } from '@solidjs/router';
|
||||||
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
|
||||||
|
|
||||||
|
type VerificationStatus = 'PENDING' | 'UNDER_REVIEW' | 'DOCUMENTS_REQUESTED' | 'REVISION_REQUESTED' | 'APPROVED' | 'REJECTED';
|
||||||
|
type VerificationPriority = 'HIGH' | 'MEDIUM' | 'LOW';
|
||||||
|
|
||||||
|
type VerificationRow = {
|
||||||
|
id: string;
|
||||||
|
applicantName: string;
|
||||||
|
requestType:
|
||||||
|
| 'Profile Approval'
|
||||||
|
| 'Portfolio Approval'
|
||||||
|
| 'Company Approval'
|
||||||
|
| 'Job Seeker Approval'
|
||||||
|
| 'Service Seeker Profile Approval'
|
||||||
|
| 'Service Seeker Requirement'
|
||||||
|
| 'Job Approval';
|
||||||
|
roleLabel: string;
|
||||||
|
submittedOn: string;
|
||||||
|
status: VerificationStatus;
|
||||||
|
priority: VerificationPriority;
|
||||||
|
userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER';
|
||||||
|
area: string;
|
||||||
|
userId: string;
|
||||||
|
roleKey: string;
|
||||||
|
payload?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SubmittedDocument = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
type: 'IMAGE' | 'PDF';
|
||||||
|
url: string;
|
||||||
|
status: 'SUBMITTED' | 'MISSING' | 'INVALID';
|
||||||
|
};
|
||||||
|
|
||||||
|
type PortfolioAsset = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApprovalQueueItem = {
|
||||||
|
id: string;
|
||||||
|
requestType: VerificationRow['requestType'];
|
||||||
|
applicantName: string;
|
||||||
|
roleLabel: string;
|
||||||
|
userType: VerificationRow['userType'];
|
||||||
|
roleKey: string;
|
||||||
|
area: string;
|
||||||
|
submittedOn: string;
|
||||||
|
documents: SubmittedDocument[];
|
||||||
|
submittedFields: Array<{ label: string; value: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_PROFILE_FIELDS: Record<string, string[]> = {
|
||||||
|
CUSTOMER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Area', 'Place', 'PIN Code', 'Service Category'],
|
||||||
|
COMPANY: ['Company Name', 'Company Email', 'Company Phone', 'City', 'Area', 'Place', 'PIN Code', 'Website URL'],
|
||||||
|
JOB_SEEKER: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Current Role', 'Total Experience', 'City', 'Area', 'Place'],
|
||||||
|
PROFESSIONAL: ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_DOCUMENTS: Record<string, string[]> = {
|
||||||
|
CUSTOMER: ['Identity Proof', 'Address Proof'],
|
||||||
|
COMPANY: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'],
|
||||||
|
JOB_SEEKER: ['Identity Proof', 'Address Proof', 'Education Proof'],
|
||||||
|
PHOTOGRAPHER: ['Identity Proof', 'Address Proof', 'Portfolio Ownership Proof'],
|
||||||
|
MAKEUP_ARTIST: ['Identity Proof', 'Address Proof', 'Professional Certifications'],
|
||||||
|
DEVELOPER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
VIDEO_EDITOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
UGC_CONTENT_CREATOR: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
GRAPHIC_DESIGNER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
SOCIAL_MEDIA_MANAGER: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
FITNESS_TRAINER: ['Identity Proof', 'Address Proof', 'Certification Proof'],
|
||||||
|
TUTOR: ['Identity Proof', 'Address Proof', 'Educational Proof'],
|
||||||
|
CATERING_SERVICES: ['Identity Proof', 'Address Proof', 'Food License'],
|
||||||
|
PROFESSIONAL: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const REQUIREMENT_ROLE_FIELDS: Record<string, string[]> = {
|
||||||
|
PHOTOGRAPHER: ['Event Type', 'Shoot Type', 'Event Date & Time', 'Event Duration (Hours)', 'Venue / Location', 'Number of People', 'Delivery Deadline'],
|
||||||
|
MAKEUP_ARTIST: ['Event Type', 'Makeup Category', 'Event Date & Time', 'Artists Required', 'Venue / Location', 'Skin Tone Preference', 'Ready By Time'],
|
||||||
|
TUTOR: ['Subject', 'Class / Grade', 'Mode (Online / Offline)', 'Sessions Per Week', 'Preferred Start Date', 'Student Location', 'Exam Goal'],
|
||||||
|
DEVELOPER: ['Project Type', 'Platform', 'Preferred Stack', 'Project Duration', 'Launch Deadline', 'Team Size Needed', 'Support Duration'],
|
||||||
|
VIDEO_EDITOR: ['Video Category', 'Final Duration', 'Footage Volume', 'Delivery Date', 'Editing Style', 'Platform', 'Revision Rounds'],
|
||||||
|
UGC_CONTENT_CREATOR: ['Campaign Goal', 'Platform', 'Deliverables Needed', 'Brand Category', 'Delivery Deadline', 'Target Audience', 'Usage Rights Duration'],
|
||||||
|
FITNESS_TRAINER: ['Primary Goal', 'Current Activity Level', 'Preferred Mode', 'Training Days Per Week', 'Preferred Timings', 'Health Conditions', 'Goal Timeline'],
|
||||||
|
CATERING_SERVICES: ['Event Type', 'Cuisine Preference', 'Guest Count', 'Service Date', 'Venue / Location', 'Meal Slot', 'Serving Style'],
|
||||||
|
GRAPHIC_DESIGNER: ['Project Type', 'Brand Industry', 'Deliverables Needed', 'Deadline', 'Target Audience', 'Reference Links', 'Output Formats'],
|
||||||
|
SOCIAL_MEDIA_MANAGER: ['Primary Goal', 'Platforms', 'Posting Frequency', 'Campaign Duration', 'Start Date', 'Brand Category', 'Monthly Budget'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const JOB_POSTING_FIELDS = [
|
||||||
|
'Job Title',
|
||||||
|
'Department',
|
||||||
|
'Job Category',
|
||||||
|
'Employment Type',
|
||||||
|
'Seniority',
|
||||||
|
'Openings',
|
||||||
|
'Role & Requirements',
|
||||||
|
'Compensation',
|
||||||
|
'Location',
|
||||||
|
'Description',
|
||||||
|
];
|
||||||
|
|
||||||
|
const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue';
|
||||||
|
|
||||||
|
const API = '';
|
||||||
|
|
||||||
|
const toTitle = (value: string) => String(value || '').replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
|
|
||||||
|
const statusUi = (status: VerificationStatus) => {
|
||||||
|
if (status === 'APPROVED') return { bg: '#ECFDF3', border: '#BBF7D0', text: '#166534', label: 'Approved' };
|
||||||
|
if (status === 'UNDER_REVIEW') return { bg: '#EEF2FF', border: '#C7D2FE', text: '#3730A3', label: 'Under Review' };
|
||||||
|
if (status === 'DOCUMENTS_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Document Requested' };
|
||||||
|
if (status === 'REVISION_REQUESTED') return { bg: '#FFF7ED', border: '#FED7AA', text: '#C2410C', label: 'Revision Requested' };
|
||||||
|
if (status === 'REJECTED') return { bg: '#FEF2F2', border: '#FECACA', text: '#B91C1C', label: 'Rejected' };
|
||||||
|
return { bg: '#FFFBEB', border: '#FDE68A', text: '#92400E', label: 'Pending' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const priorityUi = (priority: VerificationPriority) => {
|
||||||
|
if (priority === 'HIGH') return { color: '#DC2626', label: 'High' };
|
||||||
|
if (priority === 'MEDIUM') return { color: '#D97706', label: 'Medium' };
|
||||||
|
return { color: '#64748B', label: 'Low' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseDate = (value: string) => {
|
||||||
|
const ts = Date.parse(String(value || ''));
|
||||||
|
return Number.isNaN(ts) ? 0 : ts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRoleSpecKey = (value: string) => {
|
||||||
|
const key = String(value || '').toUpperCase();
|
||||||
|
if (key.includes('COMPANY')) return 'COMPANY';
|
||||||
|
if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
|
||||||
|
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOB_SEEKER';
|
||||||
|
if (key.includes('PHOTOGRAPHER') || key.includes('PHOTO')) return 'PHOTOGRAPHER';
|
||||||
|
if (key.includes('MAKEUP')) return 'MAKEUP_ARTIST';
|
||||||
|
if (key.includes('DEVELOPER')) return 'DEVELOPER';
|
||||||
|
if (key.includes('VIDEO')) return 'VIDEO_EDITOR';
|
||||||
|
if (key.includes('UGC') || (key.includes('CONTENT') && key.includes('CREATOR'))) return 'UGC_CONTENT_CREATOR';
|
||||||
|
if (key.includes('GRAPHIC')) return 'GRAPHIC_DESIGNER';
|
||||||
|
if (key.includes('SOCIAL')) return 'SOCIAL_MEDIA_MANAGER';
|
||||||
|
if (key.includes('FITNESS')) return 'FITNESS_TRAINER';
|
||||||
|
if (key.includes('TUTOR')) return 'TUTOR';
|
||||||
|
if (key.includes('CATER')) return 'CATERING_SERVICES';
|
||||||
|
return 'PROFESSIONAL';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function VerificationManagementPage() {
|
||||||
|
const [rows, setRows] = createSignal<VerificationRow[]>([]);
|
||||||
|
const [search, setSearch] = createSignal('');
|
||||||
|
const [statusFilter, setStatusFilter] = createSignal<'ALL' | VerificationStatus>('ALL');
|
||||||
|
const [sortBy, setSortBy] = createSignal<'latest' | 'oldest' | 'priority'>('latest');
|
||||||
|
const [sortOpen, setSortOpen] = createSignal(false);
|
||||||
|
const [filterOpen, setFilterOpen] = createSignal(false);
|
||||||
|
const [error, setError] = createSignal('');
|
||||||
|
|
||||||
|
const [categoryTab, setCategoryTab] = createSignal<'all' | 'profile' | 'portfolio' | 'company' | 'job_seeker' | 'service_profile' | 'service_requirement' | 'job'>('all');
|
||||||
|
const [listTab, setListTab] = createSignal<'all' | 'view'>('all');
|
||||||
|
const [selectedRow, setSelectedRow] = createSignal<VerificationRow | null>(null);
|
||||||
|
const [viewer, setViewer] = createSignal<{ open: boolean; title: string; type: 'IMAGE' | 'PDF'; url: string }>({
|
||||||
|
open: false,
|
||||||
|
title: '',
|
||||||
|
type: 'IMAGE',
|
||||||
|
url: '',
|
||||||
|
});
|
||||||
|
const [docSelection, setDocSelection] = createSignal<Record<string, boolean>>({});
|
||||||
|
const [requestNote, setRequestNote] = createSignal('');
|
||||||
|
const [actionMessage, setActionMessage] = createSignal('');
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
setError('');
|
||||||
|
const res = await fetch(`${API}/api/admin/verifications?page=1&limit=200`, {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Failed to load verification queue (${res.status})`);
|
||||||
|
const data = await res.json().catch(() => ({} as any));
|
||||||
|
const items = Array.isArray(data?.items) ? data.items : [];
|
||||||
|
|
||||||
|
const mergedRows: VerificationRow[] = items.map((v: any) => {
|
||||||
|
const payload = v.payload || {};
|
||||||
|
const userType = (v.type === 'job_approval' ? 'COMPANY' : (v.type === 'requirement_approval' ? 'CUSTOMER' : 'PROFESSIONAL')) as VerificationRow['userType'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: v.id,
|
||||||
|
applicantName: v.user_name || 'Applicant',
|
||||||
|
requestType: (v.type === 'job_approval' ? 'Job Approval' : (v.type === 'requirement_approval' ? 'Service Seeker Requirement' : 'Profile Approval')) as VerificationRow['requestType'],
|
||||||
|
roleLabel: toTitle(v.role_key || 'User'),
|
||||||
|
submittedOn: v.created_at,
|
||||||
|
status: v.status as VerificationStatus,
|
||||||
|
priority: 'MEDIUM',
|
||||||
|
userType,
|
||||||
|
area: payload.city || payload.area || 'Unknown',
|
||||||
|
userId: v.user_id,
|
||||||
|
roleKey: v.role_key,
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setRows(mergedRows);
|
||||||
|
} catch (e: any) {
|
||||||
|
setRows([]);
|
||||||
|
setError(e?.message ? e.message : 'Could not load verification queue.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
void load();
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabCounts = createMemo(() => {
|
||||||
|
const all = rows();
|
||||||
|
return {
|
||||||
|
all: all.length,
|
||||||
|
profile: all.filter((r) => r.requestType === 'Profile Approval').length,
|
||||||
|
portfolio: all.filter((r) => r.requestType === 'Portfolio Approval').length,
|
||||||
|
company: all.filter((r) => r.requestType === 'Company Approval').length,
|
||||||
|
jobSeeker: all.filter((r) => r.requestType === 'Job Seeker Approval').length,
|
||||||
|
serviceProfile: all.filter((r) => r.requestType === 'Service Seeker Profile Approval').length,
|
||||||
|
serviceRequirement: all.filter((r) => r.requestType === 'Service Seeker Requirement').length,
|
||||||
|
job: all.filter((r) => r.requestType === 'Job Approval').length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredRows = createMemo(() => {
|
||||||
|
const query = search().trim().toLowerCase();
|
||||||
|
const activeTab = categoryTab();
|
||||||
|
const status = statusFilter();
|
||||||
|
const sort = sortBy();
|
||||||
|
|
||||||
|
const scoped = rows().filter((row) => {
|
||||||
|
if (activeTab === 'profile' && row.requestType !== 'Profile Approval') return false;
|
||||||
|
if (activeTab === 'portfolio' && row.requestType !== 'Portfolio Approval') return false;
|
||||||
|
if (activeTab === 'company' && row.requestType !== 'Company Approval') return false;
|
||||||
|
if (activeTab === 'job_seeker' && row.requestType !== 'Job Seeker Approval') return false;
|
||||||
|
if (activeTab === 'service_profile' && row.requestType !== 'Service Seeker Profile Approval') return false;
|
||||||
|
if (activeTab === 'service_requirement' && row.requestType !== 'Service Seeker Requirement') return false;
|
||||||
|
if (activeTab === 'job' && row.requestType !== 'Job Approval') return false;
|
||||||
|
if (status !== 'ALL' && row.status !== status) return false;
|
||||||
|
|
||||||
|
if (!query) return true;
|
||||||
|
return [row.id, row.applicantName, row.requestType, row.roleLabel, row.area]
|
||||||
|
.some((value) => String(value || '').toLowerCase().includes(query));
|
||||||
|
});
|
||||||
|
|
||||||
|
const next = [...scoped];
|
||||||
|
if (sort === 'priority') {
|
||||||
|
const rank = (p: VerificationPriority) => (p === 'HIGH' ? 3 : p === 'MEDIUM' ? 2 : 1);
|
||||||
|
next.sort((a, b) => rank(b.priority) - rank(a.priority));
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
next.sort((a, b) => (sort === 'oldest' ? 1 : -1) * (parseDate(a.submittedOn) - parseDate(b.submittedOn)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayRows = createMemo(() => {
|
||||||
|
return filteredRows();
|
||||||
|
});
|
||||||
|
|
||||||
|
const metrics = createMemo(() => {
|
||||||
|
const all = rows();
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
const submittedToday = all.filter((r) => String(r.submittedOn || '').slice(0, 10) === today);
|
||||||
|
return {
|
||||||
|
totalPending: all.filter((r) => r.status === 'PENDING' || r.status === 'UNDER_REVIEW').length,
|
||||||
|
approvedToday: submittedToday.filter((r) => r.status === 'APPROVED').length,
|
||||||
|
rejectedToday: submittedToday.filter((r) => r.status === 'REJECTED').length,
|
||||||
|
needsRevision: all.filter((r) => r.status === 'DOCUMENTS_REQUESTED' || r.status === 'REVISION_REQUESTED').length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportCsv = () => {
|
||||||
|
const headers = ['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Area'];
|
||||||
|
const lines = filteredRows().map((row) => [
|
||||||
|
row.id,
|
||||||
|
row.requestType,
|
||||||
|
row.applicantName,
|
||||||
|
row.roleLabel,
|
||||||
|
String(row.submittedOn || '').slice(0, 10),
|
||||||
|
row.status,
|
||||||
|
row.priority,
|
||||||
|
row.area,
|
||||||
|
]);
|
||||||
|
const csv = [headers, ...lines]
|
||||||
|
.map((line) => line.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(','))
|
||||||
|
.join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `verification-queue-${new Date().toISOString().slice(0, 10)}.csv`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDocuments = createMemo<SubmittedDocument[]>(() => {
|
||||||
|
const row = selectedRow();
|
||||||
|
if (!row) return [];
|
||||||
|
if (row.requestType === 'Job Approval') {
|
||||||
|
return [
|
||||||
|
{ id: 'job_desc', title: 'Job Description File', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
|
||||||
|
{ id: 'company_proof', title: 'Company Verification Snapshot', type: 'PDF', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (row.requestType === 'Service Seeker Requirement') {
|
||||||
|
return [
|
||||||
|
{ id: 'requirement_brief', title: 'Requirement Brief', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' },
|
||||||
|
{ id: 'reference', title: 'Reference Attachment', type: 'IMAGE', url: '/nxtgauge-icon.png', status: 'SUBMITTED' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
|
||||||
|
const fromPayload = Array.isArray(row.payload?.documents) ? row.payload.documents : [];
|
||||||
|
if (fromPayload.length) {
|
||||||
|
return fromPayload.slice(0, 8).map((doc: any, idx: number) => ({
|
||||||
|
id: String(doc.id || `doc-${idx + 1}`),
|
||||||
|
title: String(doc.title || doc.name || `Document ${idx + 1}`),
|
||||||
|
type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE',
|
||||||
|
url: String(doc.url || '/nxtgauge-logo.png'),
|
||||||
|
status: String(doc.status || '').toUpperCase() === 'MISSING'
|
||||||
|
? 'MISSING'
|
||||||
|
: String(doc.status || '').toUpperCase() === 'INVALID'
|
||||||
|
? 'INVALID'
|
||||||
|
: 'SUBMITTED',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const docs = ROLE_DOCUMENTS[roleSpecKey] || ROLE_DOCUMENTS.PROFESSIONAL;
|
||||||
|
return docs.map((title, idx) => ({
|
||||||
|
id: `${roleSpecKey.toLowerCase()}-doc-${idx + 1}`,
|
||||||
|
title,
|
||||||
|
type: title.toLowerCase().includes('proof') ? 'IMAGE' : 'PDF',
|
||||||
|
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
|
||||||
|
status: idx === docs.length - 1 ? 'MISSING' : 'SUBMITTED',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPortfolio = createMemo<PortfolioAsset[]>(() => {
|
||||||
|
const row = selectedRow();
|
||||||
|
if (!row || !(row.userType === 'PROFESSIONAL' || row.requestType === 'Portfolio Approval')) return [];
|
||||||
|
const fromPayload = Array.isArray(row.payload?.portfolio_images)
|
||||||
|
? row.payload.portfolio_images
|
||||||
|
: Array.isArray(row.payload?.images)
|
||||||
|
? row.payload.images
|
||||||
|
: Array.isArray(row.payload?.gallery)
|
||||||
|
? row.payload.gallery
|
||||||
|
: [];
|
||||||
|
if (fromPayload.length) {
|
||||||
|
return fromPayload.slice(0, 6).map((asset: any, idx: number) => ({
|
||||||
|
id: String(asset.id || `pf-${idx + 1}`),
|
||||||
|
title: String(asset.title || asset.name || `Portfolio ${idx + 1}`),
|
||||||
|
url: String(asset.url || '/nxtgauge-logo.png'),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return Array.from({ length: 6 }).map((_, idx) => ({
|
||||||
|
id: `pf-${idx + 1}`,
|
||||||
|
title: `Portfolio ${idx + 1}`,
|
||||||
|
url: idx % 2 === 0 ? '/nxtgauge-logo.png' : '/nxtgauge-icon.png',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedFieldValues = createMemo<Array<{ label: string; value: string }>>(() => {
|
||||||
|
const row = selectedRow();
|
||||||
|
if (!row) return [];
|
||||||
|
const roleSpecKey = normalizeRoleSpecKey(row.roleKey || row.userType);
|
||||||
|
const payload = row.payload || {};
|
||||||
|
|
||||||
|
const fullName = String(
|
||||||
|
payload.full_name
|
||||||
|
|| payload.fullName
|
||||||
|
|| [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ')
|
||||||
|
|| row.applicantName
|
||||||
|
|| '',
|
||||||
|
).trim();
|
||||||
|
const email = String(payload.email || payload.email_address || payload.emailAddress || 'applicant@nxtgauge.com');
|
||||||
|
const phone = String(payload.phone || payload.mobile || payload.mobile_number || payload.mobileNumber || '+91 90000 00000');
|
||||||
|
const area = String(payload.area || row.area || 'T. Nagar');
|
||||||
|
const place = String(payload.place || payload.locality || 'Chennai');
|
||||||
|
const city = String(payload.city || 'Chennai');
|
||||||
|
const state = String(payload.state || 'Tamil Nadu');
|
||||||
|
const pin = String(payload.pin_code || payload.pinCode || '600001');
|
||||||
|
const gender = String(payload.gender || 'Not specified');
|
||||||
|
|
||||||
|
const byLabel: Record<string, string> = {
|
||||||
|
'First Name': fullName.split(' ')[0] || fullName,
|
||||||
|
'Last Name': fullName.split(' ').slice(1).join(' ') || '—',
|
||||||
|
'Full Name': fullName || '—',
|
||||||
|
'Email Address': email,
|
||||||
|
'Mobile Number': phone,
|
||||||
|
Area: area,
|
||||||
|
Place: place,
|
||||||
|
City: city,
|
||||||
|
State: state,
|
||||||
|
'PIN Code': pin,
|
||||||
|
Gender: gender,
|
||||||
|
'Address Line 1': String(payload.address_line1 || payload.addressLine1 || payload.address || 'No. 12, Main Road'),
|
||||||
|
'Address Line 2 (Optional)': String(payload.address_line2 || payload.addressLine2 || '—'),
|
||||||
|
'Service Category': String(payload.service_category || payload.serviceCategory || row.roleLabel || 'General'),
|
||||||
|
'Company Name': String(payload.company_name || payload.companyName || row.applicantName || '—'),
|
||||||
|
'Company Email': email,
|
||||||
|
'Company Phone': phone,
|
||||||
|
'Website URL': String(payload.website || payload.website_url || payload.websiteUrl || '—'),
|
||||||
|
'Contact Person Name': String(payload.contact_person_name || payload.contactPersonName || fullName || '—'),
|
||||||
|
'Current Role': String(payload.current_role || payload.currentRole || row.roleLabel || '—'),
|
||||||
|
'Total Experience': String(payload.total_experience || payload.totalExperience || payload.experience || '—'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (row.requestType === 'Job Approval') {
|
||||||
|
return JOB_POSTING_FIELDS.map((label) => ({
|
||||||
|
label,
|
||||||
|
value: ({
|
||||||
|
'Job Title': String(payload.title || payload.job_title || '—'),
|
||||||
|
Department: String(payload.department || payload.company_department || '—'),
|
||||||
|
'Job Category': String(payload.category || payload.job_category || '—'),
|
||||||
|
'Employment Type': String(payload.employment_type || payload.type || '—'),
|
||||||
|
Seniority: String(payload.seniority || payload.level || '—'),
|
||||||
|
Openings: String(payload.openings || payload.positions || '—'),
|
||||||
|
'Role & Requirements': String(payload.requirements || payload.skills || '—'),
|
||||||
|
Compensation: String(payload.salary_range || payload.compensation || '—'),
|
||||||
|
Location: String(payload.location || payload.city || 'Chennai'),
|
||||||
|
Description: String(payload.description || payload.summary || '—'),
|
||||||
|
} as Record<string, string>)[label] || '—',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.requestType === 'Service Seeker Requirement') {
|
||||||
|
const reqRoleKey = normalizeRoleSpecKey(String(payload.role_key || payload.profession || payload.category || row.roleKey));
|
||||||
|
const dynamicReqFields = REQUIREMENT_ROLE_FIELDS[reqRoleKey] || REQUIREMENT_ROLE_FIELDS.PHOTOGRAPHER;
|
||||||
|
const baseReqFields = ['Requirement Title', 'Priority', 'Requirement Description', 'Expected Start Date', 'Service City', 'Contact Number'];
|
||||||
|
const fields = [...baseReqFields, ...dynamicReqFields];
|
||||||
|
return fields.map((label) => ({
|
||||||
|
label,
|
||||||
|
value: ({
|
||||||
|
'Requirement Title': String(payload.title || payload.requirement_title || '—'),
|
||||||
|
Priority: String(payload.priority || '—'),
|
||||||
|
'Requirement Description': String(payload.description || payload.details || '—'),
|
||||||
|
'Expected Start Date': String(payload.start_date || payload.expected_start_date || '—'),
|
||||||
|
'Service City': String(payload.city || 'Chennai'),
|
||||||
|
'Contact Number': String(payload.phone || payload.contact_number || '+91 90000 00000'),
|
||||||
|
} as Record<string, string>)[label]
|
||||||
|
|| String(
|
||||||
|
payload[label]
|
||||||
|
|| payload[label.toLowerCase().replace(/[^a-z0-9]+/g, '_')]
|
||||||
|
|| '—',
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.requestType === 'Portfolio Approval') {
|
||||||
|
const portfolioFields = [
|
||||||
|
'About',
|
||||||
|
'Services & Pricing',
|
||||||
|
'Portfolio Photos',
|
||||||
|
'Experience & Tools',
|
||||||
|
'Specialties',
|
||||||
|
'Languages',
|
||||||
|
'Service Areas',
|
||||||
|
];
|
||||||
|
return portfolioFields.map((label) => ({
|
||||||
|
label,
|
||||||
|
value: ({
|
||||||
|
About: String(payload.about || payload.bio || '—'),
|
||||||
|
'Services & Pricing': Array.isArray(payload.services) ? `${payload.services.length} service(s) added` : String(payload.pricing || '—'),
|
||||||
|
'Portfolio Photos': Array.isArray(payload.images || payload.portfolio_images || payload.gallery) ? `${(payload.images || payload.portfolio_images || payload.gallery).length} image(s)` : '0 image(s)',
|
||||||
|
'Experience & Tools': String(payload.experience || payload.tools || '—'),
|
||||||
|
Specialties: Array.isArray(payload.specialties) ? payload.specialties.join(', ') : String(payload.specialties || '—'),
|
||||||
|
Languages: Array.isArray(payload.languages) ? payload.languages.join(', ') : String(payload.languages || '—'),
|
||||||
|
'Service Areas': Array.isArray(payload.service_areas || payload.travel_areas) ? (payload.service_areas || payload.travel_areas).join(', ') : String(payload.service_areas || '—'),
|
||||||
|
} as Record<string, string>)[label] || '—',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = ROLE_PROFILE_FIELDS[roleSpecKey] || ROLE_PROFILE_FIELDS.PROFESSIONAL;
|
||||||
|
return fields.map((label) => ({ label, value: byLabel[label] || '—' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const pushToApprovalQueue = (row: VerificationRow) => {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
const item: ApprovalQueueItem = {
|
||||||
|
id: row.id,
|
||||||
|
requestType: row.requestType,
|
||||||
|
applicantName: row.applicantName,
|
||||||
|
roleLabel: row.roleLabel,
|
||||||
|
userType: row.userType,
|
||||||
|
roleKey: row.roleKey,
|
||||||
|
area: row.area,
|
||||||
|
submittedOn: row.submittedOn,
|
||||||
|
documents: selectedDocuments(),
|
||||||
|
submittedFields: selectedFieldValues(),
|
||||||
|
};
|
||||||
|
const raw = window.localStorage.getItem(APPROVAL_QUEUE_STORAGE_KEY);
|
||||||
|
const parsed = raw ? JSON.parse(raw) : [];
|
||||||
|
const current = Array.isArray(parsed) ? parsed as ApprovalQueueItem[] : [];
|
||||||
|
const filtered = current.filter((entry) => entry.id !== item.id);
|
||||||
|
window.localStorage.setItem(APPROVAL_QUEUE_STORAGE_KEY, JSON.stringify([item, ...filtered]));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySelectedStatus = async (nextStatus: VerificationStatus) => {
|
||||||
|
const current = selectedRow();
|
||||||
|
if (!current) return;
|
||||||
|
|
||||||
|
const isApprove = nextStatus === 'APPROVED';
|
||||||
|
const isReject = nextStatus === 'REJECTED';
|
||||||
|
|
||||||
|
if (!isApprove && !isReject) {
|
||||||
|
// local update only for intermediate states if needed, but usually we skip backend call here
|
||||||
|
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
|
||||||
|
setSelectedRow({ ...current, status: nextStatus });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accessToken = typeof sessionStorage !== 'undefined'
|
||||||
|
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||||
|
: '';
|
||||||
|
const common = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||||
|
},
|
||||||
|
credentials: 'include' as const,
|
||||||
|
body: isReject ? JSON.stringify({ reason: requestNote() }) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const endpoint = `/api/admin/verifications/${current.id}/${isApprove ? 'approve' : 'reject'}`;
|
||||||
|
|
||||||
|
const res = await fetch(`${API}${endpoint}`, common);
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text();
|
||||||
|
throw new Error(`Failed to update status (${res.status}): ${txt}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRows((prev) => prev.map((item) => (item.id === current.id ? { ...item, status: nextStatus } : item)));
|
||||||
|
setSelectedRow({ ...current, status: nextStatus });
|
||||||
|
|
||||||
|
if (isApprove) {
|
||||||
|
pushToApprovalQueue({ ...current, status: nextStatus });
|
||||||
|
setActionMessage('Successfully verified and sent to Approval Management.');
|
||||||
|
} else {
|
||||||
|
setActionMessage('Successfully rejected submission.');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'Failed to update backend status');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestSelectedDocuments = () => {
|
||||||
|
const selectedIds = Object.entries(docSelection()).filter(([, checked]) => checked).map(([id]) => id);
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
setActionMessage('Select at least one document before sending request.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applySelectedStatus('DOCUMENTS_REQUESTED');
|
||||||
|
setActionMessage(`Document request sent for ${selectedIds.length} item(s).`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestProfileChanges = () => {
|
||||||
|
applySelectedStatus('REVISION_REQUESTED');
|
||||||
|
setActionMessage('Revision request sent to applicant.');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="w-full space-y-6 pb-8">
|
||||||
|
<div style="margin-bottom:1.5rem">
|
||||||
|
<h1 class="text-[28px] font-bold leading-tight text-[#111827]">Verification Management</h1>
|
||||||
|
<p class="mt-1 text-[14px] text-[#6B7280]">Review and verify all platform submissions before they move to approval management</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={error()}>
|
||||||
|
<div style="border-radius:10px;border:1px solid #FECACA;background:#FEF2F2;padding:12px 16px;font-size:13px;color:#B91C1C">{error()}</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
|
||||||
|
{([
|
||||||
|
{ key: 'all', label: 'All Verifications' },
|
||||||
|
{ key: 'view', label: 'View Verification' },
|
||||||
|
] as const).map((tab) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setListTab(tab.key)}
|
||||||
|
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${listTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={listTab() === 'all'}>
|
||||||
|
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
|
||||||
|
{([
|
||||||
|
{ key: 'all', label: `All Verifications (${tabCounts().all})` },
|
||||||
|
{ key: 'profile', label: `Profile Approvals (${tabCounts().profile})` },
|
||||||
|
{ key: 'portfolio', label: `Portfolio Approvals (${tabCounts().portfolio})` },
|
||||||
|
{ key: 'company', label: `Company Approvals (${tabCounts().company})` },
|
||||||
|
{ key: 'job_seeker', label: `Job Seeker Approvals (${tabCounts().jobSeeker})` },
|
||||||
|
{ key: 'service_profile', label: `Service Seeker Profile (${tabCounts().serviceProfile})` },
|
||||||
|
{ key: 'service_requirement', label: `Service Seeker Requirements (${tabCounts().serviceRequirement})` },
|
||||||
|
{ key: 'job', label: `Job Approvals (${tabCounts().job})` },
|
||||||
|
] as const).map((tab) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCategoryTab(tab.key)}
|
||||||
|
style={`height:44px;padding:0 2px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;white-space:nowrap;${categoryTab() === tab.key ? 'color:#FF5E13;box-shadow:inset 0 -2px 0 #FF5E13' : 'color:#6B7280'}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={listTab() === 'all'}>
|
||||||
|
<div style="position:relative;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:visible;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:14px 20px;border-bottom:1px solid #F3F4F6">
|
||||||
|
<input
|
||||||
|
value={search()}
|
||||||
|
onInput={(e) => setSearch(e.currentTarget.value)}
|
||||||
|
placeholder="Search by ID, name or type..."
|
||||||
|
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div style="position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setSortOpen((v) => !v); setFilterOpen(false); }}
|
||||||
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
||||||
|
Sort
|
||||||
|
</button>
|
||||||
|
<Show when={sortOpen()}>
|
||||||
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||||
|
{([
|
||||||
|
{ key: 'latest', label: 'Latest Submitted' },
|
||||||
|
{ key: 'oldest', label: 'Oldest Submitted' },
|
||||||
|
{ key: 'priority', label: 'Priority (High-Low)' },
|
||||||
|
] as const).map((item) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setSortBy(item.key); setSortOpen(false); }}
|
||||||
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="position:relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setFilterOpen((v) => !v); setSortOpen(false); }}
|
||||||
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||||
|
Filters
|
||||||
|
</button>
|
||||||
|
<Show when={filterOpen()}>
|
||||||
|
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||||
|
{([
|
||||||
|
{ key: 'ALL', label: 'All Status' },
|
||||||
|
{ key: 'PENDING', label: 'Pending' },
|
||||||
|
{ key: 'UNDER_REVIEW', label: 'Under Review' },
|
||||||
|
{ key: 'DOCUMENTS_REQUESTED', label: 'Documents Requested' },
|
||||||
|
{ key: 'REVISION_REQUESTED', label: 'Revision Requested' },
|
||||||
|
{ key: 'APPROVED', label: 'Approved' },
|
||||||
|
{ key: 'REJECTED', label: 'Rejected' },
|
||||||
|
] as const).map((item) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setStatusFilter(item.key); setFilterOpen(false); }}
|
||||||
|
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={exportCsv}
|
||||||
|
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer"
|
||||||
|
>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto overflow-y-visible">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A;text-align:left">
|
||||||
|
{['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Actions'].map((header) => (
|
||||||
|
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<Show
|
||||||
|
when={displayRows().length > 0}
|
||||||
|
fallback={
|
||||||
|
<tr>
|
||||||
|
<td colSpan={8} style="padding:32px;text-align:center">
|
||||||
|
<p style="font-size:15px;font-weight:600;color:#111827">No verification requests found</p>
|
||||||
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Try changing filters or search.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For each={displayRows()}>
|
||||||
|
{(row) => {
|
||||||
|
const s = statusUi(row.status);
|
||||||
|
const p = priorityUi(row.priority);
|
||||||
|
return (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6" class="hover:bg-[#FAFAFA] transition-colors">
|
||||||
|
<td style="padding:12px 20px;font-size:12px;font-family:monospace;color:#6B7280">#{row.id}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.requestType}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<div style="display:flex;flex-direction:column;gap:2px">
|
||||||
|
<span style="font-size:14px;font-weight:600;color:#111827">{row.applicantName}</span>
|
||||||
|
<span style="font-size:12px;color:#6B7280">{row.area || 'Chennai'}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px;font-size:14px;color:#111827">{row.roleLabel}</td>
|
||||||
|
<td style="padding:12px 20px;font-size:13px;color:#6B7280">{String(row.submittedOn || '').slice(0, 10) || '—'}</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${s.border};background:${s.bg};color:${s.text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<span style={`display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:${p.color}`}>
|
||||||
|
<span style={`display:inline-block;width:6px;height:6px;border-radius:50%;background:${p.color}`} />
|
||||||
|
{p.label}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 20px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedRow(row);
|
||||||
|
setDocSelection({});
|
||||||
|
setRequestNote('');
|
||||||
|
setActionMessage('');
|
||||||
|
setListTab('view');
|
||||||
|
}}
|
||||||
|
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;display:inline-flex;align-items:center;cursor:pointer"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</Show>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={displayRows().length > 0}>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #F3F4F6;padding:12px 20px">
|
||||||
|
<p style="font-size:13px;color:#6B7280">
|
||||||
|
Showing <strong style="font-weight:600;color:#111827">1–{displayRows().length}</strong> of <strong style="font-weight:600;color:#111827">{displayRows().length}</strong> verifications
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;align-items:center;gap:4px">
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">‹</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;background:#FF5E13;color:white;font-size:13px;font-weight:600;border:none;cursor:pointer">1</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">2</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:13px;font-weight:500;cursor:pointer">3</button>
|
||||||
|
<button type="button" style="display:inline-flex;width:30px;height:30px;align-items:center;justify-content:center;border-radius:7px;border:1px solid #E5E7EB;background:white;color:#6B7280;cursor:pointer;font-size:15px">›</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={listTab() === 'view'}>
|
||||||
|
<Show
|
||||||
|
when={selectedRow()}
|
||||||
|
fallback={
|
||||||
|
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;padding:48px 24px;text-align:center">
|
||||||
|
<p style="font-size:15px;font-weight:600;color:#111827">No verification selected</p>
|
||||||
|
<p style="margin-top:6px;font-size:13px;color:#6B7280">Click <strong>View</strong> on a row from <strong>All Verifications</strong>.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style="margin-top:24px;display:grid;gap:12px">
|
||||||
|
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:20px 24px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<div>
|
||||||
|
<h2 style="font-size:18px;font-weight:700;color:#111827">#{selectedRow()!.id}</h2>
|
||||||
|
<p style="margin-top:2px;font-size:13px;color:#6B7280">{selectedRow()!.requestType}</p>
|
||||||
|
</div>
|
||||||
|
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${statusUi(selectedRow()!.status).border};background:${statusUi(selectedRow()!.status).bg};color:${statusUi(selectedRow()!.status).text};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||||||
|
{statusUi(selectedRow()!.status).label}
|
||||||
|
</span>
|
||||||
|
</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">Applicant Name</p>
|
||||||
|
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.applicantName}</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">Role</p>
|
||||||
|
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.roleLabel}</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">Area</p>
|
||||||
|
<p style="margin-top:4px;font-size:14px;font-weight:600;color:#111827">{selectedRow()!.area || 'Chennai'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Form Details</h3>
|
||||||
|
<span style="font-size:12px;color:#6B7280">{selectedFieldValues().length} fields</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding:14px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||||||
|
<For each={selectedFieldValues()}>
|
||||||
|
{(entry) => (
|
||||||
|
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||||||
|
<p style="margin:0;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">{entry.label}</p>
|
||||||
|
<p style="margin:6px 0 0;font-size:13px;font-weight:700;color:#111827;line-height:1.35">{entry.value}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<h3 style="margin:0;font-size:16px;font-weight:700;color:#111827">Submitted Documents</h3>
|
||||||
|
<span style="font-size:12px;color:#6B7280">{selectedDocuments().length} files</span>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#0D0D2A;text-align:left">
|
||||||
|
{['Document', 'State', 'View', 'Request'].map((header) => (
|
||||||
|
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<For each={selectedDocuments()}>
|
||||||
|
{(doc) => (
|
||||||
|
<tr style="border-bottom:1px solid #F3F4F6">
|
||||||
|
<td style="padding:12px 16px;font-size:14px;font-weight:600;color:#111827">{doc.title}</td>
|
||||||
|
<td style="padding:12px 16px;font-size:13px;color:#6B7280">{toTitle(doc.status)}</td>
|
||||||
|
<td style="padding:12px 16px">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewer({ open: true, title: doc.title, type: doc.type, url: doc.url })}
|
||||||
|
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td style="padding:12px 16px">
|
||||||
|
<label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;color:#374151;cursor:pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(docSelection()[doc.id])}
|
||||||
|
onChange={(e) => setDocSelection((prev) => ({ ...prev, [doc.id]: e.currentTarget.checked }))}
|
||||||
|
style="accent-color:#FF5E13"
|
||||||
|
/>
|
||||||
|
Request
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||||||
|
<div style="padding:16px 20px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between;background:#FFFBF0">
|
||||||
|
<h3 style="margin:0;font-size:16px;font-weight:700;color:#854D0E">Verification Requirements Checklist</h3>
|
||||||
|
<span style="font-size:12px;font-weight:600;color:#A16207">System Audit</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding:20px;display:grid;gap:12px">
|
||||||
|
<For each={selectedDocuments()}>
|
||||||
|
{(doc) => (
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-radius:8px;background:#F9FAFB;border:1px solid #E5E7EB">
|
||||||
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
|
<div style={`width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;${doc.status === 'SUBMITTED' ? 'background:#22C55E' : 'background:#EF4444'}`}>
|
||||||
|
<Show when={doc.status === 'SUBMITTED'} fallback={<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>}>
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<span style="font-size:13px;font-weight:600;color:#374151">{doc.title}</span>
|
||||||
|
</div>
|
||||||
|
<span style={`font-size:11px;font-weight:700;padding:2px 8px;border-radius:4px;${doc.status === 'SUBMITTED' ? 'background:#DCFCE7;color:#166534' : 'background:#FEE2E2;color:#991B1B'}`}>
|
||||||
|
{doc.status === 'SUBMITTED' ? 'RECEIVED' : 'MISSING'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<div style="margin-top:8px;padding-top:16px;border-top:1px dashed #E5E7EB">
|
||||||
|
<p style="font-size:12px;font-weight:700;color:#6B7280;margin-bottom:8px;text-transform:uppercase">Profile Completeness</p>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
||||||
|
<For each={selectedFieldValues().slice(0, 6)}>
|
||||||
|
{(field) => (
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;font-size:12px;color:#4B5563">
|
||||||
|
<div style={`width:6px;height:6px;border-radius:50%;${field.value && field.value !== '—' ? 'background:#22C55E' : 'background:#EF4444'}`} />
|
||||||
|
<span style="opacity:0.8">{field.label}:</span>
|
||||||
|
<span style="font-weight:600">{field.value && field.value !== '—' ? 'Filled' : 'Empty'}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);padding:16px">
|
||||||
|
<h3 style="margin:0 0 8px;font-size:16px;font-weight:700;color:#111827">Reviewer Actions</h3>
|
||||||
|
<textarea
|
||||||
|
value={requestNote()}
|
||||||
|
onInput={(e) => setRequestNote(e.currentTarget.value)}
|
||||||
|
placeholder="Reviewer note for applicant"
|
||||||
|
style="width:100%;min-height:74px;border:1px solid #E5E7EB;border-radius:10px;padding:10px;font-size:13px;color:#374151;resize:vertical"
|
||||||
|
/>
|
||||||
|
<div style="margin-top:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||||
|
<button type="button" onClick={requestSelectedDocuments} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Selected Documents</button>
|
||||||
|
<button type="button" onClick={requestProfileChanges} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Request Changes</button>
|
||||||
|
<button type="button" onClick={() => applySelectedStatus('APPROVED')} style="height:34px;border-radius:8px;border:none;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:700;color:white;cursor:pointer">Approve</button>
|
||||||
|
<button type="button" onClick={() => applySelectedStatus('REJECTED')} style="height:34px;border-radius:8px;border:1px solid #FECACA;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#B91C1C;cursor:pointer">Reject</button>
|
||||||
|
</div>
|
||||||
|
<Show when={actionMessage()}>
|
||||||
|
<div style="margin-top:10px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;padding:8px 10px;font-size:12px;color:#374151">{actionMessage()}</div>
|
||||||
|
</Show>
|
||||||
|
<Show when={selectedRow()!.status === 'APPROVED'}>
|
||||||
|
<A href="/admin/approval" style="margin-top:8px;height:32px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:700;display:inline-flex;align-items:center;text-decoration:none">
|
||||||
|
Open Approval Management
|
||||||
|
</A>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;align-items:center;gap:10px">
|
||||||
|
<A
|
||||||
|
href={`/admin/verification/${encodeURIComponent(selectedRow()!.id)}?roleKey=${encodeURIComponent(selectedRow()!.roleKey || '')}&userId=${encodeURIComponent(selectedRow()!.userId || '')}&type=${encodeURIComponent(selectedRow()!.requestType)}`}
|
||||||
|
style="height:36px;border-radius:8px;background:#0D0D2A;padding:0 16px;font-size:13px;font-weight:600;color:white;display:inline-flex;align-items:center;text-decoration:none"
|
||||||
|
>
|
||||||
|
Open Full Review
|
||||||
|
</A>
|
||||||
|
<button type="button" onClick={() => setListTab('all')} style="height:36px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 16px;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Back to List</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={viewer().open}>
|
||||||
|
<div style="position:fixed;inset:0;z-index:9999;background:rgba(17,24,39,0.72);display:flex;align-items:center;justify-content:center;padding:20px">
|
||||||
|
<div style="width:min(90vw,900px);max-height:88vh;border-radius:14px;overflow:hidden;border:1px solid #E5E7EB;background:white;display:flex;flex-direction:column">
|
||||||
|
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{viewer().title}</p>
|
||||||
|
<button type="button" onClick={() => setViewer({ open: false, title: '', type: 'IMAGE', url: '' })} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Close</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:12px;overflow:auto;display:flex;justify-content:center;align-items:center;background:#F8FAFC">
|
||||||
|
<Show
|
||||||
|
when={viewer().type === 'IMAGE'}
|
||||||
|
fallback={<iframe src={viewer().url} style="width:100%;height:70vh;border:none;background:white" title={viewer().title} />}
|
||||||
|
>
|
||||||
|
<img src={viewer().url} alt={viewer().title} style="max-width:100%;max-height:70vh;object-fit:contain" />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ export default function LoginPage() {
|
||||||
const body = JSON.stringify({ email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' });
|
const body = JSON.stringify({ email: email().trim().toLowerCase(), password: password(), loginTarget: 'admin' });
|
||||||
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' };
|
const headers = { 'Content-Type': 'application/json', Accept: 'application/json', 'x-portal-target': 'admin' };
|
||||||
let payload: any = {}; let status = 500; let success = false;
|
let payload: any = {}; let status = 500; let success = false;
|
||||||
const r = await fetch('/api/gateway/auth/login', { method: 'POST', headers, credentials: 'include', body });
|
const r = await fetch('/api/auth/login', { method: 'POST', headers, credentials: 'include', body });
|
||||||
status = r.status; payload = await r.json().catch(() => ({}));
|
status = r.status; payload = await r.json().catch(() => ({}));
|
||||||
if (r.ok) { success = true; }
|
if (r.ok) { success = true; }
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|
@ -101,12 +101,12 @@ export default function LoginPage() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let payload: any = {}; let status = 500;
|
let payload: any = {}; let status = 500;
|
||||||
for (const { url, body } of [
|
for (const { url, body } of [
|
||||||
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
|
{ url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
|
||||||
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
|
{ url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify(reqBody) },
|
||||||
{ url: '/api/gateway/users/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
|
{ url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
|
||||||
{ url: '/api/gateway/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
|
{ url: '/api/auth/internal/forgot-password/request-code', body: JSON.stringify({ data: reqBody }) },
|
||||||
]) {
|
]) {
|
||||||
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
|
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
|
||||||
status = r.status; payload = await r.json().catch(() => ({}));
|
status = r.status; payload = await r.json().catch(() => ({}));
|
||||||
if (r.ok) break;
|
if (r.ok) break;
|
||||||
|
|
@ -136,7 +136,7 @@ export default function LoginPage() {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let payload: any = {}; let status = 500; let success = false;
|
let payload: any = {}; let status = 500; let success = false;
|
||||||
for (const url of ['/api/gateway/users/auth/internal/forgot-password/verify-code', '/api/gateway/auth/internal/forgot-password/verify-code']) {
|
for (const url of ['/api/auth/internal/forgot-password/verify-code', '/api/auth/internal/forgot-password/verify-code']) {
|
||||||
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
|
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body });
|
||||||
status = r.status; payload = await r.json().catch(() => ({}));
|
status = r.status; payload = await r.json().catch(() => ({}));
|
||||||
if (r.ok) { success = true; break; }
|
if (r.ok) { success = true; break; }
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue