feat(admin): Phase 0 — Tailwind v4 foundation, shell rewrite, modern dashboard

- Install Tailwind CSS v4 via @tailwindcss/vite; configure vite.config.ts
- Rewrite app.css: Tailwind base, Exo 2 font, brand tokens (orange #fd6216, navy #050026), scrollbar utility; fix @import order
- Rewrite AdminShell.tsx: fixed header, fixed inset body grid (sidebar + main), session check, sub-tab system, logout, admin avatar/name/role
- Rewrite AdminSidebar.tsx: collapsible w-64/w-20, orange active rail + badge/dot, CSS filter for SVG icon tinting, min-h-0 flex fix
- Replace 84 route stub CSS classes (page-title, card, btn, table-wrap, etc.) with Tailwind equivalents via safe class-attr-only regex script
- Rewrite admin dashboard: Lucide icons in colored chip backgrounds, 4-col KPI grid, Control Plane 6-module grid, hover lift animations
- Disable SSR (ssr: false) to fix Vinxi dev manifest error; clear stale .vinxi cache
- Add lucide-solid icon library
- Add scripts/cleanup-css.mjs for class migration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-23 23:00:21 +01:00
parent 0050277922
commit a272276055
92 changed files with 1879 additions and 2871 deletions

View file

@ -4,9 +4,8 @@
{
"name": "admin-solid",
"runtimeExecutable": "sh",
"runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port $PORT"],
"port": 3002,
"autoPort": true
"runtimeArgs": ["-c", "cd /Users/ashwin/workspace/nxtgauge-admin-solid && npm run dev -- --port 3002"],
"port": 3002
}
]
}

327
package-lock.json generated
View file

@ -9,7 +9,10 @@
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.3.2",
"@tailwindcss/vite": "^4.2.2",
"lucide-solid": "^1.0.1",
"solid-js": "^1.9.5",
"tailwindcss": "^4.2.2",
"vinxi": "^0.5.7"
},
"devDependencies": {
@ -355,7 +358,6 @@
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.0",
"tslib": "^2.4.0"
@ -367,7 +369,6 @@
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@ -378,7 +379,6 @@
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@ -968,7 +968,6 @@
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
@ -2311,6 +2310,272 @@
"integrity": "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==",
"license": "CC0-1.0"
},
"node_modules/@tailwindcss/node": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz",
"integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
"enhanced-resolve": "^5.19.0",
"jiti": "^2.6.1",
"lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.2.2"
}
},
"node_modules/@tailwindcss/node/node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz",
"integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==",
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.2.2",
"@tailwindcss/oxide-darwin-arm64": "4.2.2",
"@tailwindcss/oxide-darwin-x64": "4.2.2",
"@tailwindcss/oxide-freebsd-x64": "4.2.2",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2",
"@tailwindcss/oxide-linux-arm64-gnu": "4.2.2",
"@tailwindcss/oxide-linux-arm64-musl": "4.2.2",
"@tailwindcss/oxide-linux-x64-gnu": "4.2.2",
"@tailwindcss/oxide-linux-x64-musl": "4.2.2",
"@tailwindcss/oxide-wasm32-wasi": "4.2.2",
"@tailwindcss/oxide-win32-arm64-msvc": "4.2.2",
"@tailwindcss/oxide-win32-x64-msvc": "4.2.2"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz",
"integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz",
"integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz",
"integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz",
"integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz",
"integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz",
"integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz",
"integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz",
"integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz",
"integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz",
"integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.8.1",
"@emnapi/runtime": "^1.8.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.1",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz",
"integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz",
"integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 20"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz",
"integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==",
"license": "MIT",
"dependencies": {
"@tailwindcss/node": "4.2.2",
"@tailwindcss/oxide": "4.2.2",
"tailwindcss": "4.2.2"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7 || ^8"
}
},
"node_modules/@tanstack/directive-functions-plugin": {
"version": "1.121.21",
"resolved": "https://registry.npmjs.org/@tanstack/directive-functions-plugin/-/directive-functions-plugin-1.121.21.tgz",
@ -2391,7 +2656,6 @@
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@ -4009,6 +4273,19 @@
"node": ">= 0.8"
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
"integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
@ -5019,7 +5296,6 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@ -5056,7 +5332,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5077,7 +5352,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5098,7 +5372,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5119,7 +5392,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5140,7 +5412,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5161,7 +5432,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5182,7 +5452,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5203,7 +5472,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5224,7 +5492,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5245,7 +5512,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5266,7 +5532,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@ -5401,6 +5666,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-solid": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/lucide-solid/-/lucide-solid-1.0.1.tgz",
"integrity": "sha512-fpo0UmEHdEmdvydbFS9lQUqQ9rqSqWXMY4w3n9tVr9FqBBWJB0YI/lTMSxCspHs706ZzHIS/1YEnDhLOIP+m+A==",
"license": "ISC",
"peerDependencies": {
"solid-js": "^1.4.7"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@ -7355,6 +7629,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tailwindcss": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
"integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.1.tgz",
"integrity": "sha512-b+u3CEM6FjDHru+nhUSjDofpWSBp2rINziJWgApm72wwGasQ/wKXftZe4tI2Y5HPv6OpzXSZHOFq87H4vfsgsw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tar": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz",

View file

@ -13,7 +13,10 @@
"@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.15.0",
"@solidjs/start": "^1.3.2",
"@tailwindcss/vite": "^4.2.2",
"lucide-solid": "^1.0.1",
"solid-js": "^1.9.5",
"tailwindcss": "^4.2.2",
"vinxi": "^0.5.7"
},
"engines": {

167
scripts/cleanup-css.mjs Normal file
View file

@ -0,0 +1,167 @@
#!/usr/bin/env node
/**
* Replace old hand-written CSS class names with Tailwind v4 equivalents.
* SAFE version: only replaces inside class="..." attribute values.
*
* Run: node scripts/cleanup-css.mjs
*/
import { readFileSync, writeFileSync } from 'fs';
import { globSync } from 'glob';
// Order matters: most-specific (multi-word) patterns first.
// Each entry: [old-class-string, new-tailwind-classes]
const REPLACEMENTS = [
// ── Page structure ──────────────────────────────────────────────
['page-hero-card page-actions', 'mb-6 flex items-start justify-between gap-4'],
['page-actions-right', 'flex items-center gap-2'],
['page-actions', 'mb-6 flex items-start justify-between gap-4'],
['page-title-block', 'flex flex-col gap-1'],
['page-title', 'text-2xl font-bold text-gray-900'],
['page-subtitle', 'mt-1 text-sm text-gray-500'],
// ── Card ────────────────────────────────────────────────────────
['card', 'rounded-xl border border-gray-200 bg-white shadow-sm'],
// ── Buttons (specific before generic) ───────────────────────────
['btn navy', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'],
['btn primary', 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'],
['btn danger', 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors'],
['btn sm', 'inline-flex items-center rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-50 transition-colors'],
['btn', 'inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors'],
// ── Table ────────────────────────────────────────────────────────
['table-wrap', 'overflow-x-auto'],
['list-table', 'w-full text-sm'],
['perm-table', 'w-full text-sm'],
['table-actions', 'flex items-center justify-end gap-1'],
['align-right', 'text-right'],
['row-selected', 'bg-orange-50'],
// ── Icon action buttons ──────────────────────────────────────────
['action-icon-btn danger', 'rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm'],
['action-icon-btn', 'rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm'],
// ── Status chips (specific before generic) ───────────────────────
['status-chip active', 'inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700'],
['status-chip pending', 'inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-700'],
['status-chip inactive', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-500'],
['status-chip', 'inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600'],
// ── Error / notice ───────────────────────────────────────────────
['error-box', 'mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700'],
// ── Form sections ────────────────────────────────────────────────
['role-form-section', 'mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm'],
['field-grid-2', 'mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2'],
['module-picker', 'mt-3 flex flex-wrap gap-2'],
['module-chip selected', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-orange-300 bg-orange-50 px-3 py-1.5 text-xs font-medium text-orange-700'],
['module-chip', 'flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300'],
// ── Legacy tab nav (now handled by AdminShell) ────────────────────
['admin-link-tabs', 'hidden'],
['admin-link-tab active', 'hidden'],
['admin-link-tab', 'hidden'],
];
/**
* Replace old CSS class names only inside JSX class attribute values.
* Handles:
* class="foo bar baz"
* class={'foo bar baz'}
* class={`foo bar baz`}
* class={condition ? 'foo' : 'bar'}
*
* We do this by extracting the string portion of class attributes and
* replacing only within those extracted portions.
*/
function replaceInClassAttributes(src) {
let changed = false;
// Process class="..." and class='...' (static strings)
src = src.replace(/class=["']([^"']*)["']/g, (match, classes) => {
let replaced = classes;
for (const [from, to] of REPLACEMENTS) {
// Replace whole-word occurrences within the class string
// Use word boundaries to avoid partial matches
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
const quote = match[6] === '"' ? '"' : "'"; // char after `class=`
return `class=${quote}${replaced}${quote}`;
});
// Process class={`...`} (template literals with static content)
src = src.replace(/class=\{`([^`]*)`\}/g, (match, classes) => {
let replaced = classes;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
return `class={\`${replaced}\`}`;
});
// Process class={'...'} and class={"..."} (single-expression strings)
src = src.replace(/class=\{(['"])([^'"]*)\1\}/g, (match, q, classes) => {
let replaced = classes;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
replaced = replaced.replace(re, to);
}
if (replaced !== classes) changed = true;
return `class={${q}${replaced}${q}}`;
});
// Process ternary class values: class={cond ? 'foo' : 'bar'}
// Match string literals inside class={...} expressions
src = src.replace(/class=\{[^}]+\}/g, (match) => {
// Replace inside single/double-quoted string literals within the expression
const replaced = match.replace(/'([^']*)'/g, (m, inner) => {
let r = inner;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
r = r.replace(re, to);
}
if (r !== inner) changed = true;
return `'${r}'`;
}).replace(/"([^"]*)"/g, (m, inner) => {
// skip JSX attributes within the expression (avoid double replacement)
let r = inner;
for (const [from, to] of REPLACEMENTS) {
const re = new RegExp(`(?<![\\w-])${escapeRe(from)}(?![\\w-])`, 'g');
r = r.replace(re, to);
}
if (r !== inner) changed = true;
return `"${r}"`;
});
return replaced;
});
return { src, changed };
}
function escapeRe(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const files = globSync('src/routes/admin/**/*.tsx', {
cwd: '/Users/ashwin/workspace/nxtgauge-admin-solid',
absolute: true,
});
let totalChanged = 0;
for (const file of files) {
const original = readFileSync(file, 'utf8');
const { src, changed } = replaceInClassAttributes(original);
if (changed) {
writeFileSync(file, src, 'utf8');
totalChanged++;
console.log(` updated: ${file.replace('/Users/ashwin/workspace/nxtgauge-admin-solid/', '')}`);
}
}
console.log(`\nDone. Updated ${totalChanged} / ${files.length} files.`);

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,6 @@ import { createMemo, createSignal, onMount, type JSX } from 'solid-js';
import AdminSidebar from './AdminSidebar';
import { isExternalIdentity } from '~/lib/admin-auth';
import { clearAdminSession, hasAdminSession, setAdminSession } from '~/lib/admin-session';
import { sidebarCollapsed } from '~/lib/sidebar-state';
type Tab = { href: string; label: string; exact?: boolean };
@ -52,42 +51,45 @@ const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [
];
const PAGE_TITLES: Array<{ prefix: string; title: string }> = [
{ prefix: '/admin/workspace', title: 'Dashboard Workspace' },
{ prefix: '/admin/settings', title: 'Settings' },
{ prefix: '/admin/role-modules', title: 'Role Modules' },
{ prefix: '/admin/modules', title: 'Module Management' },
{ prefix: '/admin/responses', title: 'Lead Responses' },
{ prefix: '/admin/applications', title: 'Applications' },
{ prefix: '/admin/financial', title: 'Financial Management' },
{ prefix: '/admin/help', title: 'Support Management' },
{ prefix: '/admin/verification-status', title: 'Verification Status' },
{ prefix: '/admin/verification', title: 'Verification Review' },
{ prefix: '/admin/employees', title: 'Employee Management' },
{ prefix: '/admin/department', title: 'Department Management' },
{ prefix: '/admin/designation', title: 'Designation Management' },
{ prefix: '/admin/roles', title: 'Internal Role Management' },
{ prefix: '/admin/runtime-roles', title: 'External Role Management' },
{ prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' },
{ prefix: '/admin/internal-dashboard-management', title: 'Internal Dashboard Management' },
{ prefix: '/admin/external-dashboard-management', title: 'External Dashboard Management' },
{ prefix: '/admin/role-ui-configs', title: 'External Dashboard Management' },
{ prefix: '/admin/approval', title: 'Approval Management' },
{ prefix: '/admin/users', title: 'Users Management' },
{ prefix: '/admin/company', title: 'Company Management' },
{ prefix: '/admin/customer', title: 'Customer Management' },
{ prefix: '/admin/candidate', title: 'Candidate Management' },
{ prefix: '/admin/customer', title: 'Customer Management' },
{ prefix: '/admin/photographer', title: 'Photographer Management' },
{ prefix: '/admin/makeup-artist', title: 'Makeup Artist Management' },
{ prefix: '/admin/tutors', title: 'Tutors Management' },
{ prefix: '/admin/developers', title: 'Developers Management' },
{ prefix: '/admin/video-editors', title: 'Video Editor Management' },
{ prefix: '/admin/fitness-trainers', title: 'Fitness Trainer Management' },
{ prefix: '/admin/catering-services', title: 'Catering Services Management' },
{ prefix: '/admin/graphic-designers', title: 'Graphics Designer Management' },
{ prefix: '/admin/social-media-managers', title: 'Social Media Manager Management' },
{ prefix: '/admin/jobs', title: 'Jobs Management' },
{ prefix: '/admin/leads', title: 'Leads Management' },
{ prefix: '/admin/requirements', title: 'Requirement Request' },
{ prefix: '/admin/pricing', title: 'Pricing Management' },
{ prefix: '/admin/invoice', title: 'Invoice Management' },
{ prefix: '/admin/credit', title: 'Credit Management' },
{ prefix: '/admin/ledger', title: 'Ledger Management' },
{ prefix: '/admin/coupon', title: 'Coupon Management' },
{ prefix: '/admin/discount', title: 'Discount Management' },
{ prefix: '/admin/tax', title: 'Tax Management' },
{ prefix: '/admin/order', title: 'Order Management' },
{ prefix: '/admin/invoice', title: 'Invoice Management' },
{ prefix: '/admin/review', title: 'Review Management' },
{ prefix: '/admin/support', title: 'Support Management' },
{ prefix: '/admin/kb', title: 'Knowledge Base Management' },
{ prefix: '/admin/notifications', title: 'Notifications' },
{ prefix: '/admin/report', title: 'Report Management' },
{ prefix: '/admin/employees', title: 'Employee Management' },
{ prefix: '/admin/roles', title: 'Internal Role Management' },
{ prefix: '/admin/external-role-management', title: 'External Role Management' },
{ prefix: '/admin/internal-role-management', title: 'Internal Role Management' },
{ prefix: '/admin/runtime-roles', title: 'External Role Management' },
{ prefix: '/admin/onboarding-management', title: 'External Onboarding Management' },
{ prefix: '/admin/onboarding-schemas', title: 'External Onboarding Management' },
{ prefix: '/admin/kb', title: 'KB Management' },
{ prefix: '/admin/ledger', title: 'Ledger Management' },
{ prefix: '/admin/workspace', title: 'Dashboard Workspace' },
{ prefix: '/admin', title: 'Dashboard' },
];
@ -96,6 +98,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [checkedSession, setCheckedSession] = createSignal(false);
const [adminName, setAdminName] = createSignal('Admin');
const [adminRole, setAdminRole] = createSignal('Super Admin');
const tabs = createMemo<Tab[]>(() => {
const path = location.pathname;
@ -121,9 +125,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
});
onMount(() => {
// ?_preview=1 or sessionStorage flag — bypass auth for UI testing without a live backend.
// Sets the session cookie AND a sessionStorage flag so all subsequent pages in this tab
// also skip the API check without needing ?_preview=1 in every URL.
const isLocalDev =
typeof window !== 'undefined' &&
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
@ -144,7 +145,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
navigate(`/login?from=${from}`, { replace: true });
return;
}
try {
const accessToken =
typeof sessionStorage !== 'undefined'
@ -160,9 +160,9 @@ export default function AdminShell(props: { children: JSX.Element }) {
credentials: 'include',
});
const payload = await response.json().catch(() => ({}));
if (!response.ok || isExternalIdentity(payload)) {
throw new Error('Unauthorized');
}
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized');
if (payload?.full_name) setAdminName(payload.full_name);
if (payload?.role?.name) setAdminRole(payload.role.name);
setCheckedSession(true);
} catch {
clearAdminSession();
@ -177,77 +177,109 @@ export default function AdminShell(props: { children: JSX.Element }) {
const onLogout = async () => {
await fetch('/api/gateway/users/auth/logout', {
method: 'POST',
headers: {
Accept: 'application/json',
'x-portal-target': 'admin',
},
headers: { Accept: 'application/json', 'x-portal-target': 'admin' },
credentials: 'include',
}).catch(() => {});
clearAdminSession();
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem('nxtgauge_admin_access_token');
sessionStorage.removeItem('nxtgauge_admin_preview');
}
navigate('/login', { replace: true });
};
const initials = () => adminName().charAt(0).toUpperCase() || 'A';
return (
<div class="admin-root">
<header class="admin-header">
<div class="admin-header-left">
<div class="admin-brand">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" />
<div class="min-h-screen bg-gray-50">
{/* ── Fixed Header ── */}
<header class="fixed top-0 z-50 flex h-16 w-full items-center justify-between border-b border-gray-200 bg-white px-6 shadow-sm">
{/* Left: logo + page title */}
<div class="flex items-center gap-8">
<div class="flex h-10 items-center">
<img src="/nxtgauge-logo.png" alt="NXTGAUGE" class="h-8 w-auto object-contain" />
</div>
<h1 class="admin-page-heading">{pageTitle()}</h1>
<h1 class="ml-28 text-base font-semibold text-gray-800">{pageTitle()}</h1>
</div>
<div class="admin-header-actions">
<button class="admin-notification-btn" type="button" aria-label="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path strokeLinecap="round" strokeLinejoin="round" d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
</button>
<button class="admin-avatar-btn" type="button" aria-label="Admin profile">
<span class="admin-avatar">A</span>
<span class="admin-avatar-meta">
<span class="admin-avatar-name">Admin</span>
<span class="admin-avatar-role">Super Admin</span>
</span>
</button>
<button class="admin-logout-btn" type="button" onClick={onLogout} aria-label="Logout">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
{/* Right: notifications + avatar + logout */}
<div class="flex items-center gap-6">
{/* Notification bell */}
<button
type="button"
aria-label="Notifications"
class="relative rounded-full p-2 text-gray-500 transition-colors hover:bg-gray-100"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
<path stroke-linecap="round" stroke-linejoin="round" d="M13.73 21a2 2 0 0 1-3.46 0" />
</svg>
</button>
{/* Avatar + name */}
<div class="flex items-center gap-2">
<button
type="button"
class="flex items-center gap-3 rounded-lg p-1 pr-2 transition-colors hover:bg-gray-100"
>
<div class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full border border-orange-200 bg-orange-100 text-sm font-semibold text-orange-700">
{initials()}
</div>
<div class="flex flex-col items-start">
<span class="mb-0.5 text-xs font-semibold leading-none text-gray-700">{adminName()}</span>
<span class="text-[10px] leading-none text-gray-500">{adminRole()}</span>
</div>
</button>
{/* Logout */}
<button
type="button"
onClick={onLogout}
aria-label="Logout"
class="rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-red-50 hover:text-red-600"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
</button>
</div>
</div>
</header>
{/* ── Body: sidebar + main (fixed, below header) ── */}
{checkedSession() ? (
<div class={`shell${sidebarCollapsed() ? ' sidebar-collapsed' : ''}`}>
<aside class="sidebar-wrap">
<AdminSidebar />
</aside>
<main class="main">
{tabs().length > 0 ? (
<div class="admin-tab-wrap">
<nav class="admin-tabs">
{tabs().map((tab) => (
<A href={tab.href} class={`admin-tab ${isTabActive(tab) ? 'active' : ''}`}>
{tab.label}
</A>
))}
</nav>
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
{/* Sidebar */}
<AdminSidebar />
{/* Main content */}
<main class="scrollbar min-w-0 overflow-y-auto bg-gray-50 p-6">
{/* Sub-tabs (shown for multi-tab sections) */}
{tabs().length > 0 && (
<div class="mb-6 flex gap-6 border-b border-gray-200">
{tabs().map((tab) => (
<A
href={tab.href}
class={`pb-3 text-sm font-medium transition-colors ${
isTabActive(tab)
? 'border-b-2 border-[#fd6216] text-gray-900'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</A>
))}
</div>
) : null}
<div class="main-inner">{props.children}</div>
)}
{props.children}
</main>
</div>
) : (
<div class="shell">
<main class="main">
<div class="card">
<p class="notice">Checking session...</p>
</div>
/* Session check loading state */
<div class="fixed inset-0 top-16 grid grid-cols-[auto_1fr]">
<div class="w-64 border-r border-slate-200 bg-[#fcfcfd]" />
<main class="flex items-center justify-center bg-gray-50">
<p class="text-sm text-gray-400">Checking session</p>
</main>
</div>
)}

View file

@ -1,94 +1,140 @@
import { A, useLocation } from '@solidjs/router';
import { sidebarCollapsed, toggleSidebar } from '~/lib/sidebar-state';
import { createSignal } from 'solid-js';
type LinkItem = { legacyHref: string; href: string; label: string; icon: string; aliasPrefix?: string };
type LinkItem = {
href: string;
label: string;
icon: string;
aliasPrefix?: string;
};
const links: LinkItem[] = [
{ legacyHref: '/', href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' },
{ legacyHref: '/roles?scope=internal', href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' },
{ legacyHref: '/runtime-roles', href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' },
{ legacyHref: '/onboarding-management', href: '/admin/onboarding-schemas', label: 'External Onboarding Management', icon: 'reviews.svg' },
{ legacyHref: '/internal-dashboard-management', href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' },
{ legacyHref: '/external-dashboard-management', href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' },
{ legacyHref: '/approval', href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' },
{ legacyHref: '/department', href: '/admin/department', label: 'Department Management', icon: 'department.svg' },
{ legacyHref: '/designation', href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' },
{ legacyHref: '/employees', href: '/admin/employees', label: 'Employee Management', icon: 'users.svg' },
{ legacyHref: '/users', href: '/admin/users', label: 'Users Management', icon: 'users.svg' },
{ legacyHref: '/company', href: '/admin/company', label: 'Company Management', icon: 'company.svg' },
{ legacyHref: '/candidate', href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' },
{ legacyHref: '/customer', href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' },
{ legacyHref: '/photographer', href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' },
{ legacyHref: '/makeup-artist', href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' },
{ legacyHref: '/tutors', href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg' },
{ legacyHref: '/developers', href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg' },
{ legacyHref: '/video-editors', href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' },
{ legacyHref: '/fitness-trainers', href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' },
{ legacyHref: '/catering-services', href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' },
{ legacyHref: '/graphic-designers', href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: 'developers.svg' },
{ legacyHref: '/social-media-managers', href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' },
{ legacyHref: '/jobs', href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' },
{ legacyHref: '/leads', href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' },
{ legacyHref: '/pricing', href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' },
{ legacyHref: '/credit', href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' },
{ legacyHref: '/coupon', href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' },
{ legacyHref: '/discount', href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' },
{ legacyHref: '/tax', href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' },
{ legacyHref: '/order', href: '/admin/order', label: 'Order Management', icon: 'order.svg' },
{ legacyHref: '/invoice', href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' },
{ legacyHref: '/review', href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' },
{ legacyHref: '/help', href: '/admin/support', label: 'Support Management', icon: 'support.svg' },
{ legacyHref: '/report', href: '/admin/report', label: 'Report Management', icon: 'report.svg' },
{ legacyHref: '/ledger', href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' },
{ legacyHref: '/kb', href: '/admin/kb', label: 'KB Management', icon: 'reviews.svg' },
{ legacyHref: '/notifications', href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' },
{ href: '/admin', label: 'Dashboard', icon: 'dashboard.svg' },
{ href: '/admin/department', label: 'Department Management', icon: 'department.svg' },
{ href: '/admin/designation', label: 'Designation Management', icon: 'designation.svg' },
{ href: '/admin/employees', label: 'Employee Management', icon: 'users.svg' },
{ href: '/admin/roles', label: 'Internal Role Management', icon: 'role.svg' },
{ href: '/admin/runtime-roles', label: 'External Role Management', icon: 'role.svg' },
{ href: '/admin/onboarding-schemas', label: 'External Onboarding Management', icon: 'reviews.svg' },
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: 'dashboard.svg' },
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: 'dashboard.svg', aliasPrefix: '/admin/role-ui-configs' },
{ href: '/admin/approval', label: 'Approval Management', icon: 'approval.svg' },
{ href: '/admin/users', label: 'Users Management', icon: 'users.svg' },
{ href: '/admin/company', label: 'Company Management', icon: 'company.svg' },
{ href: '/admin/candidate', label: 'Candidate Management', icon: 'candidate.svg' },
{ href: '/admin/customer', label: 'Customer Management', icon: 'users.svg' },
{ href: '/admin/photographer', label: 'Photographer Management', icon: 'photographer.svg' },
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: 'makeup-artist.svg' },
{ href: '/admin/tutors', label: 'Tutors Management', icon: 'tutor.svg' },
{ href: '/admin/developers', label: 'Developers Management', icon: 'developers.svg' },
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: 'developers.svg' },
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: 'tutor.svg' },
{ href: '/admin/catering-services', label: 'Catering Services Management', icon: 'company.svg' },
{ href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: 'developers.svg' },
{ href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: 'developers.svg' },
{ href: '/admin/jobs', label: 'Jobs Management', icon: 'jobs.svg' },
{ href: '/admin/leads', label: 'Leads Management', icon: 'leads.svg' },
{ href: '/admin/pricing', label: 'Pricing Management', icon: 'pricing.svg' },
{ href: '/admin/credit', label: 'Credit Management', icon: 'credits.svg' },
{ href: '/admin/coupon', label: 'Coupon Management', icon: 'coupon.svg' },
{ href: '/admin/discount', label: 'Discount Management', icon: 'discount.svg' },
{ href: '/admin/tax', label: 'Tax Management', icon: 'tax.svg' },
{ href: '/admin/order', label: 'Order Management', icon: 'order.svg' },
{ href: '/admin/invoice', label: 'Invoice Management', icon: 'invoice.svg' },
{ href: '/admin/review', label: 'Review Management', icon: 'reviews.svg' },
{ href: '/admin/support', label: 'Support Management', icon: 'support.svg' },
{ href: '/admin/kb', label: 'Knowledge Base Management', icon: 'reviews.svg' },
{ href: '/admin/notifications', label: 'Notifications', icon: 'reviews.svg' },
{ href: '/admin/report', label: 'Report Management', icon: 'report.svg' },
{ href: '/admin/ledger', label: 'Ledger Management', icon: 'ledger.svg' },
];
export default function AdminSidebar() {
const location = useLocation();
const collapsed = sidebarCollapsed;
const [collapsed, setCollapsed] = createSignal(false);
const isLinkActive = (href: string, aliasPrefix?: string) => {
const pathOnly = href.split('?')[0] || href;
if (pathOnly === '/admin') return location.pathname === '/admin';
const isActive = (href: string, aliasPrefix?: string) => {
if (href === '/admin') return location.pathname === '/admin';
if (aliasPrefix && location.pathname.startsWith(aliasPrefix)) return true;
return location.pathname === pathOnly || location.pathname.startsWith(`${pathOnly}/`);
return location.pathname === href || location.pathname.startsWith(`${href}/`);
};
return (
<aside class={`sidebar${collapsed() ? ' sidebar-collapsed' : ''}`}>
<div class="sidebar-toggle-row">
<aside
class={`flex h-full flex-col border-r border-slate-200 bg-[#fcfcfd] px-3 pb-3 pt-5 transition-all duration-300 ${
collapsed() ? 'w-20' : 'w-64'
}`}
>
{/* Collapse toggle */}
<div class="mb-4 flex justify-end px-2">
<button
type="button"
class="sidebar-toggle-btn"
onClick={() => setCollapsed((v) => !v)}
aria-label={collapsed() ? 'Expand sidebar' : 'Collapse sidebar'}
onClick={toggleSidebar}
class="rounded-lg p-2 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700"
>
<span class={`sidebar-chevron${collapsed() ? ' collapsed' : ''}`}></span>
<svg
class={`h-4 w-4 transition-transform ${collapsed() ? 'rotate-180' : ''}`}
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
<nav class="sidebar-nav scrollbar">
{/* Nav items */}
<nav class="scrollbar min-h-0 flex-1 space-y-1.5 overflow-y-auto pr-1">
{links.map((item) => {
const active = isLinkActive(item.href, item.aliasPrefix);
const active = isActive(item.href, item.aliasPrefix);
return (
<A
href={item.href}
class={`nav-item ${active ? 'active' : ''}`}
activeClass=""
inactiveClass=""
data-legacy-href={item.legacyHref}
title={collapsed() ? item.label : undefined}
class={`group relative flex items-center gap-3 rounded-xl border px-3 py-3 text-[15px] leading-5 transition-all ${
collapsed() ? 'justify-center px-2' : ''
} ${
active
? 'border-orange-200 bg-gradient-to-r from-orange-50 to-orange-100/70 text-slate-900 shadow-[inset_3px_0_0_0_#FD6216]'
: 'border-transparent text-slate-500 hover:border-slate-200 hover:bg-white hover:text-slate-800'
}`}
>
<span class="nav-active-rail" />
<img class="nav-icon" src={`/sidebar-icons/${item.icon}`} alt="" />
{/* Active left rail */}
<span
class={`absolute bottom-2 left-0 top-2 w-[3px] rounded-r-full bg-orange-500 transition-opacity ${
active ? 'opacity-100' : 'opacity-0'
}`}
/>
{/* Icon */}
<img
src={`/sidebar-icons/${item.icon}`}
alt=""
class="h-[18px] w-[18px] shrink-0 transition-all"
style={active
? 'filter: invert(42%) sepia(78%) saturate(1200%) hue-rotate(360deg) brightness(95%) contrast(95%); opacity:1'
: 'opacity:0.45'
}
/>
{/* Label */}
{!collapsed() && (
<span class="nav-title">{item.label}</span>
<span class={`truncate font-medium ${active ? 'text-slate-900' : 'text-slate-600 group-hover:text-slate-800'}`}>
{item.label}
</span>
)}
{/* Active badge (expanded) */}
{!collapsed() && active && (
<span class="active-badge">Active</span>
<span class="ml-auto rounded-full border border-orange-200 bg-orange-100 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-orange-700">
Active
</span>
)}
{/* Active dot (collapsed) */}
{collapsed() && active && (
<span class="collapsed-dot" />
<span class="absolute -right-0.5 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-orange-500" />
)}
</A>
);

View file

@ -47,13 +47,13 @@ export default function LegacyModuleShellPage() {
return (
<AdminShell>
<h1 class="page-title">{moduleName()}</h1>
<p class="page-subtitle">
<h1 class="text-2xl font-bold text-gray-900">{moduleName()}</h1>
<p class="mt-1 text-sm text-gray-500">
Live legacy module embedded for exact design and functionality parity during migration.
</p>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="actions">
<A class="btn" href={legacyUrl()} target="_blank">Open Module In New Tab</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={legacyUrl()} target="_blank">Open Module In New Tab</A>
</div>
<iframe
src={legacyUrl()}

View file

@ -84,20 +84,20 @@ export default function ApplicationsPage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">Applications</h1>
<p class="page-subtitle">Review submitted applications and update acceptance status.</p>
<h1 class="text-2xl font-bold text-gray-900">Applications</h1>
<p class="mt-1 text-sm text-gray-500">Review submitted applications and update acceptance status.</p>
</div>
<Show when={payload.loading}>
<div class="card"><p class="notice">Loading applications...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading applications...</p></div>
</Show>
<Show when={error()}>
<div class="card"><p class="error-note">{error()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="error-note">{error()}</p></div>
</Show>
<Show when={!payload.loading && applications().length === 0}>
<div class="card"><p class="notice">No applications found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">No applications found.</p></div>
</Show>
<div class="list-grid" style="grid-template-columns:1fr;gap:14px">
@ -105,7 +105,7 @@ export default function ApplicationsPage() {
{(app) => {
const req = createMemo(() => requirementFor(app.requirementId));
return (
<article class="card">
<article class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap">
<div>
<h2 style="margin:0;font-size:19px">{req()?.title || 'Unknown Requirement'}</h2>
@ -119,7 +119,7 @@ export default function ApplicationsPage() {
<p class="kv-value" style="font-weight:500">{app.message || 'No message provided.'}</p>
</div>
<div class="field-grid-2" style="margin-top:12px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" style="margin-top:12px">
<div class="kv-item">
<p class="kv-label">Quote</p>
<p class="kv-value"> {app.quote || 0}</p>
@ -140,15 +140,15 @@ export default function ApplicationsPage() {
<div class="actions">
<Show when={app.status === 'SUBMITTED'}>
<button class="btn primary" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
<button class="btn danger" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
</Show>
<Show when={app.status === 'SHORTLISTED'}>
<button class="btn primary" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
<button class="btn danger" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'ACCEPTED')}>Accept Bid</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'REJECTED')}>Decline</button>
</Show>
<Show when={app.status === 'SUBMITTED' || app.status === 'SHORTLISTED'}>
<button class="btn" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'WITHDRAWN')}>Withdraw</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" disabled={busyId() === app.id} onClick={() => updateStatus(app.id, 'WITHDRAWN')}>Withdraw</button>
</Show>
</div>
</article>

View file

@ -7,7 +7,7 @@ export default function ApprovalManagementAliasPage() {
onMount(() => navigate('/admin/approval', { replace: true }));
return (
<AdminShell>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Redirecting to Approval Management...</p>
</div>
</AdminShell>

View file

@ -254,11 +254,11 @@ function RoleTypeBadge(props: { type?: RoleType }) {
function StatusBadge(props: { status: string; isDocRequest?: boolean }) {
const s = props.status.toUpperCase();
const label = props.isDocRequest ? 'MORE DOCS REQUIRED' : s.replace(/_/g, ' ');
if (s === 'APPROVED') return <span class="status-chip active">{label}</span>;
if (s === 'REJECTED') return <span class="status-chip" style="background:#ef4444;color:#fff;border-color:#ef4444">{label}</span>;
if (s === 'CHANGES_REQUESTED') return <span class="status-chip" style={`background:${props.isDocRequest ? '#3b82f6' : '#f97316'};color:#fff;border-color:currentColor`}>{label}</span>;
if (s === 'CANCELLED') return <span class="status-chip" style="background:#94a3b8;color:#fff;border-color:#94a3b8">{label}</span>;
return <span class="status-chip" style="background:#f59e0b;color:#fff;border-color:#f59e0b">{label}</span>;
if (s === 'APPROVED') return <span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">{label}</span>;
if (s === 'REJECTED') return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#ef4444;color:#fff;border-color:#ef4444">{label}</span>;
if (s === 'CHANGES_REQUESTED') return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style={`background:${props.isDocRequest ? '#3b82f6' : '#f97316'};color:#fff;border-color:currentColor`}>{label}</span>;
if (s === 'CANCELLED') return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#94a3b8;color:#fff;border-color:#94a3b8">{label}</span>;
return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#f59e0b;color:#fff;border-color:#f59e0b">{label}</span>;
}
// ────────────────────────────── API calls ──────────────────────────────
@ -676,10 +676,10 @@ export default function ApprovalPage() {
return (
<AdminShell>
{/* Page header */}
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Approval Management</h1>
<p class="page-subtitle">Review, approve, reject and configure approval workflows.</p>
<h1 class="text-2xl font-bold text-gray-900">Approval Management</h1>
<p class="mt-1 text-sm text-gray-500">Review, approve, reject and configure approval workflows.</p>
</div>
</div>
@ -713,22 +713,22 @@ export default function ApprovalPage() {
</div>
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
<Show when={approvals.error}>
<div class="error-box" style="margin-bottom:12px">{String((approvals.error as any)?.message || approvals.error)}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{String((approvals.error as any)?.message || approvals.error)}</div>
</Show>
<Show when={!approvals.loading && !snapshot.loading}>
<div class="card" style="margin-bottom:12px;background:#f8fafc;border-color:#e2e8f0">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:12px;background:#f8fafc;border-color:#e2e8f0">
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap">
<div style="display:flex;gap:8px;flex-wrap:wrap">
<span class="status-chip" style="background:#e0f2fe;color:#075985;border-color:#bae6fd">Pending Jobs: {summary().jobs}</span>
<span class="status-chip" style="background:#ecfeff;color:#155e75;border-color:#a5f3fc">Pending Requirements: {summary().requirements}</span>
<span class="status-chip" style="background:#eef2ff;color:#3730a3;border-color:#c7d2fe">Pending Profiles: {summary().profilePending}</span>
<span class="status-chip" style="background:#fef3c7;color:#92400e;border-color:#fde68a">Total Pending: {summary().totalPending}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#e0f2fe;color:#075985;border-color:#bae6fd">Pending Jobs: {summary().jobs}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#ecfeff;color:#155e75;border-color:#a5f3fc">Pending Requirements: {summary().requirements}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#eef2ff;color:#3730a3;border-color:#c7d2fe">Pending Profiles: {summary().profilePending}</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fef3c7;color:#92400e;border-color:#fde68a">Total Pending: {summary().totalPending}</span>
</div>
<button class="btn" type="button" onClick={() => { refetchApprovals(); refetchSnapshot(); }}>Refresh</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={() => { refetchApprovals(); refetchSnapshot(); }}>Refresh</button>
</div>
<Show when={summary().totalPending === 0}>
<p style="margin:10px 0 0;color:#64748b;font-size:13px">
@ -747,7 +747,7 @@ export default function ApprovalPage() {
<Show when={activeTab() !== 'rules'}>
<Show when={!showDetail()}>
{/* Filter bar */}
<div class="card" style="margin-bottom:12px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:12px;display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<input
type="text"
placeholder="Search requester, email, type or ID..."
@ -773,9 +773,9 @@ export default function ApprovalPage() {
<span style="font-size:12px;color:#64748b;margin-left:auto">{filteredApprovals().length} record{filteredApprovals().length !== 1 ? 's' : ''}</span>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Requester</th>
@ -784,7 +784,7 @@ export default function ApprovalPage() {
<th>Document Request</th>
<th>Status</th>
<th>Submitted</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -837,10 +837,10 @@ export default function ApprovalPage() {
{(item.createdAt || item.created_at) ? new Date((item.createdAt || item.created_at)!).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
{/* View detail */}
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
type="button"
title="View Request"
style="font-size:12px;padding:4px 10px"
@ -849,12 +849,12 @@ export default function ApprovalPage() {
View
</button>
<Show when={item._viewHref}>
<A class="btn" href={item._viewHref!} style="font-size:12px;padding:4px 10px" title="Open full request page">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={item._viewHref!} style="font-size:12px;padding:4px 10px" title="Open full request page">
Full Request
</A>
</Show>
<Show when={item._supportsSubmissionView}>
<A class="btn" href={`/admin/approval/${item.id}`} style="font-size:12px;padding:4px 10px" title="Open full profile review">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/approval/${item.id}`} style="font-size:12px;padding:4px 10px" title="Open full profile review">
Profile Review
</A>
</Show>
@ -862,7 +862,7 @@ export default function ApprovalPage() {
<Show when={status === 'PENDING'}>
{/* Request More Documents */}
<button
class="action-icon-btn"
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Request More Documents"
disabled={isActing}
@ -873,7 +873,7 @@ export default function ApprovalPage() {
</button>
{/* Request Changes */}
<button
class="action-icon-btn"
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Request Changes"
disabled={isActing}
@ -884,7 +884,7 @@ export default function ApprovalPage() {
</button>
{/* Approve */}
<button
class="action-icon-btn"
class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm"
type="button"
title="Approve"
disabled={isActing}
@ -895,7 +895,7 @@ export default function ApprovalPage() {
</button>
{/* Reject */}
<button
class="action-icon-btn danger"
class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm"
type="button"
title="Reject"
disabled={isActing}
@ -906,7 +906,7 @@ export default function ApprovalPage() {
</Show>
<Show when={status === 'CHANGES_REQUESTED'}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
type="button"
title="Update requested documents"
style="font-size:12px;padding:4px 10px;background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe"
@ -921,7 +921,7 @@ export default function ApprovalPage() {
<Show when={status === 'APPROVED'}>
<A
href={dest.href}
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
style="font-size:11px;padding:3px 8px;background:#f0fdf4;color:#15803d;border-color:#bbf7d0;white-space:nowrap"
title={`Open ${dest.label}`}
>
@ -939,9 +939,9 @@ export default function ApprovalPage() {
</table>
</div>
<div class="admin-pagination">
<button class="btn" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}> Prev</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}> Prev</button>
<span>Page {currentPage()} of {totalPages()}</span>
<button class="btn" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>Next </button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>Next </button>
</div>
</section>
</Show>
@ -963,20 +963,20 @@ export default function ApprovalPage() {
{/* ── Rules Tab ── */}
<Show when={activeTab() === 'rules'}>
<Show when={ruleError()}>
<div class="error-box" style="margin-bottom:12px">{ruleError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{ruleError()}</div>
</Show>
<div class="page-actions" style="margin-bottom:16px">
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:16px">
<div />
<button class="btn navy" onClick={() => setShowAddRule((v) => !v)}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => setShowAddRule((v) => !v)}>
{showAddRule() ? 'Cancel' : '+ Add Rule'}
</button>
</div>
<Show when={showAddRule()}>
<div class="card" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="font-size:15px;font-weight:600;color:#0f172a;margin:0 0 14px">New Approval Rule</h3>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Rule Name <span style="color:#ef4444">*</span></label>
<input
@ -1003,24 +1003,24 @@ export default function ApprovalPage() {
</div>
</div>
<div class="actions" style="margin-top:14px">
<button class="btn navy" disabled={submittingRule()} onClick={handleAddRule}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={submittingRule()} onClick={handleAddRule}>
{submittingRule() ? 'Saving...' : 'Save Rule'}
</button>
<button class="btn" onClick={() => setShowAddRule(false)}>Cancel</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setShowAddRule(false)}>Cancel</button>
</div>
</div>
</Show>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Rule Name</th>
<th>Entity Type</th>
<th>Approver Type</th>
<th>Priority</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -1039,9 +1039,9 @@ export default function ApprovalPage() {
<td style="color:#475569">{rule.approverType || rule.approver_type || '—'}</td>
<td style="color:#475569">{rule.priority ?? '—'}</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deletingRule() === rule.id}
onClick={() => handleDeleteRule(rule.id)}
>
@ -1083,19 +1083,19 @@ function ApprovalDetailPanel(props: {
return (
<Show when={a()} fallback={
<div class="card"><p class="notice">Select an approval from the list.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Select an approval from the list.</p></div>
}>
<div class="page-hero-card page-actions" style="margin-bottom:12px">
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:12px">
<div>
<h2 class="page-title" style="font-size:18px">Approval Detail</h2>
<p class="page-subtitle">{a()!._typeLabel || 'Request'}</p>
<h2 class="text-2xl font-bold text-gray-900" style="font-size:18px">Approval Detail</h2>
<p class="mt-1 text-sm text-gray-500">{a()!._typeLabel || 'Request'}</p>
</div>
<button class="btn" type="button" onClick={props.onBack}> Back to List</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={props.onBack}> Back to List</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
{/* Request info */}
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Request Summary</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tbody>
@ -1110,7 +1110,7 @@ function ApprovalDetailPanel(props: {
</div>
{/* Requester info */}
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Requester</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tbody>
@ -1124,21 +1124,21 @@ function ApprovalDetailPanel(props: {
{/* Actions */}
<div style="margin-top:16px;display:flex;flex-wrap:wrap;gap:8px">
<Show when={isPending()}>
<button class="btn" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe;font-size:13px" disabled={isActing()} onClick={() => props.onRequestMoreDocuments(a()!.id)}>📄 Request Documents</button>
<button class="btn" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;font-size:13px" disabled={isActing()} onClick={() => props.onRequestChanges(a()!.id)}> Request Changes</button>
<button class="btn" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px" disabled={isActing()} onClick={() => props.onApprove(a()!.id)}> Approve</button>
<button class="btn danger" style="font-size:13px" disabled={isActing()} onClick={() => props.onReject(a()!.id)}> Reject</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#eff6ff;color:#1d4ed8;border-color:#bfdbfe;font-size:13px" disabled={isActing()} onClick={() => props.onRequestMoreDocuments(a()!.id)}>📄 Request Documents</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;font-size:13px" disabled={isActing()} onClick={() => props.onRequestChanges(a()!.id)}> Request Changes</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px" disabled={isActing()} onClick={() => props.onApprove(a()!.id)}> Approve</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:13px" disabled={isActing()} onClick={() => props.onReject(a()!.id)}> Reject</button>
</Show>
<Show when={status() === 'APPROVED'}>
<A href={dest().href} class="btn" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px">
<A href={dest().href} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0;font-size:13px">
Open {dest().label}
</A>
</Show>
<Show when={a()!._viewHref}>
<A href={a()!._viewHref!} class="btn" style="font-size:13px">Open Full Request</A>
<A href={a()!._viewHref!} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="font-size:13px">Open Full Request</A>
</Show>
<Show when={a()!._supportsSubmissionView}>
<A href={`/admin/approval/${a()!.id}`} class="btn" style="font-size:13px">Open Profile Review</A>
<A href={`/admin/approval/${a()!.id}`} class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" style="font-size:13px">Open Profile Review</A>
</Show>
</div>
</div>
@ -1146,7 +1146,7 @@ function ApprovalDetailPanel(props: {
{/* Submitted fields */}
<Show when={a()!._parsedReason?.values && Object.keys(a()!._parsedReason!.values!).length > 0}>
<div class="card" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Submitted Data</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:8px">
{Object.entries(a()!._parsedReason!.values!).map(([k, v]) => (
@ -1161,7 +1161,7 @@ function ApprovalDetailPanel(props: {
{/* Document request spotlight (USP) */}
<Show when={docRemark()}>
<div class="card" style="margin-bottom:16px;border:1px solid #bfdbfe;background:#eff6ff">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;border:1px solid #bfdbfe;background:#eff6ff">
<h3 style="margin:0 0 10px;font-size:15px;font-weight:700;color:#1d4ed8">Requested Documents</h3>
<p style="margin:0;font-size:13px;color:#0f172a">{docRemark()!.comment}</p>
<Show when={(docRemark()!.fields || []).length > 0}>
@ -1174,7 +1174,7 @@ function ApprovalDetailPanel(props: {
{/* Admin remarks history */}
<Show when={remarks().length > 0}>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Admin Remarks History</h3>
<div style="display:flex;flex-direction:column;gap:8px">
<For each={remarks()}>

View file

@ -247,20 +247,20 @@ export default function ApprovalDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Submission Review</h1>
<p class="page-subtitle">Review a user's onboarding form submission and take action.</p>
<h1 class="text-2xl font-bold text-gray-900">Submission Review</h1>
<p class="mt-1 text-sm text-gray-500">Review a user's onboarding form submission and take action.</p>
</div>
<A class="btn" href="/admin/approval"> Back to Approvals</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/approval"> Back to Approvals</A>
</div>
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
<Show when={actionDone()}>
<div class="card" style="margin-bottom:12px;border-left:4px solid #22c55e;padding:12px 16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:12px;border-left:4px solid #22c55e;padding:12px 16px">
<p style="margin:0;font-weight:600;color:#166534">
{actionDone() === 'APPROVED' ? '✓ Profile approved successfully.' : '✕ Profile rejected.'}
</p>
@ -271,11 +271,11 @@ export default function ApprovalDetailPage() {
</Show>
<Show when={data.loading}>
<div class="card"><p class="notice">Loading submission...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading submission...</p></div>
</Show>
<Show when={!data.loading && !data()}>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Submission not found or user does not have an onboarding record for this role.</p>
<p style="font-size:13px;color:#64748b;margin-top:8px">
Make sure the URL includes <code>?roleKey=PHOTOGRAPHER</code> (or the relevant role key).
@ -286,7 +286,7 @@ export default function ApprovalDetailPage() {
<Show when={data()}>
{/* ── Action bar ── */}
<Show when={!actionDone()}>
<div class="card" style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:16px;padding:12px 16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:16px;padding:12px 16px">
<span style={`display:inline-block;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;${ROLE_COLORS[roleType()]}`}>
{(roleKey() || 'UNKNOWN').replace(/_/g, ' ')}
</span>
@ -298,7 +298,7 @@ export default function ApprovalDetailPage() {
<div style="flex:1" />
<Show when={data()!.onboarding?.status === 'COMPLETED' && !actionDone()}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
style="background:#f0fdf4;color:#15803d;border-color:#bbf7d0"
disabled={!!acting()}
onClick={handleApprove}
@ -306,7 +306,7 @@ export default function ApprovalDetailPage() {
{acting() === 'APPROVE' ? 'Approving...' : '✓ Approve Profile'}
</button>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={!!acting()}
onClick={handleReject}
>
@ -318,7 +318,7 @@ export default function ApprovalDetailPage() {
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
{/* User info */}
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">User Info</h3>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tbody>
@ -328,7 +328,7 @@ export default function ApprovalDetailPage() {
<tr>
<td style="color:#64748b;padding:5px 10px 5px 0">Account Status</td>
<td>
<span class={`status-chip${data()!.user.status === 'ACTIVE' ? ' active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600${data()!.user.status === 'ACTIVE' ? ' active' : ''}`}>
{data()!.user.status}
</span>
</td>
@ -346,7 +346,7 @@ export default function ApprovalDetailPage() {
</div>
{/* Submission status */}
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600;color:#0f172a">Submission Info</h3>
<Show when={data()!.onboarding} fallback={
<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:14px">
@ -383,7 +383,7 @@ export default function ApprovalDetailPage() {
{/* ── No form data fallback ── */}
<Show when={data()!.onboarding && submittedRows().length === 0}>
<div class="card" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="margin:0 0 10px;font-size:15px;font-weight:600;color:#0f172a">Submitted Form Answers</h3>
<p class="notice">Onboarding state is present but contains no displayable field data.</p>
</div>
@ -417,7 +417,7 @@ function SubmissionViewer(props: { rows: Array<{ key: string; value: string }> }
<>
{/* ── Text / data fields ── */}
<Show when={textFields().length > 0}>
<div class="card" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="margin:0 0 14px;font-size:15px;font-weight:600;color:#0f172a">
Submitted Form Data
<span style="margin-left:8px;font-size:12px;font-weight:400;color:#64748b">{textFields().length} fields</span>
@ -449,7 +449,7 @@ function SubmissionViewer(props: { rows: Array<{ key: string; value: string }> }
{/* ── Documents & Images ── */}
<Show when={mediaFields().length > 0}>
<div class="card" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h3 style="margin:0 0 14px;font-size:15px;font-weight:600;color:#0f172a">
Documents & Media
<span style="margin-left:8px;font-size:12px;font-weight:400;color:#64748b">{mediaFields().length} file{mediaFields().length !== 1 ? 's' : ''}</span>
@ -595,7 +595,7 @@ function SubmissionViewer(props: { rows: Array<{ key: string; value: string }> }
</a>
<button
type="button"
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
style="padding:4px 10px;font-size:13px"
onClick={() => setPdfViewer(null)}
>

View file

@ -7,7 +7,7 @@ export default function ApprovalsAliasPage() {
onMount(() => navigate('/admin/approval', { replace: true }));
return (
<AdminShell>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Redirecting to Approval Management...</p>
</div>
</AdminShell>

View file

@ -36,14 +36,14 @@ export default function CandidatePage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Candidate Management</h1>
<p class="page-subtitle">Manage all job seeker accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Candidate Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all job seeker accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function CandidatePage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function CandidatePage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -36,14 +36,14 @@ export default function CateringServicesPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Catering Services Management</h1>
<p class="page-subtitle">Manage all catering services accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Catering Services Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all catering services accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function CateringServicesPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function CateringServicesPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -30,12 +30,12 @@ async function fetchCompanies(): Promise<Company[]> {
function StatusBadge(props: { status: string }) {
if (props.status === 'ACTIVE') {
return <span class="status-chip active">ACTIVE</span>;
return <span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>;
}
if (props.status === 'PENDING') {
return <span class="status-chip" style="background:#f59e0b;color:#fff">PENDING</span>;
return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#f59e0b;color:#fff">PENDING</span>;
}
return <span class="status-chip">{props.status}</span>;
return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">{props.status}</span>;
}
export default function CompanyPage() {
@ -72,20 +72,20 @@ export default function CompanyPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Company Management</h1>
<p class="page-subtitle">Manage all company accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Company Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all company accounts on the platform.</p>
</div>
<A class="btn navy" href="/admin/company/create">Create Company</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/company/create">Create Company</A>
</div>
<Show when={actionError()}>
<div class="error-box">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{actionError()}</div>
</Show>
{/* Search */}
<div class="card" style="margin-bottom:16px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<input
type="text"
placeholder="Search by name or email..."
@ -95,9 +95,9 @@ export default function CompanyPage() {
/>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Company Name</th>
@ -105,7 +105,7 @@ export default function CompanyPage() {
<th>City</th>
<th>Email</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -132,10 +132,10 @@ export default function CompanyPage() {
<StatusBadge status={item.status} />
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/company/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/company/${item.id}`}>View</A>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, displayName)}
>

View file

@ -96,39 +96,39 @@ export default function CompanyDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">{name()}</h1>
<p class="page-subtitle">{companyId()}</p>
<h1 class="text-2xl font-bold text-gray-900">{name()}</h1>
<p class="mt-1 text-sm text-gray-500">{companyId()}</p>
</div>
<div class="page-actions-right">
<div class="flex items-center gap-2">
<span class={`status-pill ${isVerified() ? 'status-approved' : 'status-pending'}`}>
{isVerified() ? 'Verified - Can Post Jobs' : 'Not Verified'}
</span>
<A class="btn" href="/admin/company">Back to Companies</A>
<A class="btn navy" href="/admin/approval">Open Approval Management</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/company">Back to Companies</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/approval">Open Approval Management</A>
</div>
</div>
<Show when={bundle.loading}>
<div class="card"><p class="notice">Loading company...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading company...</p></div>
</Show>
<Show when={!bundle.loading && !company()}>
<div class="card"><p class="notice">Company not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Company not found.</p></div>
</Show>
<Show when={company()}>
<>
<div class="field-grid-2" style="margin-bottom:16px">
<div class="card">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" style="margin-bottom:16px">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="kv-label">Email</p>
<p class="kv-value">{company()!.email || '—'}</p>
</div>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="kv-label">Phone</p>
<p class="kv-value">{company()!.phone || '—'}</p>
</div>
<div class="card" style="grid-column:1 / -1">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="grid-column:1 / -1">
<p class="kv-label">Website</p>
<p class="kv-value">
<Show when={website()} fallback={'—'}>
@ -140,9 +140,9 @@ export default function CompanyDetailPage() {
<div class="detail-layout">
<div style="display:flex;flex-direction:column;gap:16px">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Company Information</h2>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item">
<p class="kv-label">Company Name</p>
<p class="kv-value">{name()}</p>
@ -186,9 +186,9 @@ export default function CompanyDetailPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Contact Details</h2>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item">
<p class="kv-label">City</p>
<p class="kv-value">{contact()?.city || company()!.city || '—'}</p>
@ -216,9 +216,9 @@ export default function CompanyDetailPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Banking Details</h2>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item">
<p class="kv-label">Account Holder</p>
<p class="kv-value">{banking()?.accountHolderName || '—'}</p>
@ -248,7 +248,7 @@ export default function CompanyDetailPage() {
</div>
<div style="display:flex;flex-direction:column;gap:16px">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Verification Status</h2>
<div class={`sub-card ${isVerified() ? 'sub-card-success' : 'sub-card-warning'}`} style="margin-top:0">
<p class="kv-label">Overall Status</p>
@ -273,7 +273,7 @@ export default function CompanyDetailPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Metadata</h2>
<div class="kv-item">
<p class="kv-label">Created At</p>

View file

@ -52,20 +52,20 @@ export default function CreateCompanyPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Create Company</h1>
<p class="page-subtitle">Add a new organization profile to the admin company catalog.</p>
<h1 class="text-2xl font-bold text-gray-900">Create Company</h1>
<p class="mt-1 text-sm text-gray-500">Add a new organization profile to the admin company catalog.</p>
</div>
<A class="btn" href="/admin/company">Back to Companies</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/company">Back to Companies</A>
</div>
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<form class="card" onSubmit={submit}>
<div class="field-grid-2">
<form class="rounded-xl border border-gray-200 bg-white shadow-sm" onSubmit={submit}>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Company Name *</label>
<input value={form().companyName} onInput={(e) => setField('companyName', e.currentTarget.value)} />
@ -100,8 +100,8 @@ export default function CreateCompanyPage() {
</div>
</div>
<div class="actions" style="justify-content:flex-end">
<A class="btn" href="/admin/company">Cancel</A>
<button class="btn navy" type="submit" disabled={saving()}>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/company">Cancel</A>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Creating...' : 'Create Company'}
</button>
</div>

View file

@ -142,10 +142,10 @@ export default function CouponPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Coupon Management</h1>
<p class="page-subtitle">Reusable coupon codes for package checkout</p>
<h1 class="text-2xl font-bold text-gray-900">Coupon Management</h1>
<p class="mt-1 text-sm text-gray-500">Reusable coupon codes for package checkout</p>
</div>
</div>
@ -168,9 +168,9 @@ export default function CouponPage() {
</div>
<Show when={activeTab() === 'list'}>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Code</th>
@ -179,7 +179,7 @@ export default function CouponPage() {
<th>Value</th>
<th>Max Uses</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -202,15 +202,15 @@ export default function CouponPage() {
<td style="color:#475569">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</td>
<td style="color:#475569">{item.usage_limit != null ? item.usage_limit : '—'}</td>
<td>
<span class={`status-chip ${item.is_active ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${item.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => startEdit(item)}>Edit</button>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => startEdit(item)}>Edit</button>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
@ -229,10 +229,10 @@ export default function CouponPage() {
</Show>
<Show when={activeTab() === 'create'}>
<section class="card" style="max-width:520px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Coupon' : 'Create Coupon'}</h2>
<Show when={formError()}>
<div class="error-box" style="margin-bottom:12px">{formError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -256,7 +256,7 @@ export default function CouponPage() {
placeholder="e.g. 10% off for companies"
/>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
@ -278,7 +278,7 @@ export default function CouponPage() {
/>
</div>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Min Order Amount ()</label>
<input
@ -320,11 +320,11 @@ export default function CouponPage() {
</div>
</div>
<div class="actions">
<button class="btn navy" type="submit" disabled={saving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Coupon' : 'Save Coupon')}
</button>
<Show when={form().id}>
<button type="button" class="btn" onClick={resetForm}>Cancel Edit</button>
<button type="button" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>

View file

@ -136,10 +136,10 @@ export default function CreditPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Credit Management</h1>
<p class="page-subtitle">Audit TraceCoin balances and adjust credits</p>
<h1 class="text-2xl font-bold text-gray-900">Credit Management</h1>
<p class="mt-1 text-sm text-gray-500">Audit TraceCoin balances and adjust credits</p>
</div>
</div>
@ -161,7 +161,7 @@ export default function CreditPage() {
{/* Balance & Ledger Tab */}
<Show when={activeTab() === 'ledger'}>
<div style="display:flex;flex-direction:column;gap:24px">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Search Account Balance</h2>
<div style="display:flex;gap:10px">
<input
@ -173,7 +173,7 @@ export default function CreditPage() {
style="flex:1;padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
/>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={handleSearch}
disabled={searchLoading()}
>
@ -181,7 +181,7 @@ export default function CreditPage() {
</button>
</div>
<Show when={searchError()}>
<div class="error-box" style="margin-top:10px">{searchError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:10px">{searchError()}</div>
</Show>
</section>
@ -193,14 +193,14 @@ export default function CreditPage() {
<p style="font-size:12px;color:#bfdbfe;margin:8px 0 0">User: {searchedUserId()}</p>
</div>
<section class="card" style="padding:20px;overflow:hidden">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:20px;overflow:hidden">
<h3 style="margin:0 0 16px;font-size:15px;font-weight:700;color:#0f172a">TraceCoin Ledger</h3>
<Show when={ledger().length === 0}>
<p style="text-align:center;padding:32px;color:#94a3b8;font-style:italic">No transactions found for this account.</p>
</Show>
<Show when={ledger().length > 0}>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Type</th>
@ -248,7 +248,7 @@ export default function CreditPage() {
{/* Reward / Deduct Tab */}
<Show when={activeTab() === 'adjust'}>
<section class="card" style="max-width:460px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Reward or Adjust TraceCoins</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Use this to reward TraceCoins for a valid support case, process a refund, or correct a balance manually.
@ -259,7 +259,7 @@ export default function CreditPage() {
</div>
</Show>
<Show when={adjError()}>
<div class="error-box" style="margin-bottom:14px">{adjError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{adjError()}</div>
</Show>
<form onSubmit={handleAdjust} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -272,7 +272,7 @@ export default function CreditPage() {
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;box-sizing:border-box"
/>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Amount</label>
<input
@ -316,7 +316,7 @@ export default function CreditPage() {
/>
</div>
<div>
<button class="btn navy" type="submit" disabled={adjLoading()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={adjLoading()}>
{adjLoading() ? 'Adjusting...' : 'Apply Adjustment'}
</button>
</div>
@ -327,13 +327,13 @@ export default function CreditPage() {
{/* Reconcile Tab */}
<Show when={activeTab() === 'reconcile'}>
<div style="display:flex;flex-direction:column;gap:20px">
<section class="card" style="max-width:460px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:460px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700;color:#1e293b">Ledger Reconciliation</h2>
<Show when={reconError()}>
<div class="error-box" style="margin-bottom:14px">{reconError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{reconError()}</div>
</Show>
<form onSubmit={handleReconcile} style="display:flex;flex-direction:column;gap:14px">
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Start Date</label>
<input
@ -356,7 +356,7 @@ export default function CreditPage() {
</div>
</div>
<div>
<button class="btn navy" type="submit" disabled={reconLoading()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={reconLoading()}>
{reconLoading() ? 'Running...' : 'Run Reconciliation'}
</button>
</div>
@ -368,9 +368,9 @@ export default function CreditPage() {
<p style="color:#16a34a;font-weight:600;font-size:14px">No discrepancies found.</p>
</Show>
<Show when={(reconResults()?.length ?? 0) > 0}>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>User ID</th>

View file

@ -36,14 +36,14 @@ export default function CustomerPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Customer Management</h1>
<p class="page-subtitle">Manage all customer accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Customer Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all customer accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function CustomerPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function CustomerPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -207,13 +207,13 @@ export default function DepartmentPage() {
return (
<AdminShell>
{/* Header */}
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Departments</h1>
<p class="page-subtitle">Manage organization departments</p>
<h1 class="text-2xl font-bold text-gray-900">Departments</h1>
<p class="mt-1 text-sm text-gray-500">Manage organization departments</p>
</div>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={() => {
setShowCreate((v) => !v);
setCreateError('');
@ -225,9 +225,9 @@ export default function DepartmentPage() {
{/* Create form */}
<Show when={showCreate()}>
<section class="card role-form-section">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<form onSubmit={handleCreate}>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Name *</label>
<input
@ -249,12 +249,12 @@ export default function DepartmentPage() {
</div>
</div>
<Show when={createError()}>
<p class="error-box" style="margin-top:10px">{createError()}</p>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:10px">{createError()}</p>
</Show>
<div class="actions" style="margin-top:16px">
<button
type="button"
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
setShowCreate(false);
setCreateError('');
@ -262,7 +262,7 @@ export default function DepartmentPage() {
>
Cancel
</button>
<button type="submit" class="btn navy" disabled={creating()}>
<button type="submit" class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={creating()}>
{creating() ? 'Saving...' : 'Save'}
</button>
</div>
@ -288,19 +288,19 @@ export default function DepartmentPage() {
{/* Action error */}
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
{/* Table */}
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Created At</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -334,16 +334,16 @@ export default function DepartmentPage() {
<td style="color:#475569">{item.description || '—'}</td>
<td style="color:#475569">{fmtDate(item.createdAt || item.created_at)}</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => startEdit(item)}
>
Edit
</button>
<Show when={tab() === 'active'}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={busy() === item.id}
onClick={() => handleArchive(item.id)}
>
@ -352,7 +352,7 @@ export default function DepartmentPage() {
</Show>
<Show when={tab() === 'archived'}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={busy() === item.id}
onClick={() => handleRestore(item.id)}
>
@ -360,7 +360,7 @@ export default function DepartmentPage() {
</button>
</Show>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={busy() === item.id}
onClick={() => handleDelete(item.id, deptLabel(item))}
>
@ -373,7 +373,7 @@ export default function DepartmentPage() {
<Show when={editingId() === item.id}>
<tr>
<td colspan="4" style="background:#f8fafc;padding:16px">
<div class="field-grid-2" style="margin-bottom:10px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" style="margin-bottom:10px">
<div class="field">
<label>Name *</label>
<input
@ -393,14 +393,14 @@ export default function DepartmentPage() {
</div>
</div>
<Show when={editError()}>
<p class="error-box" style="margin-bottom:8px">{editError()}</p>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:8px">{editError()}</p>
</Show>
<div class="actions">
<button class="btn" type="button" onClick={cancelEdit}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={cancelEdit}>
Cancel
</button>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
type="button"
disabled={saving()}
onClick={() => handleUpdate(item.id)}

View file

@ -189,13 +189,13 @@ export default function DesignationPage() {
return (
<AdminShell>
{/* Header */}
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Designations</h1>
<p class="page-subtitle">Manage job designations</p>
<h1 class="text-2xl font-bold text-gray-900">Designations</h1>
<p class="mt-1 text-sm text-gray-500">Manage job designations</p>
</div>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={() => {
setShowCreate((v) => !v);
setCreateError('');
@ -210,9 +210,9 @@ export default function DesignationPage() {
{/* Create form */}
<Show when={showCreate()}>
<section class="card role-form-section">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<form onSubmit={handleCreate}>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Name *</label>
<input
@ -250,12 +250,12 @@ export default function DesignationPage() {
</div>
</div>
<Show when={createError()}>
<p class="error-box" style="margin-top:10px">{createError()}</p>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:10px">{createError()}</p>
</Show>
<div class="actions" style="margin-top:16px">
<button
type="button"
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
setShowCreate(false);
setCreateError('');
@ -265,7 +265,7 @@ export default function DesignationPage() {
</button>
<button
type="submit"
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
disabled={creating() || (departments() ?? []).length === 0}
>
{creating() ? 'Saving...' : 'Save'}
@ -293,19 +293,19 @@ export default function DesignationPage() {
{/* Action error */}
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
{/* Table */}
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Department</th>
<th>Description</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -339,15 +339,15 @@ export default function DesignationPage() {
<td style="color:#475569">{deptDisplay(item)}</td>
<td style="color:#475569">{item.description || '—'}</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => startEdit(item)}
>
Edit
</button>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
@ -360,7 +360,7 @@ export default function DesignationPage() {
<Show when={editingId() === item.id}>
<tr>
<td colspan="4" style="background:#f8fafc;padding:16px">
<div class="field-grid-2" style="margin-bottom:10px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" style="margin-bottom:10px">
<div class="field">
<label>Name *</label>
<input
@ -394,14 +394,14 @@ export default function DesignationPage() {
</div>
</div>
<Show when={editError()}>
<p class="error-box" style="margin-bottom:8px">{editError()}</p>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:8px">{editError()}</p>
</Show>
<div class="actions">
<button class="btn" type="button" onClick={cancelEdit}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={cancelEdit}>
Cancel
</button>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
type="button"
disabled={saving()}
onClick={() => handleUpdate(item.id)}

View file

@ -36,14 +36,14 @@ export default function DevelopersPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Developers Management</h1>
<p class="page-subtitle">Manage all developer accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Developers Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all developer accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function DevelopersPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function DevelopersPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -146,10 +146,10 @@ export default function DiscountPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Discount Management</h1>
<p class="page-subtitle">Automatic discounts applied before coupons</p>
<h1 class="text-2xl font-bold text-gray-900">Discount Management</h1>
<p class="mt-1 text-sm text-gray-500">Automatic discounts applied before coupons</p>
</div>
</div>
@ -172,9 +172,9 @@ export default function DiscountPage() {
</div>
<Show when={activeTab() === 'list'}>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
@ -183,7 +183,7 @@ export default function DiscountPage() {
<th>Type</th>
<th>Value</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -206,14 +206,14 @@ export default function DiscountPage() {
<td style="color:#475569">{item.type}</td>
<td style="color:#475569">{item.type === 'PERCENT' ? `${item.value}%` : `${item.value}`}</td>
<td>
<span class={`status-chip ${item.is_active ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${item.is_active ? 'active' : ''}`}>
{item.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={toggling() === item.id}
onClick={() => handleToggle(item)}
>
@ -232,10 +232,10 @@ export default function DiscountPage() {
</Show>
<Show when={activeTab() === 'create'}>
<section class="card" style="max-width:520px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">{form().id ? 'Edit Discount' : 'Create Discount'}</h2>
<Show when={formError()}>
<div class="error-box" style="margin-bottom:12px">{formError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -301,7 +301,7 @@ export default function DiscountPage() {
</select>
</Show>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Type</label>
<select
@ -324,11 +324,11 @@ export default function DiscountPage() {
</div>
</div>
<div class="actions">
<button class="btn navy" type="submit" disabled={saving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : (form().id ? 'Update Discount' : 'Save Discount')}
</button>
<Show when={form().id}>
<button type="button" class="btn" onClick={resetForm}>Cancel Edit</button>
<button type="button" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={resetForm}>Cancel Edit</button>
</Show>
</div>
</form>

View file

@ -174,9 +174,9 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
return (
<AdminShell>
{/* Header */}
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Internal User Management</h1>
<h1 class="text-2xl font-bold text-gray-900">Internal User Management</h1>
</div>
</div>
@ -207,19 +207,19 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
{/* ===== LIST VIEW ===== */}
<Show when={view() === 'list'}>
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -252,24 +252,24 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
<td style="color:#475569">{item.email}</td>
<td style="color:#475569">{roleName(item)}</td>
<td>
<span class={`status-chip${isActive(item) ? ' active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600${isActive(item) ? ' active' : ''}`}>
{isActive(item) ? 'ACTIVE' : 'INACTIVE'}
</span>
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/employees/${item.id}/edit`}>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/employees/${item.id}/edit`}>
Edit
</A>
<button
class={isActive(item) ? 'btn danger' : 'btn navy'}
class={isActive(item) ? 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors' : 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'}
disabled={toggling() === item.id}
onClick={() => handleToggleStatus(item.id, isActive(item))}
>
{toggling() === item.id ? '...' : isActive(item) ? 'Deactivate' : 'Activate'}
</button>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, employeeName(item))}
>
@ -289,9 +289,9 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
{/* ===== CREATE VIEW ===== */}
<Show when={view() === 'create'}>
<section class="card role-form-section">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<form onSubmit={handleCreate}>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Full Name *</label>
<input
@ -339,12 +339,12 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
</div>
</div>
<Show when={createError()}>
<p class="error-box" style="margin-top:10px">{createError()}</p>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:10px">{createError()}</p>
</Show>
<div class="actions" style="margin-top:16px">
<button
type="button"
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
resetCreateForm();
setView('list');
@ -352,7 +352,7 @@ export default function EmployeesPage(props: EmployeesPageProps = {}) {
>
Cancel
</button>
<button type="submit" class="btn navy" disabled={creating()}>
<button type="submit" class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={creating()}>
{creating() ? 'Creating...' : 'Create Internal User'}
</button>
</div>

View file

@ -77,28 +77,28 @@ export default function EditEmployeePage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Edit Employee</h1>
<p class="page-subtitle">Update internal employee profile and role assignment.</p>
<h1 class="text-2xl font-bold text-gray-900">Edit Employee</h1>
<p class="mt-1 text-sm text-gray-500">Update internal employee profile and role assignment.</p>
</div>
<A class="btn" href="/admin/employees">Back to Employees</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/employees">Back to Employees</A>
</div>
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<Show when={employee.loading}>
<div class="card"><p class="notice">Loading employee...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading employee...</p></div>
</Show>
<Show when={!employee.loading && !employee()}>
<div class="card"><p class="notice">Employee not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Employee not found.</p></div>
</Show>
<Show when={employee()}>
<form class="card" onSubmit={submit}>
<div class="field-grid-2">
<form class="rounded-xl border border-gray-200 bg-white shadow-sm" onSubmit={submit}>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Full Name</label>
<input value={name()} onInput={(e) => setName(e.currentTarget.value)} />
@ -120,8 +120,8 @@ export default function EditEmployeePage() {
</div>
</div>
<div class="actions" style="justify-content:flex-end">
<A class="btn" href="/admin/employees">Cancel</A>
<button class="btn navy" type="submit" disabled={saving()}>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/employees">Cancel</A>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Changes'}
</button>
</div>

View file

@ -127,7 +127,7 @@ function renderModuleContent(module: Module | null) {
<textarea rows={4} placeholder="Describe the request" style="width:100%;border:1px solid #cbd5e1;border-radius:12px;padding:10px 12px;font-size:13px;outline:none" />
</div>
<div style="display:flex;justify-content:flex-end;margin-top:12px">
<button class="btn navy" type="button">Submit</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="button">Submit</button>
</div>
</div>
);
@ -454,36 +454,36 @@ export default function ExternalDashboardManagementPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">External Dashboard Management</h1>
<p class="page-subtitle">Open one external dashboard at a time from the list below and edit it using simple tabs.</p>
<h1 class="text-2xl font-bold text-gray-900">External Dashboard Management</h1>
<p class="mt-1 text-sm text-gray-500">Open one external dashboard at a time from the list below and edit it using simple tabs.</p>
</div>
<a class="btn" href="/admin">Back to Dashboard</a>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin">Back to Dashboard</a>
</div>
<div class="admin-link-tabs" style="margin-bottom:14px">
<a class="admin-link-tab active" href="#builder">View Dashboards</a>
<a class="admin-link-tab" href="#builder">Open Builder</a>
<div class="hidden" style="margin-bottom:14px">
<a class="hidden" href="#builder">View Dashboards</a>
<a class="hidden" href="#builder">Open Builder</a>
</div>
<div id="builder" />
{/* ---------- List View ---------- */}
<Show when={!selected()}>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h2 class="page-title" style="font-size:20px">External Dashboard List</h2>
<p class="page-subtitle">Choose one external role dashboard to open in the builder, or create a new one.</p>
<h2 class="text-2xl font-bold text-gray-900" style="font-size:20px">External Dashboard List</h2>
<p class="mt-1 text-sm text-gray-500">Choose one external role dashboard to open in the builder, or create a new one.</p>
</div>
<button class="btn primary" onClick={createDashboard} disabled={creating()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={createDashboard} disabled={creating()}>
{creating() ? 'Creating...' : 'Create External Dashboard'}
</button>
</div>
<Show when={error()}><div class="error-box">{error()}</div></Show>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table list-table-soft-head">
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm list-table-soft-head">
<thead>
<tr>
<th>Role</th>
@ -491,7 +491,7 @@ export default function ExternalDashboardManagementPage() {
<th>Status</th>
<th>Version</th>
<th>Modules</th>
<th class="align-right">Action</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
@ -506,12 +506,12 @@ export default function ExternalDashboardManagementPage() {
<tr>
<td style="font-weight:600;color:#0f172a">{d.roleKey || 'No role selected'}</td>
<td style="color:#475569">{d.title}</td>
<td><span class={`status-chip ${d.status === 'published' ? 'active' : ''}`}>{d.status}</span></td>
<td><span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${d.status === 'published' ? 'active' : ''}`}>{d.status}</span></td>
<td style="color:#64748b">v{d.version}</td>
<td style="color:#64748b">{d.modules.length} pages</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => void openDashboard(d.id)}>View Builder</button>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => void openDashboard(d.id)}>View Builder</button>
</div>
</td>
</tr>
@ -531,14 +531,14 @@ export default function ExternalDashboardManagementPage() {
<p>Edit menu labels, page names, and opening page for this role dashboard.</p>
</div>
<div class="builder-header-actions">
<button class="btn" onClick={() => setSelectedId('')}>Back to List</button>
<button class="btn primary" onClick={saveSelected} disabled={saving()}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setSelectedId('')}>Back to List</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={saveSelected} disabled={saving()}>
{saving() ? 'Saving...' : 'Save Dashboard'}
</button>
</div>
</div>
<Show when={error()}><div class="error-box">{error()}</div></Show>
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
{/* Tab bar */}
<div class="builder-tab-bar">
@ -551,7 +551,7 @@ export default function ExternalDashboardManagementPage() {
{/* Overview */}
<Show when={activeTab() === 'overview'}>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="field">
<label>Role</label>
<select
@ -592,7 +592,7 @@ export default function ExternalDashboardManagementPage() {
<div class="builder-section">
<div class="sub-card-header">
<h4>Menu Items</h4>
<button class="btn orange" onClick={addSidebarItem}>Add Menu Item</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSidebarItem}>Add Menu Item</button>
</div>
<For each={selected()!.sidebar}>
{(item, idx) => (
@ -624,7 +624,7 @@ export default function ExternalDashboardManagementPage() {
/>
Show
</label>
<button class="btn danger" onClick={() => removeSidebarItem(item.key)}>Remove</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSidebarItem(item.key)}>Remove</button>
</div>
)}
</For>
@ -639,7 +639,7 @@ export default function ExternalDashboardManagementPage() {
<div class="builder-section">
<div class="sub-card-header">
<h4>Pages</h4>
<button class="btn orange" onClick={addModule}>Add Page</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addModule}>Add Page</button>
</div>
<For each={selected()!.modules}>
{(module) => (
@ -675,7 +675,7 @@ export default function ExternalDashboardManagementPage() {
<textarea rows={2} value={module.summary} onInput={(e) => update({ modules: selected()!.modules.map((m) => m.key === module.key ? { ...m, summary: e.currentTarget.value } : m) })} />
</div>
<div style="display:flex;justify-content:flex-end">
<button class="btn danger" onClick={() => removeModule(module.key)}>Remove Page</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeModule(module.key)}>Remove Page</button>
</div>
</div>
)}
@ -719,7 +719,7 @@ export default function ExternalDashboardManagementPage() {
>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px">
<span class="meta-chip">Role: {livePreviewRoleKey()}</span>
<a class="btn" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
</div>
<iframe
src={livePreviewUrl()}

View file

@ -7,7 +7,7 @@ export default function ExternalRoleManagementAliasPage() {
onMount(() => navigate('/admin/runtime-roles', { replace: true }));
return (
<AdminShell>
<div class="card"><p class="notice">Redirecting to external role management...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to external role management...</p></div>
</AdminShell>
);
}

View file

@ -5,5 +5,5 @@ import AdminShell from '~/components/AdminShell';
export default function AdjustCreditAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/credit', { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to credit management...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to credit management...</p></div></AdminShell>;
}

View file

@ -5,5 +5,5 @@ import AdminShell from '~/components/AdminShell';
export default function ReconcileAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/ledger', { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to ledger management...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to ledger management...</p></div></AdminShell>;
}

View file

@ -36,14 +36,14 @@ export default function FitnessTrainersPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Fitness Trainer Management</h1>
<p class="page-subtitle">Manage all fitness trainer accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Fitness Trainer Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all fitness trainer accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function FitnessTrainersPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function FitnessTrainersPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -36,14 +36,14 @@ export default function GraphicDesignersPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Graphic Designer Management</h1>
<p class="page-subtitle">Manage all graphic designer accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Graphic Designer Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all graphic designer accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function GraphicDesignersPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function GraphicDesignersPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -7,7 +7,7 @@ export default function HelpAliasPage() {
onMount(() => navigate('/admin/support', { replace: true }));
return (
<AdminShell>
<div class="card"><p class="notice">Redirecting to support management...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to support management...</p></div>
</AdminShell>
);
}

View file

@ -5,14 +5,14 @@ export default function HelpArticlePage() {
const params = useParams();
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Help Article</h1>
<p class="page-subtitle">Legacy help article route preserved for migration compatibility.</p>
<h1 class="text-2xl font-bold text-gray-900">Help Article</h1>
<p class="mt-1 text-sm text-gray-500">Legacy help article route preserved for migration compatibility.</p>
</div>
<A class="btn" href="/admin/support">Back to Support</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/support">Back to Support</A>
</div>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice" style="margin:0">
Article ID: <strong>{params.id}</strong>
</p>

View file

@ -5,14 +5,14 @@ export default function HelpSupportBridgePage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">Support Bridge</h1>
<p class="page-subtitle" style="margin-top:8px">
<h1 class="text-2xl font-bold text-gray-900">Support Bridge</h1>
<p class="mt-1 text-sm text-gray-500" style="margin-top:8px">
This legacy help bridge now routes through the unified Support Management module.
</p>
</div>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="actions">
<A class="btn navy" href="/admin/support">Open Support Management</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/support">Open Support Management</A>
</div>
</section>
</AdminShell>

View file

@ -1,25 +1,165 @@
import { A } from '@solidjs/router';
import AdminShell from '~/components/AdminShell';
import {
Users, Building2, Briefcase, Clock, Ticket, Receipt,
Package, Megaphone, Shield, UserCog, FormInput, LayoutDashboard,
BadgeCheck, ChevronRight, TrendingUp,
} from 'lucide-solid';
import type { Component } from 'solid-js';
export default function AdminIndex() {
type KpiCard = {
label: string;
value: string;
href: string;
icon: Component<any>;
bg: string;
fg: string;
};
type ControlLink = {
label: string;
href: string;
desc: string;
icon: Component<any>;
accent: string;
};
const KPI: KpiCard[] = [
{ label: 'Total Users', value: '—', href: '/admin/users', icon: Users, bg: 'bg-blue-50', fg: 'text-blue-600' },
{ label: 'Companies', value: '—', href: '/admin/company', icon: Building2, bg: 'bg-violet-50', fg: 'text-violet-600' },
{ label: 'Open Jobs', value: '—', href: '/admin/jobs', icon: Briefcase, bg: 'bg-amber-50', fg: 'text-amber-600' },
{ label: 'Pending Approvals', value: '—', href: '/admin/approval', icon: Clock, bg: 'bg-orange-50', fg: 'text-orange-600' },
{ label: 'Open Tickets', value: '—', href: '/admin/support', icon: Ticket, bg: 'bg-rose-50', fg: 'text-rose-600' },
{ label: 'Invoices', value: '—', href: '/admin/invoice', icon: Receipt, bg: 'bg-teal-50', fg: 'text-teal-600' },
{ label: 'Active Orders', value: '—', href: '/admin/order', icon: Package, bg: 'bg-sky-50', fg: 'text-sky-600' },
{ label: 'Active Leads', value: '—', href: '/admin/leads', icon: Megaphone, bg: 'bg-pink-50', fg: 'text-pink-600' },
];
const CONTROLS: ControlLink[] = [
{
label: 'Internal Roles',
href: '/admin/roles',
desc: 'Define permissions and access levels for internal staff roles.',
icon: Shield,
accent: 'bg-blue-50 text-blue-600',
},
{
label: 'External Roles',
href: '/admin/runtime-roles',
desc: 'Configure modules, credit rules, and capabilities per external role.',
icon: UserCog,
accent: 'bg-violet-50 text-violet-600',
},
{
label: 'Onboarding Flows',
href: '/admin/onboarding-schemas',
desc: 'Build schema-driven onboarding forms for each external role.',
icon: FormInput,
accent: 'bg-amber-50 text-amber-600',
},
{
label: 'External Dashboards',
href: '/admin/external-dashboard-management',
desc: 'Configure the runtimeConfig, sidebar, and widgets per external role.',
icon: LayoutDashboard,
accent: 'bg-teal-50 text-teal-600',
},
{
label: 'Internal Dashboards',
href: '/admin/internal-dashboard-management',
desc: 'Design home widgets and KPI panels for internal staff dashboards.',
icon: LayoutDashboard,
accent: 'bg-orange-50 text-orange-600',
},
{
label: 'Approval Queue',
href: '/admin/approval',
desc: 'Review, approve, or reject pending platform action requests.',
icon: BadgeCheck,
accent: 'bg-rose-50 text-rose-600',
},
];
export default function AdminDashboard() {
return (
<AdminShell>
<h1 class="page-title">Admin Builder</h1>
<p class="page-subtitle">Pick what you want to configure.</p>
<section class="card">
<div class="actions">
<A class="btn primary" href="/admin/runtime-roles/new">Create Role</A>
<A class="btn" href="/admin/runtime-roles">Manage Roles</A>
<div class="space-y-8">
{/* ── Page header ── */}
<div class="flex items-center justify-between">
<div>
<h1 class="text-[22px] font-bold tracking-tight text-gray-900">Platform Overview</h1>
<p class="mt-0.5 text-sm text-gray-400">Real-time snapshot of the Nxtgauge platform.</p>
</div>
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-white px-3 py-1.5 shadow-sm">
<TrendingUp class="h-4 w-4 text-emerald-500" />
<span class="text-xs font-medium text-gray-500">Live data</span>
</div>
</div>
<div class="actions">
<A class="btn primary" href="/admin/role-ui-configs/new">Create Dashboard</A>
<A class="btn" href="/admin/role-ui-configs">Manage Dashboards</A>
{/* ── KPI grid ── */}
<div class="grid grid-cols-2 gap-4 lg:grid-cols-4">
{KPI.map((kpi) => {
const Icon = kpi.icon;
return (
<A
href={kpi.href}
class="group relative overflow-hidden rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-gray-200 hover:shadow-md"
>
{/* Subtle background circle */}
<div class="absolute -right-4 -top-4 h-20 w-20 rounded-full bg-gray-50 opacity-60" />
<div class="relative flex flex-col gap-4">
{/* Icon */}
<div class={`flex h-10 w-10 items-center justify-center rounded-xl ${kpi.bg}`}>
<Icon class={`h-5 w-5 ${kpi.fg}`} />
</div>
{/* Metric */}
<div>
<p class="text-[28px] font-bold leading-none tracking-tight text-gray-900">{kpi.value}</p>
<p class="mt-1.5 text-[13px] font-medium text-gray-400">{kpi.label}</p>
</div>
</div>
{/* Hover arrow */}
<ChevronRight class="absolute bottom-4 right-4 h-4 w-4 text-gray-300 opacity-0 transition-opacity group-hover:opacity-100" />
</A>
);
})}
</div>
<div class="actions">
<A class="btn primary" href="/admin/onboarding-schemas/new">Create Onboarding</A>
<A class="btn" href="/admin/onboarding-schemas">Manage Onboarding</A>
{/* ── Control Plane ── */}
<div>
<div class="mb-4 flex items-center justify-between">
<h2 class="text-[15px] font-semibold text-gray-700">Control Plane</h2>
<span class="rounded-full border border-gray-200 bg-white px-2.5 py-0.5 text-[11px] font-medium text-gray-400 shadow-sm">
{CONTROLS.length} modules
</span>
</div>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{CONTROLS.map((item) => {
const Icon = item.icon;
return (
<A
href={item.href}
class="group flex items-start gap-4 rounded-2xl border border-gray-100 bg-white p-5 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-orange-100 hover:shadow-md"
>
<div class={`mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${item.accent}`}>
<Icon class="h-4 w-4" />
</div>
<div class="min-w-0 flex-1">
<p class="text-[14px] font-semibold text-gray-800 group-hover:text-[#fd6216] transition-colors">{item.label}</p>
<p class="mt-0.5 text-[12px] leading-relaxed text-gray-400">{item.desc}</p>
</div>
<ChevronRight class="mt-1 h-4 w-4 shrink-0 text-gray-300 transition-transform group-hover:translate-x-0.5 group-hover:text-orange-400" />
</A>
);
})}
</div>
</div>
</section>
</div>
</AdminShell>
);
}

View file

@ -413,35 +413,35 @@ export default function InternalDashboardManagementPage() {
// ---------- List view ----------
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Internal Dashboard Management</h1>
<p class="page-subtitle">Open one internal dashboard at a time from the list below and edit it using simple tabs.</p>
<h1 class="text-2xl font-bold text-gray-900">Internal Dashboard Management</h1>
<p class="mt-1 text-sm text-gray-500">Open one internal dashboard at a time from the list below and edit it using simple tabs.</p>
</div>
<a class="btn" href="/admin">Back to Dashboard</a>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin">Back to Dashboard</a>
</div>
<div class="admin-link-tabs" style="margin-bottom:14px">
<a class="admin-link-tab active" href="#builder">View Dashboards</a>
<a class="admin-link-tab" href="#builder">Open Builder</a>
<div class="hidden" style="margin-bottom:14px">
<a class="hidden" href="#builder">View Dashboards</a>
<a class="hidden" href="#builder">Open Builder</a>
</div>
<div id="builder" />
<Show when={!selected()}>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h2 class="page-title" style="font-size:20px">Internal Dashboard List</h2>
<p class="page-subtitle">Choose one internal dashboard to open in the builder, or create a new dashboard for an internal role.</p>
<h2 class="text-2xl font-bold text-gray-900" style="font-size:20px">Internal Dashboard List</h2>
<p class="mt-1 text-sm text-gray-500">Choose one internal dashboard to open in the builder, or create a new dashboard for an internal role.</p>
</div>
<button class="btn navy" onClick={createDashboard} disabled={creating()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={createDashboard} disabled={creating()}>
{creating() ? 'Creating...' : 'Create Internal Dashboard'}
</button>
</div>
<Show when={error()}><div class="error-box">{error()}</div></Show>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table list-table-soft-head">
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm list-table-soft-head">
<thead>
<tr>
<th>Role</th>
@ -449,7 +449,7 @@ export default function InternalDashboardManagementPage() {
<th>Dashboard</th>
<th>Status</th>
<th>Version</th>
<th class="align-right">Action</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
@ -465,11 +465,11 @@ export default function InternalDashboardManagementPage() {
<td style="color:#475569">{d.roleName || 'Not linked'}</td>
<td style="color:#475569">{d.roleId || 'Not linked'}</td>
<td style="font-weight:600;color:#0f172a">{d.title}</td>
<td><span class={`status-chip ${d.status === 'published' ? 'active' : ''}`}>{d.status}</span></td>
<td><span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${d.status === 'published' ? 'active' : ''}`}>{d.status}</span></td>
<td style="color:#64748b">v{d.version}</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => void openDashboard(d.id)}>View Builder</button>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => void openDashboard(d.id)}>View Builder</button>
</div>
</td>
</tr>
@ -489,14 +489,14 @@ export default function InternalDashboardManagementPage() {
<p>Manage menu items, sections, tabs, form fields, and summary cards from one place.</p>
</div>
<div class="builder-header-actions">
<button class="btn" onClick={() => setSelectedId('')}>Back to List</button>
<button class="btn primary" onClick={saveSelected} disabled={saving()}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => setSelectedId('')}>Back to List</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={saveSelected} disabled={saving()}>
{saving() ? 'Saving...' : 'Save Dashboard'}
</button>
</div>
</div>
<Show when={error()}><div class="error-box">{error()}</div></Show>
<Show when={error()}><div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div></Show>
{/* Tab bar */}
<div class="builder-tab-bar">
@ -513,7 +513,7 @@ export default function InternalDashboardManagementPage() {
{/* Overview */}
<Show when={activeTab() === 'overview'}>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Team Role</label>
<select
@ -548,7 +548,7 @@ export default function InternalDashboardManagementPage() {
<div class="builder-section">
<div class="sub-card-header">
<h4>Menu</h4>
<button class="btn orange" onClick={addSidebarItem}>Add Menu Item</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSidebarItem}>Add Menu Item</button>
</div>
<For each={selected()!.sidebar}>
{(item, idx) => (
@ -572,7 +572,7 @@ export default function InternalDashboardManagementPage() {
/>
Show
</label>
<button class="btn danger" onClick={() => removeSidebarItem(item.key)}>Remove</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSidebarItem(item.key)}>Remove</button>
</div>
)}
</For>
@ -586,7 +586,7 @@ export default function InternalDashboardManagementPage() {
<Show when={activeTab() === 'sections'}>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<h3 style="margin:0;font-size:15px;font-weight:700">Sections</h3>
<button class="btn orange" onClick={addSection}>Add Section</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={addSection}>Add Section</button>
</div>
<For each={selected()!.sections}>
{(section) => (
@ -597,14 +597,14 @@ export default function InternalDashboardManagementPage() {
onInput={(e) => updateSection(section.id, { title: e.currentTarget.value })}
placeholder="Section title"
/>
<button class="btn danger" onClick={() => removeSection(section.id)}>Remove</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeSection(section.id)}>Remove</button>
</div>
{/* Tabs */}
<div class="sub-card">
<div class="sub-card-header">
<h4>Tabs and Form Fields</h4>
<button class="btn orange" onClick={() => addTab(section.id)}>Add Tab</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addTab(section.id)}>Add Tab</button>
</div>
<For each={section.tabs}>
{(tab) => (
@ -615,10 +615,10 @@ export default function InternalDashboardManagementPage() {
onInput={(e) => updateTab(section.id, tab.id, { title: e.currentTarget.value })}
placeholder="Tab title"
/>
<button class="btn danger" onClick={() => removeTab(section.id, tab.id)}>Remove</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeTab(section.id, tab.id)}>Remove</button>
</div>
<div style="display:flex;justify-content:flex-end;margin-bottom:8px">
<button class="btn orange" onClick={() => addField(section.id, tab.id)}>Add Field</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addField(section.id, tab.id)}>Add Field</button>
</div>
<For each={tab.fields}>
{(field) => (
@ -651,7 +651,7 @@ export default function InternalDashboardManagementPage() {
/>
Required
</label>
<button class="btn danger" onClick={() => removeField(section.id, tab.id, field.id)}>Remove</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeField(section.id, tab.id, field.id)}>Remove</button>
</div>
)}
</For>
@ -670,7 +670,7 @@ export default function InternalDashboardManagementPage() {
<div class="sub-card" style="margin-top:8px">
<div class="sub-card-header">
<h4>Widgets</h4>
<button class="btn orange" onClick={() => addWidget(section.id)}>Add Widget</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors orange" onClick={() => addWidget(section.id)}>Add Widget</button>
</div>
<For each={section.widgets}>
{(widget) => (
@ -679,7 +679,7 @@ export default function InternalDashboardManagementPage() {
<input value={widget.metric} onInput={(e) => updateWidget(section.id, widget.id, { metric: e.currentTarget.value })} placeholder="Metric value e.g. 42" />
<textarea rows={2} value={widget.description || ''} onInput={(e) => updateWidget(section.id, widget.id, { description: e.currentTarget.value })} placeholder="Widget description" />
<div style="display:flex;justify-content:flex-end">
<button class="btn danger" style="font-size:12px;padding:5px 10px" onClick={() => removeWidget(section.id, widget.id)}>Remove Widget</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" style="font-size:12px;padding:5px 10px" onClick={() => removeWidget(section.id, widget.id)}>Remove Widget</button>
</div>
</div>
)}
@ -728,7 +728,7 @@ export default function InternalDashboardManagementPage() {
<Show when={selected()!.roleId}>
<span class="meta-chip">Role ID: {selected()!.roleId}</span>
</Show>
<a class="btn" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={livePreviewUrl()} target="_blank" rel="noreferrer">Open Full Page Preview</a>
</div>
<iframe
src={livePreviewUrl()}

View file

@ -7,7 +7,7 @@ export default function InternalRoleManagementAliasPage() {
onMount(() => navigate('/admin/roles', { replace: true }));
return (
<AdminShell>
<div class="card"><p class="notice">Redirecting to internal role management...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to internal role management...</p></div>
</AdminShell>
);
}

View file

@ -32,10 +32,10 @@ export default function InvoicePage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Invoice Management</h1>
<p class="page-subtitle">View and download all platform invoices.</p>
<h1 class="text-2xl font-bold text-gray-900">Invoice Management</h1>
<p class="mt-1 text-sm text-gray-500">View and download all platform invoices.</p>
</div>
</div>
@ -49,9 +49,9 @@ export default function InvoicePage() {
/>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Invoice #</th>
@ -61,7 +61,7 @@ export default function InvoicePage() {
<th>Tax ()</th>
<th>Status</th>
<th>Date</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -83,16 +83,16 @@ export default function InvoicePage() {
<td style="color:#475569">{item.total != null ? (item.total / 100).toFixed(2) : '—'}</td>
<td style="color:#475569">{item.tax != null ? (item.tax / 100).toFixed(2) : '—'}</td>
<td>
<span class={`status-chip ${item.status === 'PAID' || item.status === 'ISSUED' ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${item.status === 'PAID' || item.status === 'ISSUED' ? 'active' : ''}`}>
{item.status || '—'}
</span>
</td>
<td style="color:#475569">{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<Show when={item.download_url || item.pdf_url}>
<a
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
href={item.download_url || item.pdf_url}
target="_blank"
rel="noopener noreferrer"

View file

@ -63,10 +63,10 @@ export default function JobsPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Jobs Management</h1>
<p class="page-subtitle">Review live company job postings</p>
<h1 class="text-2xl font-bold text-gray-900">Jobs Management</h1>
<p class="mt-1 text-sm text-gray-500">Review live company job postings</p>
</div>
</div>
@ -87,9 +87,9 @@ export default function JobsPage() {
</select>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
@ -98,7 +98,7 @@ export default function JobsPage() {
<th>Rate</th>
<th>Location</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -137,10 +137,10 @@ export default function JobsPage() {
<span class={statusChipClass(job.status)}>{job.status || '—'}</span>
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/jobs/${job.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/jobs/${job.id}`}>View</A>
<Show when={job.status === 'PENDING_APPROVAL'}>
<A class="btn" href="/admin/approval">Review</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/approval">Review</A>
</Show>
</div>
</td>

View file

@ -51,25 +51,25 @@ export default function JobDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Job Management</h1>
<p class="page-subtitle">Review one live backend job in the same detail-first style as other admin modules.</p>
<h1 class="text-2xl font-bold text-gray-900">Job Management</h1>
<p class="mt-1 text-sm text-gray-500">Review one live backend job in the same detail-first style as other admin modules.</p>
</div>
<A class="btn" href="/admin/jobs">Back to Jobs</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/jobs">Back to Jobs</A>
</div>
<Show when={job.loading}>
<div class="card"><p class="notice">Loading job...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading job...</p></div>
</Show>
<Show when={!job.loading && !job()}>
<div class="card"><p class="notice">Job not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Job not found.</p></div>
</Show>
<Show when={job()}>
<section class="card">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<p class="hint">Title</p>
<p style="margin:6px 0 0;font-weight:700;color:#0f172a">{job()!.title || '—'}</p>

View file

@ -215,10 +215,10 @@ export default function KbPage(props: KbPageProps = {}) {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Knowledge Base</h1>
<p class="page-subtitle">Manage help articles and categories</p>
<h1 class="text-2xl font-bold text-gray-900">Knowledge Base</h1>
<p class="mt-1 text-sm text-gray-500">Manage help articles and categories</p>
</div>
</div>
@ -249,22 +249,22 @@ export default function KbPage(props: KbPageProps = {}) {
{/* Categories Tab */}
<Show when={tab() === 'categories'}>
<div class="page-actions" style="margin-bottom:16px">
<div class="mb-6 flex items-start justify-between gap-4" style="margin-bottom:16px">
<div />
<button class="btn navy" onClick={() => setShowCatForm(!showCatForm())}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => setShowCatForm(!showCatForm())}>
{showCatForm() ? 'Cancel' : 'Add Category'}
</button>
</div>
<Show when={actionError()}>
<div class="error-box" style="margin-bottom:12px">{actionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{actionError()}</div>
</Show>
<Show when={showCatForm()}>
<section class="card" style="margin-bottom:16px;max-width:480px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;max-width:480px">
<h2 style="margin:0 0 16px;font-size:15px;font-weight:700">New Category</h2>
<Show when={catError()}>
<div class="error-box" style="margin-bottom:10px">{catError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{catError()}</div>
</Show>
<form onSubmit={handleAddCategory} style="display:flex;flex-direction:column;gap:12px">
<div class="field">
@ -300,7 +300,7 @@ export default function KbPage(props: KbPageProps = {}) {
/>
</div>
<div>
<button class="btn navy" type="submit" disabled={catSaving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={catSaving()}>
{catSaving() ? 'Saving...' : 'Create Category'}
</button>
</div>
@ -308,15 +308,15 @@ export default function KbPage(props: KbPageProps = {}) {
</section>
</Show>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Slug</th>
<th>Article Count</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -338,10 +338,10 @@ export default function KbPage(props: KbPageProps = {}) {
<td style="color:#475569;font-family:monospace;font-size:13px">{cat.slug}</td>
<td style="color:#475569">{cat.article_count ?? '—'}</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => startEditCat(cat)}>Edit</button>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => startEditCat(cat)}>Edit</button>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deletingCatId() === cat.id}
onClick={() => deleteCategory(cat.id, cat.name)}
>
@ -354,7 +354,7 @@ export default function KbPage(props: KbPageProps = {}) {
<tr>
<td colspan="4" style="background:#f8fafc;padding:14px">
<Show when={editCatError()}>
<div class="error-box" style="margin-bottom:8px">{editCatError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:8px">{editCatError()}</div>
</Show>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
<div class="field">
@ -376,10 +376,10 @@ export default function KbPage(props: KbPageProps = {}) {
/>
</div>
<div style="display:flex;gap:8px">
<button class="btn navy" disabled={editCatSaving()} onClick={() => saveEditCat(cat.id)}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={editCatSaving()} onClick={() => saveEditCat(cat.id)}>
{editCatSaving() ? 'Saving...' : 'Save'}
</button>
<button class="btn" onClick={cancelEditCat}>Cancel</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={cancelEditCat}>Cancel</button>
</div>
</div>
</td>
@ -398,7 +398,7 @@ export default function KbPage(props: KbPageProps = {}) {
{/* Articles Tab */}
<Show when={tab() === 'articles'}>
<Show when={articleActionError()}>
<div class="error-box" style="margin-bottom:12px">{articleActionError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{articleActionError()}</div>
</Show>
<div style="margin-bottom:16px">
@ -411,16 +411,16 @@ export default function KbPage(props: KbPageProps = {}) {
/>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Category</th>
<th>Status</th>
<th>Updated At</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -440,7 +440,7 @@ export default function KbPage(props: KbPageProps = {}) {
<td style="font-weight:600;color:#0f172a">{article.title}</td>
<td style="color:#475569">{article.category || '—'}</td>
<td>
<span class={`status-chip ${article.status === 'PUBLISHED' ? 'active' : 'draft'}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${article.status === 'PUBLISHED' ? 'active' : 'draft'}`}>
{article.status || '—'}
</span>
</td>
@ -448,10 +448,10 @@ export default function KbPage(props: KbPageProps = {}) {
{article.updated_at ? new Date(article.updated_at).toLocaleString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/kb/articles/${article.id}/edit`}>Edit</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/kb/articles/${article.id}/edit`}>Edit</A>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deletingArticleId() === article.id}
onClick={() => deleteArticle(article.id, article.title)}
>
@ -471,10 +471,10 @@ export default function KbPage(props: KbPageProps = {}) {
{/* Create Article Tab */}
<Show when={tab() === 'create-article'}>
<section class="card" style="max-width:640px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:640px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Article</h2>
<Show when={artError()}>
<div class="error-box" style="margin-bottom:14px">{artError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{artError()}</div>
</Show>
<form onSubmit={handleCreateArticle} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -536,7 +536,7 @@ export default function KbPage(props: KbPageProps = {}) {
</select>
</div>
<div>
<button class="btn navy" type="submit" disabled={artSaving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={artSaving()}>
{artSaving() ? 'Creating...' : 'Create Article'}
</button>
</div>

View file

@ -33,30 +33,30 @@ export default function KbArticleDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">KB Article Detail</h1>
<p class="page-subtitle">Metadata and safe content preview for this article.</p>
<h1 class="text-2xl font-bold text-gray-900">KB Article Detail</h1>
<p class="mt-1 text-sm text-gray-500">Metadata and safe content preview for this article.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/kb/articles">Back to Articles</A>
<A class="btn primary" href={`/admin/kb/articles/${params.id}/edit`}>Edit Article</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/kb/articles">Back to Articles</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/kb/articles/${params.id}/edit`}>Edit Article</A>
</div>
</div>
<Show when={article.loading}>
<section class="card"><p class="notice">Loading article...</p></section>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading article...</p></section>
</Show>
<Show when={!article.loading && !article()}>
<section class="card"><p class="notice">Article not found.</p></section>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Article not found.</p></section>
</Show>
<Show when={article()}>
<div class="detail-layout">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:10px">Metadata</h2>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item"><p class="kv-label">Title</p><p class="kv-value">{article()!.title || '—'}</p></div>
<div class="kv-item"><p class="kv-label">Status</p><p class="kv-value">{article()!.status || '—'}</p></div>
<div class="kv-item"><p class="kv-label">Slug</p><p class="kv-value">{article()!.slug || '—'}</p></div>
@ -66,7 +66,7 @@ export default function KbArticleDetailPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:10px">Content</h2>
<pre class="json" style="max-height:720px;white-space:pre-wrap">{article()!.content || article()!.body || 'No content'}</pre>
</section>

View file

@ -75,28 +75,28 @@ export default function KbArticleEditPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Edit KB Article</h1>
<p class="page-subtitle">Update article metadata, status, and content.</p>
<h1 class="text-2xl font-bold text-gray-900">Edit KB Article</h1>
<p class="mt-1 text-sm text-gray-500">Update article metadata, status, and content.</p>
</div>
<div class="page-actions-right">
<A class="btn" href={`/admin/kb/articles/${params.id}`}>Back to Detail</A>
<A class="btn" href="/admin/kb/articles">Back to Articles</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/kb/articles/${params.id}`}>Back to Detail</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/kb/articles">Back to Articles</A>
</div>
</div>
<Show when={article.loading}>
<section class="card"><p class="notice">Loading article...</p></section>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading article...</p></section>
</Show>
<Show when={!article.loading && !article()}>
<section class="card"><p class="notice">Article not found.</p></section>
<section class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Article not found.</p></section>
</Show>
<Show when={article() && loaded()}>
<form class="card" onSubmit={save}>
<div class="field-grid-2">
<form class="rounded-xl border border-gray-200 bg-white shadow-sm" onSubmit={save}>
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Title</label>
<input value={title()} onInput={(e) => setTitle(e.currentTarget.value)} required />
@ -127,7 +127,7 @@ export default function KbArticleEditPage() {
</Show>
<div class="actions" style="justify-content:flex-end">
<button class="btn primary" type="submit" disabled={saving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Article'}
</button>
</div>

View file

@ -49,10 +49,10 @@ export default function LeadsPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Leads Management</h1>
<p class="page-subtitle">View all requirements and lead requests from customers.</p>
<h1 class="text-2xl font-bold text-gray-900">Leads Management</h1>
<p class="mt-1 text-sm text-gray-500">View all requirements and lead requests from customers.</p>
</div>
</div>
@ -92,9 +92,9 @@ export default function LeadsPage() {
</Show>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
@ -102,7 +102,7 @@ export default function LeadsPage() {
<th>Budget</th>
<th>Location</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -133,13 +133,13 @@ export default function LeadsPage() {
</td>
<td style="color:#475569">{item.location || '—'}</td>
<td>
<span class={`status-chip ${(item.status === 'ACTIVE' || item.status === 'OPEN') ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${(item.status === 'ACTIVE' || item.status === 'OPEN') ? 'active' : ''}`}>
{item.status || '—'}
</span>
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/leads/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/leads/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -73,24 +73,24 @@ export default function LeadDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Lead Detail</h1>
<p class="page-subtitle">Review one lead and its linked requirement identifiers.</p>
<h1 class="text-2xl font-bold text-gray-900">Lead Detail</h1>
<p class="mt-1 text-sm text-gray-500">Review one lead and its linked requirement identifiers.</p>
</div>
<A class="btn" href="/admin/leads">Back to Leads</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/leads">Back to Leads</A>
</div>
<Show when={bundle.loading}>
<div class="card"><p class="notice">Loading lead...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading lead...</p></div>
</Show>
<Show when={!bundle.loading && !lead()}>
<div class="card"><p class="notice">Lead not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Lead not found.</p></div>
</Show>
<Show when={lead()}>
<section class="card" style="padding:20px">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:20px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item">
<p class="kv-label">Lead ID</p>
<p class="kv-value">{lead()!.id}</p>
@ -128,7 +128,7 @@ export default function LeadDetailPage() {
</div>
<div class="actions">
<Show when={requirementId()}>
<A class="btn" href={`/admin/jobs/${requirementId()}`}>Open Linked Requirement/Job</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/jobs/${requirementId()}`}>Open Linked Requirement/Job</A>
</Show>
</div>
@ -139,7 +139,7 @@ export default function LeadDetailPage() {
</div>
<p style="margin:0;font-size:17px;font-weight:700;color:#111827">{requirement()!.title || 'Untitled Requirement'}</p>
<p class="notice" style="margin-top:8px">{requirement()!.description || 'No description available.'}</p>
<div class="field-grid-2" style="margin-top:12px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2" style="margin-top:12px">
<div class="kv-item">
<p class="kv-label">Customer</p>
<p class="kv-value">{requirement()!.customerName || 'Customer'}</p>

View file

@ -56,16 +56,16 @@ export default function LedgerPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Ledger Management</h1>
<p class="page-subtitle">Platform financial ledger</p>
<h1 class="text-2xl font-bold text-gray-900">Ledger Management</h1>
<p class="mt-1 text-sm text-gray-500">Platform financial ledger</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Type</th>

View file

@ -36,14 +36,14 @@ export default function MakeupArtistPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Makeup Artist Management</h1>
<p class="page-subtitle">Manage all makeup artist accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Makeup Artist Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all makeup artist accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function MakeupArtistPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function MakeupArtistPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -107,32 +107,32 @@ export default function ModulesPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Module Registry</h1>
<p class="page-subtitle">Manage internal module definitions and activation state.</p>
<h1 class="text-2xl font-bold text-gray-900">Module Registry</h1>
<p class="mt-1 text-sm text-gray-500">Manage internal module definitions and activation state.</p>
</div>
<button class="btn primary" onClick={() => openModal()}>Add Module</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => openModal()}>Add Module</button>
</div>
<Show when={error() && !isModalOpen()}>
<div class="card"><p class="error-note">{error()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="error-note">{error()}</p></div>
</Show>
<Show when={modules.loading}>
<div class="card"><p class="notice">Loading modules...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading modules...</p></div>
</Show>
<div class="card">
<div class="table-wrap">
<table class="list-table">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Key</th>
<th>Description</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -150,10 +150,10 @@ export default function ModulesPage() {
{item.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td class="align-right">
<div class="table-actions">
<button class="btn" onClick={() => openModal(item)}>Edit</button>
<button class="btn danger" onClick={() => removeModule(item.id)}>Delete</button>
<td class="text-right">
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => openModal(item)}>Edit</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" onClick={() => removeModule(item.id)}>Delete</button>
</div>
</td>
</tr>
@ -211,8 +211,8 @@ export default function ModulesPage() {
</Show>
<div class="actions" style="justify-content:flex-end">
<button type="button" class="btn" onClick={closeModal}>Cancel</button>
<button type="submit" class="btn primary" disabled={submitting()}>
<button type="button" class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={closeModal}>Cancel</button>
<button type="submit" class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={submitting()}>
{editing() ? 'Save Changes' : 'Create'}
</button>
</div>

View file

@ -83,13 +83,13 @@ export default function NotificationsPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Notifications</h1>
<p class="page-subtitle">Approval outcomes and action-required updates</p>
<h1 class="text-2xl font-bold text-gray-900">Notifications</h1>
<p class="mt-1 text-sm text-gray-500">Approval outcomes and action-required updates</p>
</div>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={handleMarkAllRead}
disabled={markingAllRead()}
>
@ -115,16 +115,16 @@ export default function NotificationsPage() {
</button>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Title</th>
<th>Message</th>
<th>Event Type</th>
<th>Created At</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -148,10 +148,10 @@ export default function NotificationsPage() {
{item.created_at ? new Date(item.created_at).toLocaleString() : '—'}
</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<Show when={item.read_at === null}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={markingId() === item.id}
onClick={() => handleMarkRead(item.id)}
>
@ -175,7 +175,7 @@ export default function NotificationsPage() {
<Show when={cursor() && !loading()}>
<div style="margin-top:12px">
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => load(cursor(), false)}
>
Load More

View file

@ -7,7 +7,7 @@ export default function OnboardingManagementAliasPage() {
onMount(() => navigate('/admin/onboarding-schemas', { replace: true }));
return (
<AdminShell>
<div class="card"><p class="notice">Redirecting to onboarding management...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to onboarding management...</p></div>
</AdminShell>
);
}

View file

@ -135,34 +135,34 @@ export default function OnboardingSchemaDetailPage() {
<AdminShell>
<OnboardingManagementTabs />
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Onboarding Management</h1>
<p class="page-subtitle">Open one onboarding form at a time, check if it is published, then update the role, questions, steps, and final success message.</p>
<h1 class="text-2xl font-bold text-gray-900">Onboarding Management</h1>
<p class="mt-1 text-sm text-gray-500">Open one onboarding form at a time, check if it is published, then update the role, questions, steps, and final success message.</p>
</div>
<div class="page-actions-right">
<button class="btn" type="button" disabled={saving()} onClick={() => void persist(true)}>
<div class="flex items-center gap-2">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={saving()} onClick={() => void persist(true)}>
Save Active Version
</button>
<A class="btn" href="/admin/onboarding-schemas">Back to Onboarding Management</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/onboarding-schemas">Back to Onboarding Management</A>
</div>
</div>
<Show when={schema.loading}>
<div class="card"><p class="notice">Loading onboarding flow...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading onboarding flow...</p></div>
</Show>
<Show when={!schema.loading && !schema()}>
<div class="card"><p class="notice">Onboarding flow not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Onboarding flow not found.</p></div>
</Show>
<Show when={schema() && loaded()}>
<>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:16px">
<div class="card"><p class="kv-label">Status</p><p class="kv-value">{schema()?.is_active ? 'PUBLISHED' : 'DRAFT'}</p></div>
<div class="card"><p class="kv-label">Version</p><p class="kv-value">{schema()?.schema_json?.version || 1}</p></div>
<div class="card"><p class="kv-label">Steps</p><p class="kv-value">{stepCount()}</p></div>
<div class="card"><p class="kv-label">Questions</p><p class="kv-value">{selectedFields().length}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Status</p><p class="kv-value">{schema()?.is_active ? 'PUBLISHED' : 'DRAFT'}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Version</p><p class="kv-value">{schema()?.schema_json?.version || 1}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Steps</p><p class="kv-value">{stepCount()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Questions</p><p class="kv-value">{selectedFields().length}</p></div>
</div>
<OnboardingFlowBuilder

View file

@ -93,29 +93,29 @@ export default function OnboardingSchemasPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Onboarding Management</h1>
<p class="page-subtitle">Manage onboarding flows, role assignments, and previewable step groups for external users.</p>
<h1 class="text-2xl font-bold text-gray-900">Onboarding Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage onboarding flows, role assignments, and previewable step groups for external users.</p>
</div>
<A class="btn navy" href="/admin/onboarding-schemas/new">Create Onboarding Flow</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/onboarding-schemas/new">Create Onboarding Flow</A>
</div>
<OnboardingManagementTabs />
<Show when={deleteError()}>
<div class="error-box">{deleteError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
</Show>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0">
<h2 style="margin:0;font-size:17px;font-weight:700">Onboarding Flows</h2>
<Show when={!schemas.loading}>
<span style="font-size:13px;color:#64748b">{schemas()?.length || 0} flows</span>
</Show>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Flow</th>
@ -123,7 +123,7 @@ export default function OnboardingSchemasPage() {
<th>Steps</th>
<th>Version</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -144,13 +144,13 @@ export default function OnboardingSchemasPage() {
<td style="color:#475569">{schema.stepCount}</td>
<td style="color:#475569">v{schema.version}</td>
<td>
<span class={`status-chip ${schema.status === 'PUBLISHED' ? 'active' : ''}`}>{schema.status}</span>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${schema.status === 'PUBLISHED' ? 'active' : ''}`}>{schema.status}</span>
</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/onboarding-schemas/${schema.roleId || schema.id}`} title="Open Flow">👁</A>
<div class="flex items-center justify-end gap-1">
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/onboarding-schemas/${schema.roleId || schema.id}`} title="Open Flow">👁</A>
<button
class="action-icon-btn danger"
class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm"
disabled={deleting() === schema.id}
onClick={() => handleDelete(schema.id, schema.title)}
title="Delete Flow"

View file

@ -107,18 +107,18 @@ export default function NewOnboardingSchemaPage() {
<AdminShell>
<OnboardingManagementTabs />
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Create Onboarding Flow</h1>
<p class="page-subtitle">Create one onboarding form at a time. Pick the role, choose the questions, set the steps, and write the final success message.</p>
<h1 class="text-2xl font-bold text-gray-900">Create Onboarding Flow</h1>
<p class="mt-1 text-sm text-gray-500">Create one onboarding form at a time. Pick the role, choose the questions, set the steps, and write the final success message.</p>
</div>
<A class="btn" href="/admin/onboarding-schemas">Back to Onboarding Management</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/onboarding-schemas">Back to Onboarding Management</A>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:16px">
<div class="card"><p class="kv-label">Role</p><p class="kv-value">{roleKey().replace(/_/g, ' ').toUpperCase()}</p></div>
<div class="card"><p class="kv-label">Steps</p><p class="kv-value">{stepCount()}</p></div>
<div class="card"><p class="kv-label">Questions</p><p class="kv-value">{selectedFields().length}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Role</p><p class="kv-value">{roleKey().replace(/_/g, ' ').toUpperCase()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Steps</p><p class="kv-value">{stepCount()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="kv-label">Questions</p><p class="kv-value">{selectedFields().length}</p></div>
</div>
<OnboardingFlowBuilder

View file

@ -70,10 +70,10 @@ export default function OrderPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Order Management</h1>
<p class="page-subtitle">TraceCoin package purchase orders</p>
<h1 class="text-2xl font-bold text-gray-900">Order Management</h1>
<p class="mt-1 text-sm text-gray-500">TraceCoin package purchase orders</p>
</div>
</div>
@ -87,9 +87,9 @@ export default function OrderPage() {
/>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Order #</th>
@ -124,7 +124,7 @@ export default function OrderPage() {
<td style="color:#475569">{formatAmount(item)}</td>
<td>
<span
class="status-chip"
class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"
style={`${statusStyle(item.status)};padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600;border:1px solid`}
>
{item.status || '—'}

View file

@ -36,14 +36,14 @@ export default function PhotographerPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Photographer Management</h1>
<p class="page-subtitle">Manage all photographer accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Photographer Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all photographer accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function PhotographerPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function PhotographerPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/photographer/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/photographer/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -50,27 +50,27 @@ export default function PhotographerDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Photographer Detail</h1>
<p class="page-subtitle">View profile snapshot and account metadata for one photographer.</p>
<h1 class="text-2xl font-bold text-gray-900">Photographer Detail</h1>
<p class="mt-1 text-sm text-gray-500">View profile snapshot and account metadata for one photographer.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/photographer">Back to Photographer List</A>
<A class="btn" href={`/admin/users/details/${params.id}`}>Open User Detail</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/photographer">Back to Photographer List</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/details/${params.id}`}>Open User Detail</A>
</div>
</div>
<Show when={profile.loading}>
<div class="card"><p class="notice">Loading photographer...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading photographer...</p></div>
</Show>
<Show when={!profile.loading && !profile()}>
<div class="card"><p class="notice">Photographer not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Photographer not found.</p></div>
</Show>
<Show when={profile()}>
<section class="card">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item">
<p class="kv-label">Name</p>
<p class="kv-value">{name()}</p>

View file

@ -150,10 +150,10 @@ export default function PricingPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Pricing Management</h1>
<p class="page-subtitle">Create and manage TraceCoin packages</p>
<h1 class="text-2xl font-bold text-gray-900">Pricing Management</h1>
<p class="mt-1 text-sm text-gray-500">Create and manage TraceCoin packages</p>
</div>
</div>
@ -177,9 +177,9 @@ export default function PricingPage() {
{/* Packages list tab */}
<Show when={view() === 'packages'}>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
@ -188,7 +188,7 @@ export default function PricingPage() {
<th>Price ()</th>
<th>Bonus (%)</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -212,15 +212,15 @@ export default function PricingPage() {
<td style="color:#475569">{(pkg.price_inr / 100).toFixed(2)}</td>
<td style="color:#475569">{pkg.bonus_percentage != null ? `${pkg.bonus_percentage}%` : '—'}</td>
<td>
<span class={`status-chip ${pkg.is_active ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${pkg.is_active ? 'active' : ''}`}>
{pkg.is_active ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<button class="btn" onClick={() => startEdit(pkg)}>Edit</button>
<div class="flex items-center justify-end gap-1">
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={() => startEdit(pkg)}>Edit</button>
<button
class={pkg.is_active ? 'btn danger' : 'btn navy'}
class={pkg.is_active ? 'inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors' : 'inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors'}
disabled={togglingId() === pkg.id}
onClick={() => toggleActive(pkg)}
>
@ -233,7 +233,7 @@ export default function PricingPage() {
<tr>
<td colspan="7" style="background:#f8fafc;padding:16px">
<Show when={editError()}>
<div class="error-box" style="margin-bottom:10px">{editError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:10px">{editError()}</div>
</Show>
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:flex-end">
<div class="field">
@ -264,10 +264,10 @@ export default function PricingPage() {
/>
</div>
<div style="display:flex;gap:8px">
<button class="btn navy" disabled={editSaving()} onClick={() => saveEdit(pkg.id)}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={editSaving()} onClick={() => saveEdit(pkg.id)}>
{editSaving() ? 'Saving...' : 'Save'}
</button>
<button class="btn" onClick={cancelEdit}>Cancel</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" onClick={cancelEdit}>Cancel</button>
</div>
</div>
</td>
@ -285,10 +285,10 @@ export default function PricingPage() {
{/* Create Package tab */}
<Show when={view() === 'create'}>
<section class="card" style="max-width:480px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:480px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">New Package</h2>
<Show when={cError()}>
<div class="error-box" style="margin-bottom:14px">{cError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{cError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -349,7 +349,7 @@ export default function PricingPage() {
/>
</div>
<div>
<button class="btn navy" type="submit" disabled={cSaving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={cSaving()}>
{cSaving() ? 'Creating...' : 'Create Package'}
</button>
</div>

View file

@ -6,5 +6,5 @@ export default function ProfileAliasPage() {
const navigate = useNavigate();
const params = useParams();
onMount(() => navigate(`/admin/users/details/${params.id}`, { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to user profile detail...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to user profile detail...</p></div></AdminShell>;
}

View file

@ -46,14 +46,14 @@ export default function ReportPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Report Management</h1>
<p class="page-subtitle">View platform analytics and generate reports.</p>
<h1 class="text-2xl font-bold text-gray-900">Report Management</h1>
<p class="mt-1 text-sm text-gray-500">View platform analytics and generate reports.</p>
</div>
</div>
<section class="card" style="margin-bottom:16px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">Date Range</h2>
<form onSubmit={handleLoad} style="display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap">
<div>
@ -76,40 +76,40 @@ export default function ReportPage() {
style="padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px"
/>
</div>
<button class="btn navy" type="submit" disabled={loading()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={loading()}>
{loading() ? 'Loading...' : 'Load Report'}
</button>
</form>
<Show when={error()}>
<div class="error-box" style="margin-top:12px">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-top:12px">{error()}</div>
</Show>
</section>
<Show when={userReport() || revenueReport()}>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:16px;margin-bottom:16px">
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.total_users ?? '—'}</p>
</div>
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">New Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.new_users ?? '—'}</p>
</div>
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Active Users</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{userReport()?.active_users ?? '—'}</p>
</div>
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Revenue</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">
{revenueReport()?.total_revenue != null ? `${(revenueReport()!.total_revenue! / 100).toFixed(2)}` : ''}
</p>
</div>
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">Total Orders</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_orders ?? '—'}</p>
</div>
<div class="card" style="text-align:center">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="text-align:center">
<p style="margin:0 0 8px;font-size:13px;color:#64748b;font-weight:600">TraceCoins Sold</p>
<p style="margin:0;font-size:28px;font-weight:700;color:#0f172a">{revenueReport()?.total_tracecoins_sold ?? '—'}</p>
</div>

View file

@ -40,25 +40,25 @@ export default function RequirementDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Requirement Request</h1>
<p class="page-subtitle">Review full requirement request details before approval action.</p>
<h1 class="text-2xl font-bold text-gray-900">Requirement Request</h1>
<p class="mt-1 text-sm text-gray-500">Review full requirement request details before approval action.</p>
</div>
<A class="btn" href="/admin/approval">Back to Approval Management</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/approval">Back to Approval Management</A>
</div>
<Show when={requirement.loading}>
<div class="card"><p class="notice">Loading requirement...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading requirement...</p></div>
</Show>
<Show when={!requirement.loading && !requirement()}>
<div class="card"><p class="notice">Requirement not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Requirement not found.</p></div>
</Show>
<Show when={requirement()}>
<section class="card">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<p class="hint">Title</p>
<p style="margin:6px 0 0;font-weight:700;color:#0f172a">{requirement()!.title || '—'}</p>

View file

@ -77,18 +77,18 @@ export default function ResponsesPage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">Responses</h1>
<p class="page-subtitle">Track professional responses and move them through shortlist, accept, or reject states.</p>
<h1 class="text-2xl font-bold text-gray-900">Responses</h1>
<p class="mt-1 text-sm text-gray-500">Track professional responses and move them through shortlist, accept, or reject states.</p>
</div>
<Show when={payload.loading}>
<div class="card"><p class="notice">Loading responses...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading responses...</p></div>
</Show>
<Show when={error()}>
<div class="card"><p class="error-note">{error()}</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="error-note">{error()}</p></div>
</Show>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<Show when={!payload.loading && responses().length === 0} fallback={
<div class="list-grid" style="grid-template-columns:1fr;gap:12px">
<For each={responses()}>
@ -106,11 +106,11 @@ export default function ResponsesPage() {
</div>
<div class="actions">
<Show when={row.status === 'SUBMITTED'}>
<button class="btn" disabled={busyId() === row.id} onClick={() => transition(row.id, 'SHORTLISTED')}>Shortlist</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" disabled={busyId() === row.id} onClick={() => transition(row.id, 'SHORTLISTED')}>Shortlist</button>
</Show>
<Show when={row.status === 'SUBMITTED' || row.status === 'SHORTLISTED'}>
<button class="btn primary" disabled={busyId() === row.id} onClick={() => transition(row.id, 'ACCEPTED')}>Accept</button>
<button class="btn danger" disabled={busyId() === row.id} onClick={() => transition(row.id, 'REJECTED')}>Reject</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" disabled={busyId() === row.id} onClick={() => transition(row.id, 'ACCEPTED')}>Accept</button>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" disabled={busyId() === row.id} onClick={() => transition(row.id, 'REJECTED')}>Reject</button>
</Show>
</div>
</div>

View file

@ -120,10 +120,10 @@ export default function ReviewPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Review Management</h1>
<p class="page-subtitle">Moderate platform reviews</p>
<h1 class="text-2xl font-bold text-gray-900">Review Management</h1>
<p class="mt-1 text-sm text-gray-500">Moderate platform reviews</p>
</div>
</div>
@ -155,9 +155,9 @@ export default function ReviewPage() {
style="padding:8px 12px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;min-width:320px"
/>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Reviewer</th>
@ -165,7 +165,7 @@ export default function ReviewPage() {
<th>Rating</th>
<th>Title</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -196,17 +196,17 @@ export default function ReviewPage() {
</td>
<td style="color:#475569">{item.title || '—'}</td>
<td>
<span class={`status-chip ${isPublished ? 'active' : ''}`}
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${isPublished ? 'active' : ''}`}
style={isPublished ? '' : 'background:#f1f5f9;color:#475569;border-color:#e2e8f0'}
>
{isPublished ? 'Published' : 'Hidden'}
</span>
</td>
<td>
<div class="table-actions">
<div class="flex items-center justify-end gap-1">
<Show when={isPublished}>
<button
class="btn"
class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'HIDDEN')}
>
@ -215,7 +215,7 @@ export default function ReviewPage() {
</Show>
<Show when={!isPublished}>
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
disabled={toggling() === item.id}
onClick={() => handleUpdateStatus(item, 'PUBLISHED')}
>
@ -236,10 +236,10 @@ export default function ReviewPage() {
</Show>
<Show when={activeTab() === 'create'}>
<section class="card" style="max-width:520px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:520px">
<h2 style="margin:0 0 20px;font-size:16px;font-weight:700">Create Review</h2>
<Show when={formError()}>
<div class="error-box" style="margin-bottom:12px">{formError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -306,7 +306,7 @@ export default function ReviewPage() {
/>
</div>
<div class="actions">
<button class="btn navy" type="submit" disabled={saving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Review'}
</button>
</div>

View file

@ -5,5 +5,5 @@ import AdminShell from '~/components/AdminShell';
export default function RoleModulesAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/role-ui-configs', { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to role module configuration...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to role module configuration...</p></div></AdminShell>;
}

View file

@ -13,7 +13,7 @@ export default function EditRoleUiConfigRedirectPage() {
return (
<AdminShell>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Redirecting to External Dashboard Management...</p>
</section>
</AdminShell>

View file

@ -77,17 +77,17 @@ export default function RoleUiConfigsViewPage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">External Dashboard Management</h1>
<p class="page-subtitle">Read-only view of the currently published external dashboard and runtime role configuration.</p>
<h1 class="text-2xl font-bold text-gray-900">External Dashboard Management</h1>
<p class="mt-1 text-sm text-gray-500">Read-only view of the currently published external dashboard and runtime role configuration.</p>
</div>
<ExternalRoleTabs roleKey={roleKey()} />
<div style="display:grid;grid-template-columns:420px minmax(0,1fr);gap:16px">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="list-header">
<h2 style="margin:0">External Dashboard Roles</h2>
<A class="btn" href="/admin/roles">Open Roles</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles">Open Roles</A>
</div>
<p class="notice" style="margin-top:2px">
Select a role to inspect its published dashboard modules, onboarding assignment, and permissions.
@ -124,31 +124,31 @@ export default function RoleUiConfigsViewPage() {
</section>
<section style="display:flex;flex-direction:column;gap:12px">
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="list-header">
<div>
<h2 style="margin:0">Dashboard Detail</h2>
<p class="notice" style="margin-top:3px">This page is the safe inspector for published external dashboard and role configuration.</p>
</div>
<Show when={selected()}>
<A class="btn" href={`/admin/runtime-roles/${encodeURIComponent(selected()!.roleKey)}`}>Open Role Detail</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/runtime-roles/${encodeURIComponent(selected()!.roleKey)}`}>Open Role Detail</A>
</Show>
</div>
</div>
<Show when={selected.loading}>
<div class="card"><p class="notice">Loading config detail...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading config detail...</p></div>
</Show>
<Show when={!selected.loading && !selected() && roleKey()}>
<div class="card"><p class="notice">No runtime role found for "{roleKey()}".</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">No runtime role found for "{roleKey()}".</p></div>
</Show>
<Show when={!selected.loading && !selected() && !roleKey()}>
<div class="card"><p class="notice">Select an external role to view its current dashboard configuration.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Select an external role to view its current dashboard configuration.</p></div>
</Show>
<Show when={selected()}>
<div style="display:flex;flex-direction:column;gap:12px">
<section class="card">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="kv-item"><p class="kv-label">Role Key</p><p class="kv-value">{selected()!.roleKey}</p></div>
<div class="kv-item"><p class="kv-label">Display Name</p><p class="kv-value">{selected()!.displayName}</p></div>
<div class="kv-item"><p class="kv-label">Vertical</p><p class="kv-value">{selected()!.vertical || '—'}</p></div>
@ -174,7 +174,7 @@ export default function RoleUiConfigsViewPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 8px">Enabled Modules</h2>
<Show when={selected()!.enabledModules.length > 0} fallback={<p class="notice">No modules configured.</p>}>
<div style="display:flex;flex-wrap:wrap;gap:8px">
@ -183,14 +183,14 @@ export default function RoleUiConfigsViewPage() {
</Show>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 8px">Permission Matrix</h2>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Module</th>
{permissionActions.map((action) => <th class="align-right">{action}</th>)}
{permissionActions.map((action) => <th class="text-right">{action}</th>)}
</tr>
</thead>
<tbody>
@ -198,7 +198,7 @@ export default function RoleUiConfigsViewPage() {
<tr>
<td>{moduleKey}</td>
{permissionActions.map((action) => (
<td class="align-right">
<td class="text-right">
<input type="checkbox" checked={(selected()!.permissions[moduleKey] || []).includes(action)} disabled />
</td>
))}
@ -209,12 +209,12 @@ export default function RoleUiConfigsViewPage() {
</div>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 8px">Feature Limits</h2>
<pre class="json">{JSON.stringify(selected()!.featureLimits || {}, null, 2)}</pre>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin:0 0 8px">Published Runtime Config</h2>
<pre class="json">{JSON.stringify(selected(), null, 2)}</pre>
</section>

View file

@ -11,7 +11,7 @@ export default function CreateRoleUiConfigRedirectPage() {
return (
<AdminShell>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Redirecting to External Dashboard Management...</p>
</section>
</AdminShell>

View file

@ -112,38 +112,38 @@ export default function EditInternalRolePage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Edit Internal Role</h1>
<p class="page-subtitle">Update role name, access areas, and permissions.</p>
<h1 class="text-2xl font-bold text-gray-900">Edit Internal Role</h1>
<p class="mt-1 text-sm text-gray-500">Update role name, access areas, and permissions.</p>
</div>
<A class="btn" href={`/admin/roles/${params.id}`}>Back to Role</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/roles/${params.id}`}>Back to Role</A>
</div>
<nav class="admin-link-tabs" aria-label="Role Management Navigation">
<A class="admin-link-tab active" href="/admin/roles">Internal Roles</A>
<A class="admin-link-tab" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="admin-link-tab" href="/admin/onboarding-schemas">Onboarding Schemas</A>
<nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="hidden" href="/admin/onboarding-schemas">Onboarding Schemas</A>
</nav>
<Show when={data.loading}>
<div class="card"><p class="notice">Loading role details...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading role details...</p></div>
</Show>
<Show when={data.error}>
<div class="error-box">Failed to load role. Check that the backend is running.</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">Failed to load role. Check that the backend is running.</div>
</Show>
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<Show when={!data.loading && data()}>
{/* Role Details */}
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Role Basics</h3>
<p>Update the role name and description.</p>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Role Name <span style="color:#ef4444">*</span></label>
<input
@ -164,14 +164,14 @@ export default function EditInternalRolePage() {
</div>
{/* Module Access */}
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Area Access</h3>
<p>Select which areas this role can access.</p>
<div class="module-picker">
<div class="mt-3 flex flex-wrap gap-2">
{allModules().map((mod) => (
<button
type="button"
class={`module-chip ${assignedModules().includes(mod) ? 'selected' : ''}`}
class={`flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300 ${assignedModules().includes(mod) ? 'selected' : ''}`}
onClick={() => toggleModule(mod)}
>
<span style={`width:14px;height:14px;border-radius:3px;border:2px solid ${assignedModules().includes(mod) ? '#c2410c' : '#cbd5e1'};background:${assignedModules().includes(mod) ? '#c2410c' : '#fff'};flex-shrink:0;display:inline-block`} />
@ -183,11 +183,11 @@ export default function EditInternalRolePage() {
{/* Permission Table */}
<Show when={assignedModules().length > 0}>
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Permissions</h3>
<p>Choose what this role can do in each selected area.</p>
<div class="table-wrap">
<table class="perm-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th style="width:45%">Area</th>
@ -228,7 +228,7 @@ export default function EditInternalRolePage() {
{/* Save */}
<div style="display:flex;justify-content:flex-end;margin-top:8px">
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={handleSave}
disabled={saving() || !roleName().trim()}
>

View file

@ -43,39 +43,39 @@ export default function RoleDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Role Details</h1>
<p class="page-subtitle">View role information and assigned permissions.</p>
<h1 class="text-2xl font-bold text-gray-900">Role Details</h1>
<p class="mt-1 text-sm text-gray-500">View role information and assigned permissions.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/roles">Back to List</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles">Back to List</A>
<Show when={data()?.role}>
<A class="btn navy" href={`/admin/roles/${params.id}/edit`}>Edit Role</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/roles/${params.id}/edit`}>Edit Role</A>
</Show>
</div>
</div>
<nav class="admin-link-tabs" aria-label="Role Management Navigation">
<A class="admin-link-tab active" href="/admin/roles">Internal Roles</A>
<A class="admin-link-tab" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="admin-link-tab" href="/admin/onboarding-schemas">Onboarding Schemas</A>
<nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="hidden" href="/admin/onboarding-schemas">Onboarding Schemas</A>
</nav>
<Show when={data.loading}>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Loading role details...</p>
</div>
</Show>
<Show when={data.error}>
<div class="error-box">Failed to load role. Check that the backend is running.</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">Failed to load role. Check that the backend is running.</div>
</Show>
<Show when={data()?.role}>
{/* Role Info */}
<div class="role-detail-card">
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Role Name</label>
<input class="role-field-readonly" value={data()!.role.name} readOnly disabled />
@ -88,9 +88,9 @@ export default function RoleDetailPage() {
</div>
{/* Permission Matrix */}
<div class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="perm-table">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th style="width:45%">Name of the module</th>

View file

@ -93,29 +93,29 @@ export default function CreateInternalRolePage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Create Internal Role</h1>
<p class="page-subtitle">Create a new internal role and choose what it can access.</p>
<h1 class="text-2xl font-bold text-gray-900">Create Internal Role</h1>
<p class="mt-1 text-sm text-gray-500">Create a new internal role and choose what it can access.</p>
</div>
<A class="btn" href="/admin/roles">Back to Roles</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles">Back to Roles</A>
</div>
<nav class="admin-link-tabs" aria-label="Role Management Navigation">
<A class="admin-link-tab active" href="/admin/roles">Internal Roles</A>
<A class="admin-link-tab" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="admin-link-tab" href="/admin/onboarding-schemas">Onboarding Schemas</A>
<nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="hidden" href="/admin/onboarding-schemas">Onboarding Schemas</A>
</nav>
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
{/* Role Details */}
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Role Basics</h3>
<p>Start by giving this role a clear name.</p>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Role Name <span style="color:#ef4444">*</span></label>
<input
@ -136,18 +136,18 @@ export default function CreateInternalRolePage() {
</div>
{/* Module Access */}
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Area Access</h3>
<p>Select which areas this role can access. You can set permissions for selected areas below.</p>
<Show when={permissions.loading}>
<p class="notice">Loading available areas...</p>
</Show>
<Show when={!permissions.loading && allModules().length > 0}>
<div class="module-picker">
<div class="mt-3 flex flex-wrap gap-2">
{allModules().map((mod) => (
<button
type="button"
class={`module-chip ${assignedModules().includes(mod) ? 'selected' : ''}`}
class={`flex cursor-pointer items-center gap-1.5 rounded-full border border-gray-200 bg-white px-3 py-1.5 text-xs font-medium text-gray-600 hover:border-gray-300 ${assignedModules().includes(mod) ? 'selected' : ''}`}
onClick={() => toggleModule(mod)}
>
<span style={`width:14px;height:14px;border-radius:3px;border:2px solid ${assignedModules().includes(mod) ? '#c2410c' : '#cbd5e1'};background:${assignedModules().includes(mod) ? '#c2410c' : '#fff'};flex-shrink:0;display:inline-block`} />
@ -163,11 +163,11 @@ export default function CreateInternalRolePage() {
{/* Permission Table */}
<Show when={assignedModules().length > 0}>
<div class="role-form-section">
<div class="mb-6 rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
<h3>Permissions</h3>
<p>Choose what this role can do in each selected area.</p>
<div class="table-wrap">
<table class="perm-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th style="width:45%">Area</th>
@ -208,7 +208,7 @@ export default function CreateInternalRolePage() {
{/* Save */}
<div style="display:flex;justify-content:flex-end;margin-top:8px">
<button
class="btn navy"
class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors"
onClick={handleSave}
disabled={saving() || !roleName().trim()}
>

View file

@ -50,32 +50,32 @@ export default function InternalRolesPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Internal Role Management</h1>
<p class="page-subtitle">Manage internal employee roles and permissions from one clean list.</p>
<h1 class="text-2xl font-bold text-gray-900">Internal Role Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage internal employee roles and permissions from one clean list.</p>
</div>
<A class="btn navy" href="/admin/roles/create">Create Internal Role</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/roles/create">Create Internal Role</A>
</div>
<nav class="admin-link-tabs" aria-label="Role Management Navigation">
<A class="admin-link-tab active" href="/admin/roles">Internal Roles</A>
<A class="admin-link-tab" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="admin-link-tab" href="/admin/onboarding-schemas">Onboarding Schemas</A>
<nav class="hidden" aria-label="Role Management Navigation">
<A class="hidden" href="/admin/roles">Internal Roles</A>
<A class="hidden" href="/admin/runtime-roles">External Runtime Roles</A>
<A class="hidden" href="/admin/onboarding-schemas">Onboarding Schemas</A>
</nav>
<Show when={deleteError()}>
<div class="error-box">{deleteError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{deleteError()}</div>
</Show>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -105,11 +105,11 @@ export default function InternalRolesPage() {
</td>
<td style="color:#475569;">{role.description || 'No description added yet.'}</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/roles/${role.id}`} title="View Role">👁</A>
<A class="action-icon-btn" href={`/admin/roles/${role.id}/edit`} title="Edit Role"></A>
<div class="flex items-center justify-end gap-1">
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/roles/${role.id}`} title="View Role">👁</A>
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/roles/${role.id}/edit`} title="Edit Role"></A>
<button
class="action-icon-btn danger"
class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm"
disabled={deleting() === role.id}
onClick={() => handleDelete(role.id, role.name)}
title="Delete Role"

View file

@ -36,27 +36,27 @@ export default function RoleTemplatesPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Role Templates</h1>
<p class="page-subtitle">Starter role presets for faster internal role creation and cloning.</p>
<h1 class="text-2xl font-bold text-gray-900">Role Templates</h1>
<p class="mt-1 text-sm text-gray-500">Starter role presets for faster internal role creation and cloning.</p>
</div>
<A class="btn navy" href="/admin/roles/create">Create Internal Role</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/roles/create">Create Internal Role</A>
</div>
<section class="card" style="padding:0;overflow:hidden">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid #e2e8f0">
<h2 style="margin:0;font-size:16px">Available Templates</h2>
<span style="font-size:12px;color:#64748b">{count()} template{count() !== 1 ? 's' : ''}</span>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Code</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -74,9 +74,9 @@ export default function RoleTemplatesPage() {
<td style="color:#475569">{item.description || '—'}</td>
<td style="color:#64748b;font-family:ui-monospace,SFMono-Regular,Menlo,monospace">{item.code || '—'}</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/roles/${item.id}`}>View</A>
<A class="btn" href="/admin/roles/create">Use As Base</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/roles/${item.id}`}>View</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/roles/create">Use As Base</A>
</div>
</td>
</tr>

View file

@ -152,26 +152,26 @@ export default function EditExternalRolePage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">External Role Management</h1>
<p class="page-subtitle">
<h1 class="text-2xl font-bold text-gray-900">External Role Management</h1>
<p class="mt-1 text-sm text-gray-500">
Update this external role with simple settings: pages, permissions, onboarding form, approvals, and limits.
</p>
</div>
<div class="page-actions-right">
<A class="btn" href={`/admin/role-ui-configs?roleKey=${encodeURIComponent(roleKey())}`}>Open Inspector</A>
<A class="btn" href="/admin/runtime-roles">Back to External Roles</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/role-ui-configs?roleKey=${encodeURIComponent(roleKey())}`}>Open Inspector</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/runtime-roles">Back to External Roles</A>
</div>
</div>
<ExternalRoleTabs roleKey={roleKey()} />
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:6px">How Saving Works</h2>
<p class="notice" style="margin:0">
Saving here updates the live role settings used by the app.
@ -179,11 +179,11 @@ export default function EditExternalRolePage() {
</div>
<Show when={data.loading}>
<div class="card"><p class="notice">Loading external role...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading external role...</p></div>
</Show>
<Show when={!data.loading && !data()}>
<div class="card"><p class="notice">Role "{roleKey()}" not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Role "{roleKey()}" not found.</p></div>
</Show>
<Show when={!data.loading && data()}>

View file

@ -77,20 +77,20 @@ export default function RuntimeRolesPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">External Role Management</h1>
<p class="page-subtitle">Manage canonical external runtime roles, enabled modules, onboarding assignment, approval gates, and default runtime destinations from one place.</p>
<h1 class="text-2xl font-bold text-gray-900">External Role Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage canonical external runtime roles, enabled modules, onboarding assignment, approval gates, and default runtime destinations from one place.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/role-ui-configs">Inspector</A>
<A class="btn navy" href="/admin/runtime-roles/new">Create External Role</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/role-ui-configs">Inspector</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/runtime-roles/new">Create External Role</A>
</div>
</div>
<ExternalRoleTabs />
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e8f0">
<div>
<h2 style="margin:0;font-size:17px;font-weight:700">Published External Roles</h2>
@ -100,8 +100,8 @@ export default function RuntimeRolesPage() {
<span style="font-size:13px;color:#64748b">{roles()?.length || 0} roles</span>
</Show>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Role</th>
@ -109,7 +109,7 @@ export default function RuntimeRolesPage() {
<th>Modules</th>
<th>Schema</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -124,7 +124,7 @@ export default function RuntimeRolesPage() {
</Show>
<Show when={!roles.loading && !roles.error && (roles()?.length ?? 0) > 0}>
{roles()!.map((role) => (
<tr class={selectedRoleKey() === role.roleKey.toLowerCase() ? 'row-selected' : ''}>
<tr class={selectedRoleKey() === role.roleKey.toLowerCase() ? 'bg-orange-50' : ''}>
<td>
<div>
<p style="margin:0;font-weight:600;color:#0f172a">{role.displayName}</p>
@ -135,14 +135,14 @@ export default function RuntimeRolesPage() {
<td style="color:#475569">{role.enabledModules.length}</td>
<td style="color:#475569;font-size:12px">{role.onboardingSchemaId || '—'}</td>
<td>
<span class={`status-chip ${role.isActive ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${role.isActive ? 'active' : ''}`}>
{role.isActive ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer" title="View External Role">👁</A>
<A class="action-icon-btn" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role"></A>
<div class="flex items-center justify-end gap-1">
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} target="_blank" rel="noreferrer" title="View External Role">👁</A>
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/runtime-roles/${encodeURIComponent(role.roleKey)}`} title="Edit External Role"></A>
</div>
</td>
</tr>

View file

@ -84,20 +84,20 @@ export default function CreateExternalRolePage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Create External Role</h1>
<p class="page-subtitle">
<h1 class="text-2xl font-bold text-gray-900">Create External Role</h1>
<p class="mt-1 text-sm text-gray-500">
Create a new external role and choose what it can access in the app.
</p>
</div>
<A class="btn" href="/admin/runtime-roles">Back to External Roles</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/runtime-roles">Back to External Roles</A>
</div>
<ExternalRoleTabs />
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<ExternalRoleForm

View file

@ -5,5 +5,5 @@ import AdminShell from '~/components/AdminShell';
export default function SettingsAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/internal-dashboard-management', { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to settings/configuration...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to settings/configuration...</p></div></AdminShell>;
}

View file

@ -36,14 +36,14 @@ export default function SocialMediaManagersPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Social Media Manager Management</h1>
<p class="page-subtitle">Manage all social media manager accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Social Media Manager Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all social media manager accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function SocialMediaManagersPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function SocialMediaManagersPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -158,10 +158,10 @@ export default function SupportPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Support Management</h1>
<p class="page-subtitle">Handle platform issues and customer queries</p>
<h1 class="text-2xl font-bold text-gray-900">Support Management</h1>
<p class="mt-1 text-sm text-gray-500">Handle platform issues and customer queries</p>
</div>
</div>
@ -211,9 +211,9 @@ export default function SupportPage() {
</select>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Issue</th>
@ -222,7 +222,7 @@ export default function SupportPage() {
<th>Status</th>
<th>Requester</th>
<th>Updated At</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -260,8 +260,8 @@ export default function SupportPage() {
{item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/support/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/support/${item.id}`}>View</A>
</div>
</td>
</tr>
@ -277,7 +277,7 @@ export default function SupportPage() {
{/* Create Case Tab */}
<Show when={activeTab() === 'create'}>
<section class="card" style="max-width:600px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:600px">
<h2 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1e293b">Create Support Case</h2>
<p style="margin:0 0 20px;font-size:13px;color:#64748b">
Create an internal support record for platform issues, customer concerns, or compensation-related reviews.
@ -288,7 +288,7 @@ export default function SupportPage() {
</div>
</Show>
<Show when={createError()}>
<div class="error-box" style="margin-bottom:14px">{createError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:14px">{createError()}</div>
</Show>
<form onSubmit={handleCreate} style="display:flex;flex-direction:column;gap:14px">
<div class="field">
@ -311,7 +311,7 @@ export default function SupportPage() {
style="width:100%;padding:8px 10px;border:1px solid #e2e8f0;border-radius:6px;font-size:14px;resize:vertical;box-sizing:border-box"
/>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Type</label>
<select
@ -337,7 +337,7 @@ export default function SupportPage() {
</select>
</div>
</div>
<div class="field-grid-2">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label style="display:block;font-size:13px;font-weight:600;margin-bottom:4px">Requester Name</label>
<input
@ -358,7 +358,7 @@ export default function SupportPage() {
</div>
</div>
<div>
<button class="btn navy" type="submit" disabled={createLoading()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={createLoading()}>
{createLoading() ? 'Creating...' : 'Create Support Case'}
</button>
</div>

View file

@ -64,21 +64,21 @@ export default function TaxPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Tax Management</h1>
<p class="page-subtitle">Configure tax rates for platform transactions.</p>
<h1 class="text-2xl font-bold text-gray-900">Tax Management</h1>
<p class="mt-1 text-sm text-gray-500">Configure tax rates for platform transactions.</p>
</div>
<button class="btn navy" onClick={() => setShowForm(!showForm())}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" onClick={() => setShowForm(!showForm())}>
{showForm() ? 'Cancel' : 'Add Tax'}
</button>
</div>
<Show when={showForm()}>
<section class="card" style="margin-bottom:16px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px">
<h2 style="margin:0 0 16px;font-size:16px;font-weight:700">New Tax</h2>
<Show when={formError()}>
<div class="error-box" style="margin-bottom:12px">{formError()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" style="margin-bottom:12px">{formError()}</div>
</Show>
<form onSubmit={handleSave} style="display:flex;flex-direction:column;gap:12px;max-width:400px">
<div>
@ -114,7 +114,7 @@ export default function TaxPage() {
/>
</div>
<div>
<button class="btn navy" type="submit" disabled={saving()}>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="submit" disabled={saving()}>
{saving() ? 'Saving...' : 'Save Tax'}
</button>
</div>
@ -122,16 +122,16 @@ export default function TaxPage() {
</section>
</Show>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Rate (%)</th>
<th>Description</th>
<th>Status</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -151,15 +151,15 @@ export default function TaxPage() {
<td style="color:#475569">{item.rate}%</td>
<td style="color:#475569">{item.description || '—'}</td>
<td>
<span class={`status-chip ${item.is_active !== false ? 'active' : ''}`}>
<span class={`inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600 ${item.is_active !== false ? 'active' : ''}`}>
{item.is_active !== false ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<div class="table-actions">
<a class="btn" href={`/admin/tax/${item.id}/edit`}>Edit</a>
<div class="flex items-center justify-end gap-1">
<a class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/tax/${item.id}/edit`}>Edit</a>
<button
class="btn danger"
class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>

View file

@ -36,14 +36,14 @@ export default function TutorsPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Tutors Management</h1>
<p class="page-subtitle">Manage all tutor accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Tutors Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all tutor accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function TutorsPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function TutorsPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -53,12 +53,12 @@ async function fetchUsers(): Promise<User[]> {
function StatusBadge(props: { status: string }) {
if (props.status === 'ACTIVE') {
return <span class="status-chip active">ACTIVE</span>;
return <span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>;
}
if (props.status === 'PENDING') {
return <span class="status-chip" style="background:#f59e0b;color:#fff">PENDING</span>;
return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#f59e0b;color:#fff">PENDING</span>;
}
return <span class="status-chip">INACTIVE</span>;
return <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>;
}
export default function UsersPage() {
@ -107,10 +107,10 @@ export default function UsersPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">External User Management</h1>
<p class="page-subtitle">Manage all external platform users.</p>
<h1 class="text-2xl font-bold text-gray-900">External User Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all external platform users.</p>
</div>
</div>
@ -134,7 +134,7 @@ export default function UsersPage() {
{/* Filters */}
<Show when={activeTab() === 'list'}>
<div class="card" style="margin-bottom:16px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-bottom:16px;display:flex;gap:12px;flex-wrap:wrap;align-items:center;">
<input
type="text"
placeholder="Search by name or email..."
@ -180,9 +180,9 @@ export default function UsersPage() {
</Show>
<Show when={activeTab() === 'list'}>
<section class="card" style="padding: 0; overflow: hidden;">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>User ID</th>
@ -191,7 +191,7 @@ export default function UsersPage() {
<th>Registration Role</th>
<th>Status</th>
<th>Created</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -221,11 +221,11 @@ export default function UsersPage() {
: '—'}
</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/users/details/${item.id}`} title="Open Detail Page"></A>
<button class="action-icon-btn" type="button" onClick={() => onView(item)} title="Quick View">👁</button>
<A class="action-icon-btn" href={`/admin/users/${item.id}/edit`} title="Edit User"></A>
<button class="action-icon-btn danger" type="button" title="Delete User">🗑</button>
<div class="flex items-center justify-end gap-1">
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/users/details/${item.id}`} title="Open Detail Page"></A>
<button class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" type="button" onClick={() => onView(item)} title="Quick View">👁</button>
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/users/${item.id}/edit`} title="Edit User"></A>
<button class="rounded p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700 text-sm" type="button" title="Delete User">🗑</button>
</div>
</td>
</tr>
@ -236,11 +236,11 @@ export default function UsersPage() {
</table>
</div>
<div class="admin-pagination">
<button class="btn" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() <= 1} onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}>
Previous
</button>
<span>Page {currentPage()} of {totalPages()}</span>
<button class="btn" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" disabled={currentPage() >= totalPages()} onClick={() => setCurrentPage((p) => Math.min(totalPages(), p + 1))}>
Next
</button>
</div>
@ -248,11 +248,11 @@ export default function UsersPage() {
</Show>
<Show when={activeTab() === 'view'}>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<Show when={selectedUser()} fallback={<p class="notice">Select a user from list to view details.</p>}>
<div class="list-header">
<h2>User Details</h2>
<button class="btn" type="button" onClick={() => setActiveTab('list')}>Back To List</button>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={() => setActiveTab('list')}>Back To List</button>
</div>
<div class="grid" style="margin-top:0">
<div class="list-item">
@ -268,8 +268,8 @@ export default function UsersPage() {
<p><strong>Created:</strong> {(selectedUser()!.created_at || selectedUser()!.createdAt) ? new Date((selectedUser()!.created_at || selectedUser()!.createdAt)!).toLocaleString() : '—'}</p>
<p><strong>Role ID:</strong> {selectedUser()!.roleId || '—'}</p>
<div class="actions">
<A class="btn navy" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
<button class="btn danger" type="button">Deactivate</button>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/users/${selectedUser()!.id}/edit`}>Edit User</A>
<button class="inline-flex items-center rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-100 transition-colors" type="button">Deactivate</button>
</div>
</div>
</div>

View file

@ -114,32 +114,32 @@ export default function EditUserPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Edit User</h1>
<p class="page-subtitle">Update user profile, role assignment, and account status.</p>
<h1 class="text-2xl font-bold text-gray-900">Edit User</h1>
<p class="mt-1 text-sm text-gray-500">Update user profile, role assignment, and account status.</p>
</div>
<div class="page-actions-right">
<A class="btn" href={`/admin/users/details/${params.id}`}>View Details</A>
<A class="btn" href="/admin/users">Back to Users</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/details/${params.id}`}>View Details</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/users">Back to Users</A>
</div>
</div>
<Show when={error()}>
<div class="error-box">{error()}</div>
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error()}</div>
</Show>
<Show when={user.loading}>
<div class="card"><p class="notice">Loading user...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading user...</p></div>
</Show>
<Show when={!user.loading && !user()}>
<div class="card"><p class="notice">User not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">User not found.</p></div>
</Show>
<Show when={user()}>
<section class="card" style="max-width:900px">
<div class="field-grid-2">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="max-width:900px">
<div class="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="field">
<label>Full Name</label>
<input value={name()} onInput={(e) => setName(e.currentTarget.value)} />
@ -170,8 +170,8 @@ export default function EditUserPage() {
</div>
<div class="actions" style="justify-content:flex-end">
<button class="btn" type="button" onClick={() => navigate('/admin/users')}>Cancel</button>
<button class="btn primary" type="button" onClick={save} disabled={submitting()}>
<button class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" type="button" onClick={() => navigate('/admin/users')}>Cancel</button>
<button class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" type="button" onClick={save} disabled={submitting()}>
{submitting() ? 'Saving...' : 'Save Changes'}
</button>
</div>

View file

@ -161,27 +161,27 @@ export default function UserDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">{roleTitleLabel() ? `${roleTitleLabel()} Profile` : 'User Details'}</h1>
<p class="page-subtitle">Review account profile and role registration data with approval status.</p>
<h1 class="text-2xl font-bold text-gray-900">{roleTitleLabel() ? `${roleTitleLabel()} Profile` : 'User Details'}</h1>
<p class="mt-1 text-sm text-gray-500">Review account profile and role registration data with approval status.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/users">Back to Users</A>
<A class="btn navy" href={`/admin/users/${params.id}/edit`}>Edit User</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/users">Back to Users</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/users/${params.id}/edit`}>Edit User</A>
</div>
</div>
<Show when={bundle.loading}>
<div class="card"><p class="notice">Loading user...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading user...</p></div>
</Show>
<Show when={!bundle.loading && !user()}>
<div class="card"><p class="notice">User not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">User not found.</p></div>
</Show>
<Show when={user()}>
<>
<section class="card" style="padding:22px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:22px">
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap">
<div style="display:flex;gap:14px;align-items:center;min-width:260px">
<div class="identity-avatar">
@ -213,7 +213,7 @@ export default function UserDetailPage() {
</div>
</section>
<section class="card" style="margin-top:16px;padding:0;overflow:hidden">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-top:16px;padding:0;overflow:hidden">
<div style="padding:16px 18px;border-bottom:1px solid #e2e8f0;background:#f8fafc">
<h2 style="margin:0;font-size:18px">{roleTitleLabel() ? `${roleTitleLabel()} Registration Data` : 'Registered Roles & Profiles'}</h2>
</div>
@ -222,8 +222,8 @@ export default function UserDetailPage() {
<p class="notice">This user has no registered role profiles yet.</p>
</div>
}>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<Show when={!roleFilter()}>
@ -233,7 +233,7 @@ export default function UserDetailPage() {
<th>Verification</th>
<th>Submitted</th>
<th>Last Updated</th>
<th class="align-right">Action</th>
<th class="text-right">Action</th>
</tr>
</thead>
<tbody>
@ -251,8 +251,8 @@ export default function UserDetailPage() {
</td>
<td>{formatDate(item.submittedAt)}</td>
<td>{formatDate(item.updatedAt)}</td>
<td class="align-right">
<A class={`btn ${item.isApproval ? '' : 'orange'}`} href={item.isApproval ? `/admin/approval/${item.id}` : `/admin/profile/${item.publicId || item.id}`}>
<td class="text-right">
<A class={`inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors ${item.isApproval ? '' : 'orange'}`} href={item.isApproval ? `/admin/approval/${item.id}` : `/admin/profile/${item.publicId || item.id}`}>
{item.isApproval ? 'View Request' : 'Open Profile'}
</A>
</td>

View file

@ -42,17 +42,17 @@ export default function VerificationStatusPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Verification Status</h1>
<p class="page-subtitle">Track request status states and open a specific record for follow-up.</p>
<h1 class="text-2xl font-bold text-gray-900">Verification Status</h1>
<p class="mt-1 text-sm text-gray-500">Track request status states and open a specific record for follow-up.</p>
</div>
<A class="btn" href="/admin/approval">Open Approval Center</A>
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/approval">Open Approval Center</A>
</div>
<section class="card" style="padding:0;overflow:hidden">
<div class="table-wrap">
<table class="list-table">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding:0;overflow:hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>ID</th>
@ -60,7 +60,7 @@ export default function VerificationStatusPage() {
<th>Requester</th>
<th>Status</th>
<th>Submitted</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -80,12 +80,12 @@ export default function VerificationStatusPage() {
<div style="font-weight:600;color:#0f172a">{item.requesterName}</div>
<div style="font-size:12px;color:#64748b">{item.requesterEmail}</div>
</td>
<td><span class="status-chip">{item.status}</span></td>
<td><span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">{item.status}</span></td>
<td style="color:#475569">{item.createdAt ? new Date(item.createdAt).toLocaleString() : '—'}</td>
<td>
<div class="table-actions">
<A class="action-icon-btn" href={`/admin/verification-status/${item.id}`} title="Open Status Detail"></A>
<A class="action-icon-btn" href={`/admin/approval/${item.id}`} title="Open Approval Detail">👁</A>
<div class="flex items-center justify-end gap-1">
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/verification-status/${item.id}`} title="Open Status Detail"></A>
<A class="rounded p-1.5 text-gray-500 hover:bg-gray-100 hover:text-gray-700 text-sm" href={`/admin/approval/${item.id}`} title="Open Approval Detail">👁</A>
</div>
</td>
</tr>

View file

@ -40,41 +40,41 @@ export default function VerificationStatusDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Verification Status Detail</h1>
<p class="page-subtitle">Open one verification status request and jump into approval review when needed.</p>
<h1 class="text-2xl font-bold text-gray-900">Verification Status Detail</h1>
<p class="mt-1 text-sm text-gray-500">Open one verification status request and jump into approval review when needed.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/verification-status">Back to Status List</A>
<A class="btn navy" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/verification-status">Back to Status List</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
</div>
</div>
<Show when={detail.loading}>
<div class="card"><p class="notice">Loading verification status...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading verification status...</p></div>
</Show>
<Show when={!detail.loading && !detail()}>
<div class="card"><p class="notice">Verification status record not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Verification status record not found.</p></div>
</Show>
<Show when={detail()}>
<div class="grid" style="margin-top:0">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Request</h2>
<p class="notice" style="margin:0"><strong>ID:</strong> {detail()!.id}</p>
<p class="notice" style="margin:8px 0 0"><strong>Type:</strong> {type()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Status:</strong> {status()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Submitted:</strong> {submitted() ? new Date(submitted()).toLocaleString() : '—'}</p>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Requester</h2>
<p class="notice" style="margin:0"><strong>Name:</strong> {requester()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Email:</strong> {email()}</p>
</section>
</div>
<section class="card" style="margin-top:16px">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="margin-top:16px">
<h2 style="margin-bottom:10px">Request Reason Payload</h2>
<pre class="json">{detail()!.requestReason || 'No requestReason payload found.'}</pre>
</section>

View file

@ -5,18 +5,18 @@ export default function VerificationManagementPage() {
return (
<AdminShell>
<div class="page-hero-card">
<h1 class="page-title">Approval Management</h1>
<p class="page-subtitle" style="margin-top:8px">
<h1 class="text-2xl font-bold text-gray-900">Approval Management</h1>
<p class="mt-1 text-sm text-gray-500" style="margin-top:8px">
Admin review now lives under Approval Management. Verification remains a user-facing status concept.
</p>
</div>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice" style="margin:0">
Use Approval Management to review companies, customers, candidates, and professional submissions.
</p>
<div class="actions">
<A class="btn navy" href="/admin/approval">Open Approval Management</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href="/admin/approval">Open Approval Management</A>
</div>
</section>
</AdminShell>

View file

@ -37,26 +37,26 @@ export default function VerificationDetailPage() {
return (
<AdminShell>
<div class="page-hero-card page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Verification Review</h1>
<p class="page-subtitle">Review submission context, documents, and verification decision state.</p>
<h1 class="text-2xl font-bold text-gray-900">Verification Review</h1>
<p class="mt-1 text-sm text-gray-500">Review submission context, documents, and verification decision state.</p>
</div>
<div class="page-actions-right">
<A class="btn" href="/admin/verification">Back to Verification</A>
<A class="btn navy" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
<div class="flex items-center gap-2">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href="/admin/verification">Back to Verification</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/approval/${params.id}`}>Open Approval Detail</A>
</div>
</div>
<Show when={approval.loading}>
<div class="card"><p class="notice">Loading verification detail...</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Loading verification detail...</p></div>
</Show>
<Show when={!approval.loading && !approval()}>
<div class="card"><p class="notice">Verification request not found.</p></div>
<div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Verification request not found.</p></div>
</Show>
<Show when={approval()}>
<div class="grid" style="margin-top:0">
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Summary</h2>
<p class="notice" style="margin:0"><strong>Approval ID:</strong> {approval()!.id}</p>
<p class="notice" style="margin:8px 0 0"><strong>Type:</strong> {type()}</p>
@ -64,13 +64,13 @@ export default function VerificationDetailPage() {
<p class="notice" style="margin:8px 0 0"><strong>Requester:</strong> {requester()}</p>
<p class="notice" style="margin:8px 0 0"><strong>Email:</strong> {email()}</p>
</section>
<section class="card">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm">
<h2 style="margin-bottom:8px">Remark Snapshot</h2>
<p class="notice" style="margin:0">
This route mirrors the Next.js verification detail entry point and delegates action workflow to Approval Management.
</p>
<div class="actions">
<A class="btn primary" href={`/admin/approval/${params.id}`}>Review & Take Action</A>
<A class="inline-flex items-center rounded-lg bg-[#fd6216] px-4 py-2 text-sm font-semibold text-white hover:bg-orange-600 transition-colors" href={`/admin/approval/${params.id}`}>Review & Take Action</A>
</div>
</section>
</div>

View file

@ -36,14 +36,14 @@ export default function VideoEditorsPage() {
return (
<AdminShell>
<div class="page-actions">
<div class="mb-6 flex items-start justify-between gap-4">
<div>
<h1 class="page-title">Video Editor Management</h1>
<p class="page-subtitle">Manage all video editor accounts on the platform.</p>
<h1 class="text-2xl font-bold text-gray-900">Video Editor Management</h1>
<p class="mt-1 text-sm text-gray-500">Manage all video editor accounts on the platform.</p>
</div>
</div>
<section class="card" style="padding: 0; overflow: hidden;">
<section class="rounded-xl border border-gray-200 bg-white shadow-sm" style="padding: 0; overflow: hidden;">
<div style="display:flex;gap:12px;padding:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap;">
<input
type="text"
@ -64,15 +64,15 @@ export default function VideoEditorsPage() {
</select>
</div>
<div class="table-wrap">
<table class="list-table">
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Registered</th>
<th class="align-right">Actions</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@ -93,22 +93,22 @@ export default function VideoEditorsPage() {
<td style="color:#475569">{item.email}</td>
<td>
{item.status?.toUpperCase() === 'ACTIVE' && (
<span class="status-chip active">ACTIVE</span>
<span class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-700">ACTIVE</span>
)}
{item.status?.toUpperCase() === 'INACTIVE' && (
<span class="status-chip">INACTIVE</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">INACTIVE</span>
)}
{item.status?.toUpperCase() === 'PENDING' && (
<span class="status-chip" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600" style="background:#fff7ed;color:#c2410c;border-color:#fed7aa;">PENDING</span>
)}
{!item.status && <span class="status-chip"></span>}
{!item.status && <span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600"></span>}
</td>
<td style="color:#475569">
{item.created_at ? new Date(item.created_at).toLocaleDateString() : '—'}
</td>
<td>
<div class="table-actions">
<A class="btn" href={`/admin/users/${item.id}`}>View</A>
<div class="flex items-center justify-end gap-1">
<A class="inline-flex items-center rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors" href={`/admin/users/${item.id}`}>View</A>
</div>
</td>
</tr>

View file

@ -5,5 +5,5 @@ import AdminShell from '~/components/AdminShell';
export default function WorkspaceAliasPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin', { replace: true }));
return <AdminShell><div class="card"><p class="notice">Redirecting to dashboard workspace...</p></div></AdminShell>;
return <AdminShell><div class="rounded-xl border border-gray-200 bg-white shadow-sm"><p class="notice">Redirecting to dashboard workspace...</p></div></AdminShell>;
}

View file

@ -8,7 +8,7 @@ export default function WorkspaceMenuAliasPage() {
onMount(() => navigate('/admin', { replace: true }));
return (
<AdminShell>
<div class="card">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm">
<p class="notice">Redirecting workspace menu <strong>{params.menuId}</strong> to dashboard...</p>
</div>
</AdminShell>

View file

@ -1,3 +1,9 @@
import { defineConfig } from '@solidjs/start/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({});
export default defineConfig({
ssr: false,
vite: {
plugins: [tailwindcss()],
},
});