Merge remote-tracking branch 'forgejo/high-performance' into high-performance
Some checks failed
build-and-release / build (push) Failing after 18s

This commit is contained in:
Ashwin Kumar Sivakumar 2026-08-14 01:46:28 +05:30
commit b9c209aa67
47 changed files with 2402 additions and 935 deletions

View file

@ -1,8 +1,4 @@
/* eslint-env node */
require("@typescript-eslint/eslint-recommended");
require("@typescript-eslint/parser");
require("eslint-plugin-solid");
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
@ -16,53 +12,44 @@ module.exports = {
{
files: ["**/*.tsx"],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaFeatures: { jsx: true },
},
settings: {
solid: {
// SolidJS specific options
noImportReact: true,
},
solid: { noImportReact: true },
},
rules: {
"solid/hyperscript": "off",
"solid/jsx-no-undef": "error",
"solid/jsx-pseudo-element": "error",
"solid/jsx-single-root-elem": "error",
"solid/no-dynamic-mount": "error",
"solid/no-hyphen-in-props": "error",
"solid/no-leaked-event-handlers": "error",
"solid/no-react-unknown-property": "error",
"solid/no-unknown-property": "error",
"solid/no-useless-fragment": "error",
"solid/prefer-classlist": "error",
"solid/prefer-destructuring": "warn",
"solid/prefer-innerhtml": "warn",
"solid/prefer-classlist": "off",
"solid/prefer-for": "error",
"solid/reactivity": "warn",
"solid/event-handlers": "off",
"solid/no-react-specific-props": "off",
"solid/no-innerhtml": "warn",
"solid/no-destructure": "warn",
"solid/self-closing-comp": "off",
// Admin codebase uses inline string styles throughout — suppress style-prop noise
"solid/style-prop": "off",
},
},
],
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" }],
"@typescript-eslint/no-explicit-any": "off",
"arrow-body-style": ["warn", "as-needed"],
" curly": ["error", "multi-line"],
curly: ["error", "multi-line"],
"no-console": "off",
"no-debugger": "warn",
"no-unused-vars": "off",
"prefer-const": "error",
},
env: {
browser: true,
},
env: { browser: true },
ignorePatterns: [
"dist",
".output",
"node_modules",
"coverage",
"storybook-static",
"test-results",
"playwright-report",
],
};

1520
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,8 @@
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"qa:visbug": "bash ./scripts/visbug-storybook.sh",
"qa:e2e:blueprint": "bash ./scripts/run-nxtgauge-e2e-suite.sh"
"qa:e2e:blueprint": "bash ./scripts/run-nxtgauge-e2e-suite.sh",
"lint": "eslint src --ext .tsx,.ts --max-warnings 0"
},
"dependencies": {
"@solidjs/meta": "^0.29.4",
@ -54,8 +55,12 @@
"@testing-library/jest-dom": "^6.6.3",
"@types/node": "^26.1.1",
"@types/pngjs": "^6.0.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/browser-playwright": "^4.1.1",
"@vitest/coverage-v8": "^4.1.1",
"eslint": "^8.57.1",
"eslint-plugin-solid": "^0.14.5",
"jsdom": "^25.0.1",
"msw": "^2.7.3",
"pixelmatch": "^7.1.0",

View file

@ -9,7 +9,7 @@ import {
onMount,
type JSX,
} from "solid-js";
import { Bell, Moon, Search, Settings, Sun, User } from "lucide-solid";
import { Bell, Moon, Search, Settings, Sun } from "lucide-solid";
import AdminSidebar from "./AdminSidebar";
import { isExternalIdentity } from "~/lib/admin-auth";
import { normalizeAllowedModules } from "~/lib/admin/module-access";
@ -218,7 +218,7 @@ function extractList(data: any, keys: string[]): any[] {
return [];
}
function GlobalSearch() {
function _GlobalSearch() {
const [query, setQuery] = createSignal("");
const [open, setOpen] = createSignal(false);
const [groups, setGroups] = createSignal<SearchGroup[]>([]);
@ -355,7 +355,7 @@ function GlobalSearch() {
);
}
function ShowTabs(props: {
function _ShowTabs(props: {
tabs: Tab[];
isTabActive: (tab: Tab) => boolean;
setTabsTrackEl: (el: HTMLDivElement) => void;
@ -364,8 +364,8 @@ function ShowTabs(props: {
) => void;
tabIndicator: () => { left: number; width: number; ready: boolean };
}) {
if (props.tabs.length === 0) return null;
return (
<Show when={props.tabs.length > 0}>
<div
ref={props.setTabsTrackEl}
class="relative mb-6 mt-1 flex items-center gap-1 border-b border-[#e5e7eb]"
@ -391,13 +391,14 @@ function ShowTabs(props: {
style={{ left: `${props.tabIndicator().left}px`, width: `${props.tabIndicator().width}px` }}
/>
</div>
</Show>
);
}
export default function AdminShell(props: { children: JSX.Element }) {
const location = useLocation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [_searchParams] = useSearchParams();
const [checkedSession, setCheckedSession] = createSignal(true);
const [adminName, setAdminName] = createSignal("Admin User");
@ -406,12 +407,13 @@ export default function AdminShell(props: { children: JSX.Element }) {
const [sidebarOpen, setSidebarOpen] = createSignal(false);
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
const [unreadCount, setUnreadCount] = createSignal(0);
const [pendingVerificationCount, setPendingVerificationCount] = createSignal(0);
const [theme, setTheme] = createSignal<"light" | "dark">("light");
const [routeTransitioning, setRouteTransitioning] = createSignal(false);
const [routeTransitioning, _setRouteTransitioning] = createSignal(false);
const [tabsTrackEl, setTabsTrackEl] = createSignal<HTMLDivElement>();
const [tabRefs, setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
const [tabIndicator, setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
const [tabsTrackEl, _setTabsTrackEl] = createSignal<HTMLDivElement>();
const [tabRefs, _setTabRefs] = createSignal<Record<string, HTMLAnchorElement>>({});
const [_tabIndicator, _setTabIndicator] = createSignal({ left: 0, width: 0, ready: false });
let contentScrollRef: HTMLDivElement | undefined;
const logout = async () => {
@ -482,7 +484,6 @@ export default function AdminShell(props: { children: JSX.Element }) {
sessionStorage.removeItem("nxtgauge_admin_access_token");
sessionStorage.removeItem("nxtgauge_admin_preview");
}
clearAdminSession();
navigate(`/login?from=${encodeURIComponent(location.pathname + location.search)}`, {
replace: true,
});
@ -534,6 +535,36 @@ export default function AdminShell(props: { children: JSX.Element }) {
const interval = setInterval(fetchUnreadCount, 30000);
onCleanup(() => clearInterval(interval));
// Fetch pending-verification count and poll every 60 s so the sidebar badge
// stays up-to-date as users submit role registrations.
const fetchPendingVerificationCount = async () => {
try {
const accessToken =
typeof sessionStorage !== "undefined"
? sessionStorage.getItem("nxtgauge_admin_access_token") || ""
: "";
if (!accessToken) return;
const res = await fetch("/api/admin/verifications?status=PENDING&limit=200", {
headers: {
Accept: "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
credentials: "include",
});
if (res.ok) {
const data = await res.json().catch(() => ({}));
const items = Array.isArray(data?.items) ? data.items : [];
setPendingVerificationCount(items.length);
}
} catch {
// non-fatal — badge stays at its last value
}
};
fetchPendingVerificationCount();
const verificationPoll = setInterval(fetchPendingVerificationCount, 60000);
onCleanup(() => clearInterval(verificationPoll));
const verify = async () => {
// The server-verified session check below (/api/auth/session) is the
// real security boundary. It redirects to /login on any failure.
@ -601,7 +632,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
void verify();
});
const pageTitle = createMemo(() => {
const _pageTitle = createMemo(() => {
const path = location.pathname;
for (const entry of PAGE_TITLES) {
if (
@ -736,6 +767,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
theme={theme()}
allowedModules={allowedModules()}
isSuperAdmin={isSuperAdmin()}
badgeCounts={{ pendingVerifications: pendingVerificationCount() }}
/>
</div>

View file

@ -6,7 +6,6 @@ import {
Briefcase,
Users,
ShieldCheck,
FileText,
LayoutDashboard,
ClipboardList,
UserRoundSearch,
@ -43,9 +42,12 @@ import {
type NavItem = {
href: string;
label: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
icon: any;
aliasPrefix?: string;
moduleKeys?: string[];
/** Optional badge key — used to look up count from badgeCounts prop */
badgeKey?: string;
};
const GROUPS: NavItem[][] = [
@ -120,6 +122,7 @@ const GROUPS: NavItem[][] = [
label: "Verification Management",
icon: BadgeCheck,
moduleKeys: ["VERIFICATION_MANAGEMENT", "VERIFICATIONS"],
badgeKey: "pendingVerifications",
},
{
href: "/admin/approval",
@ -339,12 +342,6 @@ const GROUPS: NavItem[][] = [
icon: Receipt,
moduleKeys: ["LEDGER", "LEDGER_MANAGEMENT"],
},
{
href: "/admin/ai-management",
label: "AI Management",
icon: Sparkles,
moduleKeys: ["AI_MANAGEMENT", "AI"],
},
],
];
@ -357,6 +354,8 @@ export default function AdminSidebar(props: {
theme?: "light" | "dark";
allowedModules?: string[] | null;
isSuperAdmin?: boolean;
/** Map of badgeKey → count; a non-zero count shows an orange pill on the nav item */
badgeCounts?: Record<string, number>;
}) {
const location = useLocation();
@ -438,7 +437,7 @@ export default function AdminSidebar(props: {
</A>
<button
type="button"
onClick={props.onToggle}
onClick={() => props.onToggle()}
style={`position:absolute;right:-10px;top:50%;transform:translateY(-50%);width:20px;height:20px;border-radius:50%;border:1px solid ${isDark() ? "#1F2937" : "#E5E7EB"};background:${isDark() ? "#111827" : "white"};box-shadow:0 1px 4px rgba(0,0,0,0.1);display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:10;color:${isDark() ? "#CBD5E1" : "#6B7280"}`}
aria-label={props.collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
@ -472,15 +471,27 @@ export default function AdminSidebar(props: {
style={`display:flex;align-items:center;height:36px;border-radius:8px;text-decoration:none;padding:0 ${props.collapsed ? "0" : "10px"};transition:background 140ms ease,color 140ms ease;${props.collapsed ? "justify-content:center;" : ""}${active() ? "background:#FFF3EE;color:#FF5E13;" : `color:${isDark() ? "#CBD5E1" : "#6B7280"};`}`}
aria-current={active() ? "page" : undefined}
>
<Icon
size={16}
style={`flex-shrink:0;${active() ? "color:#FF5E13" : `color:${isDark() ? "#94A3B8" : "#9CA3AF"}`}`}
strokeWidth={active() ? 2.5 : 2}
/>
<span style="position:relative;display:inline-flex;flex-shrink:0">
<Icon
size={16}
style={`${active() ? "color:#FF5E13" : `color:${isDark() ? "#94A3B8" : "#9CA3AF"}`}`}
strokeWidth={active() ? 2.5 : 2}
/>
<Show when={props.collapsed && item.badgeKey && (props.badgeCounts?.[item.badgeKey] ?? 0) > 0}>
<span style="position:absolute;top:-4px;right:-5px;min-width:14px;height:14px;border-radius:9999px;background:#FF5E13;color:white;font-size:9px;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 3px;line-height:1">
{(props.badgeCounts?.[item.badgeKey!] ?? 0) > 99 ? "99+" : props.badgeCounts?.[item.badgeKey!]}
</span>
</Show>
</span>
<Show when={!props.collapsed}>
<span style="margin-left:9px;font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
<span style="margin-left:9px;font-size:12.5px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0">
{item.label}
</span>
<Show when={item.badgeKey && (props.badgeCounts?.[item.badgeKey] ?? 0) > 0}>
<span style="margin-left:6px;min-width:18px;height:18px;border-radius:9999px;background:#FF5E13;color:white;font-size:10px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;padding:0 5px;flex-shrink:0;line-height:1">
{(props.badgeCounts?.[item.badgeKey!] ?? 0) > 99 ? "99+" : props.badgeCounts?.[item.badgeKey!]}
</span>
</Show>
</Show>
</A>
);

File diff suppressed because it is too large Load diff

View file

@ -107,12 +107,12 @@ export default function UserListPage(props: { config: UserListPageConfig }) {
{/* Stat cards */}
<div class="grid grid-cols-2 gap-5 lg:grid-cols-4">
{([
<For each={[
{ label: 'Total', value: () => stats().total, Ic: Users, bg: 'bg-[#FFF1EB]', color: 'text-[#FF5E13]' },
{ label: 'Active', value: () => stats().active, Ic: UserCheck, bg: 'bg-[#ECFDF5]', color: 'text-[#059669]' },
{ label: 'Inactive', value: () => stats().inactive, Ic: UserX, bg: 'bg-[#FEF2F2]', color: 'text-[#DC2626]' },
{ label: 'Pending Verification', value: () => stats().pending, Ic: Clock, bg: 'bg-[#FFFBEB]', color: 'text-[#D97706]' },
] as const).map(s => (
]}>{(s) => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class="flex items-start justify-between">
<div>
@ -124,7 +124,7 @@ export default function UserListPage(props: { config: UserListPageConfig }) {
</div>
</div>
</div>
))}
)}</For>
</div>
{/* Table card */}

View file

@ -1,10 +0,0 @@
import { Title } from "@solidjs/meta";
export default function About() {
return (
<main>
<Title>About</Title>
<h1>About</h1>
</main>
);
}

View file

@ -1,5 +0,0 @@
import { Navigate } from '@solidjs/router';
export default function ApprovalManagementLegacyRedirectPage() {
return <Navigate href="/admin/approval" />;
}

View file

@ -483,11 +483,11 @@ export default function ApprovalManagementPage() {
<Show when={true}>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB">
{([
<For each={[
{ key: 'all', label: 'All Approvals', action: () => { setListTab('all'); setStatusFilter('all'); } },
{ key: 'escalated', label: `Escalated (${escalatedCount()})`, action: () => { setListTab('escalated'); setStatusFilter('escalated'); } },
{ key: 'view', label: 'View Approval', action: () => { setListTab('view'); } },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -495,7 +495,7 @@ export default function ApprovalManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<Show when={listTab() === 'view'}>
@ -523,8 +523,9 @@ export default function ApprovalManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:4px;min-height:44px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(['overview', 'verification', 'checklist', 'logs'] as const).map((tab, i) => {
<For each={['overview', 'verification', 'checklist', 'logs'] as const}>{(tab, getI) => {
const labels = ['Overview', 'Verification Summary', 'Approval Checklist', 'Activity Logs'];
const i = getI();
const active = () => detailTab() === tab;
return (
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
@ -532,7 +533,7 @@ export default function ApprovalManagementPage() {
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -612,18 +613,18 @@ export default function ApprovalManagementPage() {
<Show when={detailTab() === 'checklist'}>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
'Verification report is complete and reviewed',
'All required entity data is present',
'No active risk flags on applicant',
'Applicant meets all policy requirements',
'Final approval readiness confirmed',
].map((item) => (
]}>{(item) => (
<label style="display:flex;align-items:center;gap:12px;padding:12px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer">
<input type="checkbox" style="width:16px;height:16px;accent-color:#FF5E13" />
<span style="font-size:13px;color:#111827;font-weight:500">{item}</span>
</label>
))}
)}</For>
</div>
</Show>
@ -674,11 +675,14 @@ export default function ApprovalManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]}
</button>
))}
<For each={['submitted_desc', 'submitted_asc', 'priority_desc', 'priority_asc'] as const}>{(s, getI) => {
const i = getI();
return (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Submitted (Newest)', 'Submitted (Oldest)', 'Priority (High-Low)', 'Priority (Low-High)'][i]}
</button>
);
}}</For>
</div>
</Show>
</div>
@ -693,11 +697,11 @@ export default function ApprovalManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'pending', 'escalated'] as const).map((s) => (
<For each={['all', 'pending', 'escalated'] as const}>{(s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Cases' : s === 'pending' ? 'Pending Decisions' : 'Escalated'}
</button>
))}
)}</For>
</div>
</Show>
</div>

View file

@ -1,5 +0,0 @@
import { Navigate } from '@solidjs/router';
export default function ApprovalsLegacyRedirectPage() {
return <Navigate href="/admin/approval" />;
}

View file

@ -121,12 +121,12 @@ export default function CandidateManagementPage() {
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
<For each={[
{ key: 'all', label: 'All Candidates', action: () => { setListTab('all'); setStatusFilter('all'); } },
{ key: 'active', label: 'Active', action: () => { setListTab('active'); setStatusFilter('active'); } },
{ key: 'pending', label: 'Pending', action: () => { setListTab('pending'); setStatusFilter('pending'); } },
{ key: 'view', label: 'View Profile', action: () => setListTab('view') },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -134,7 +134,7 @@ export default function CandidateManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
{/* View Profile panel */}
@ -158,8 +158,9 @@ export default function CandidateManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(['overview', 'experience', 'skills'] as const).map((tab, i) => {
<For each={['overview', 'experience', 'skills'] as const}>{(tab, getI) => {
const labels = ['Overview', 'Work Experience', 'Skills & Education'];
const i = getI();
const active = () => detailTab() === tab;
return (
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
@ -167,7 +168,7 @@ export default function CandidateManagementPage() {
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -176,17 +177,17 @@ export default function CandidateManagementPage() {
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Candidate Info</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
{ l: 'Location', v: selectedCandidate()!.location || '—' },
{ l: 'Experience', v: selectedCandidate()!.experience || '—' },
{ l: 'Phone', v: selectedCandidate()!.phone || '—' },
{ l: 'Last Active', v: selectedCandidate()!.updatedAt || '—' },
].map(item => (
]}>{(item) => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v}</span>
</div>
))}
)}</For>
</div>
</div>
</div>
@ -221,11 +222,14 @@ export default function CandidateManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['name_asc', 'name_desc', 'registered_desc', 'registered_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Joined (Newest)', 'Joined (Oldest)'][i]}
</button>
))}
<For each={['name_asc', 'name_desc', 'registered_desc', 'registered_asc'] as const}>{(s, getI) => {
const i = getI();
return (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Joined (Newest)', 'Joined (Oldest)'][i]}
</button>
);
}}</For>
</div>
</Show>
</div>
@ -240,11 +244,11 @@ export default function CandidateManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'active', 'pending', 'inactive'] as const).map((s) => (
<For each={['all', 'active', 'pending', 'inactive'] as const}>{(s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : s === 'pending' ? 'Pending' : 'Inactive'}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -262,9 +266,9 @@ export default function CandidateManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Candidate Name', 'Email', 'Location', 'Registered', 'Status', 'Actions'].map(h => (
<For each={['Candidate Name', 'Email', 'Location', 'Registered', 'Status', 'Actions']}>{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -177,12 +177,12 @@ export default function CompanyManagementPage() {
<div>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
<For each={[
{ key: 'all', label: 'All Companies', action: () => { setListTab('all'); setStatusFilter('all'); } },
{ key: 'active', label: 'Active', action: () => { setListTab('active'); setStatusFilter('ACTIVE'); } },
{ key: 'pending', label: 'Pending', action: () => { setListTab('pending'); setStatusFilter('PENDING'); } },
{ key: 'view', label: 'View Company', action: () => setListTab('view') },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -190,7 +190,7 @@ export default function CompanyManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<Show when={listTab() === 'view'}>
@ -212,8 +212,9 @@ export default function CompanyManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(['overview', 'verification', 'metrics'] as const).map((tab, i) => {
<For each={['overview', 'verification', 'metrics'] as const}>{(tab, getI) => {
const labels = ['Overview', 'Verification', 'Metrics'];
const i = getI();
const active = () => detailTab() === tab;
return (
<button type="button" onClick={() => setDetailTab(tab)} style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? '#FF5E13' : '#6B7280'}`}>
@ -221,7 +222,7 @@ export default function CompanyManagementPage() {
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -230,17 +231,17 @@ export default function CompanyManagementPage() {
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Company Profile</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
{ l: 'Industry', v: selectedCompany()!.industry },
{ l: 'Location', v: selectedCompany()!.location },
{ l: 'Registration', v: selectedCompany()!.registrationNumber },
{ l: 'Subscription', v: selectedCompany()!.subscriptionType },
].map((item) => (
]}>{(item) => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v || '—'}</span>
</div>
))}
)}</For>
</div>
</div>
</div>
@ -251,16 +252,16 @@ export default function CompanyManagementPage() {
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Verification Summary</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
{ l: 'Verification Status', v: selectedCompany()!.verificationStatus },
{ l: 'Account Status', v: selectedCompany()!.accountStatus },
{ l: 'Last Updated', v: selectedCompany()!.updatedAt ? new Date(selectedCompany()!.updatedAt).toLocaleString() : '—' },
].map((item) => (
]}>{(item) => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v || '—'}</span>
</div>
))}
)}</For>
</div>
</div>
</div>
@ -271,15 +272,15 @@ export default function CompanyManagementPage() {
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Performance Metrics</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
{ l: 'Job Postings', v: String(selectedCompany()!.jobPostingsCount || 0) },
{ l: 'Total Hires', v: String(selectedCompany()!.totalHires || 0) },
].map((item) => (
]}>{(item) => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v}</span>
</div>
))}
)}</For>
</div>
</div>
</div>
@ -318,12 +319,12 @@ export default function CompanyManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'name_asc', label: 'Name (A-Z)' },
{ key: 'name_desc', label: 'Name (Z-A)' },
{ key: 'joined_desc', label: 'Joined (Newest)' },
{ key: 'joined_asc', label: 'Joined (Oldest)' },
] as const).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => {
@ -334,7 +335,7 @@ export default function CompanyManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -353,13 +354,13 @@ export default function CompanyManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'all', label: 'All Status' },
{ key: 'ACTIVE', label: 'Active' },
{ key: 'PENDING', label: 'Pending' },
{ key: 'SUSPENDED', label: 'Suspended' },
{ key: 'INACTIVE', label: 'Inactive' },
] as const).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => {
@ -370,7 +371,7 @@ export default function CompanyManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -384,9 +385,9 @@ export default function CompanyManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Company', 'Industry', 'Location', 'Status', 'Joined', 'Actions'].map((h) => (
<For each={['Company', 'Industry', 'Location', 'Status', 'Joined', 'Actions']}>{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -101,9 +101,9 @@ export default function CouponPage() {
let r = coupons();
const q = search().toLowerCase();
if (q)
r = r.filter(
{r = r.filter(
(c) => c.code.toLowerCase().includes(q) || (c.title || "").toLowerCase().includes(q)
);
);}
if (statusFilter() === "active") r = r.filter((c) => c.is_active);
if (statusFilter() === "inactive") r = r.filter((c) => !c.is_active);
const sorted = [...r];

View file

@ -619,7 +619,7 @@ export default function CreditPage() {
<div>
{/* AI Credits Sub-tabs */}
<div class="bg-white border-b border-gray-200 px-0 flex items-center gap-6 mb-6">
{(['balance', 'adjust', 'reconcile'] as const).map((t) => (
<For each={['balance', 'adjust', 'reconcile'] as const}>{(t) => (
<button
type="button"
class={
@ -631,7 +631,7 @@ export default function CreditPage() {
>
{t === 'balance' ? 'Balance & Ledger' : t === 'adjust' ? 'Reward / Deduct' : 'Reconcile'}
</button>
))}
)}</For>
</div>
{/* AI Balance & Ledger */}

View file

@ -120,12 +120,12 @@ export default function CustomerManagementPage() {
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
<For each={[
{ key: 'all', label: 'All Customers', action: () => { setListTab('all'); setStatusFilter('all'); } },
{ key: 'active', label: 'Active', action: () => { setListTab('active'); setStatusFilter('active'); } },
{ key: 'pending', label: 'Pending', action: () => { setListTab('pending'); setStatusFilter('pending'); } },
{ key: 'view', label: 'View Profile', action: () => setListTab('view') },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -133,7 +133,7 @@ export default function CustomerManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
{/* View Profile panel */}
@ -157,7 +157,8 @@ export default function CustomerManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(['overview', 'orders', 'support'] as const).map((tab, i) => {
<For each={['overview', 'orders', 'support'] as const}>{(tab, getI) => {
const i = getI();
const labels = ['Overview', 'Order History', 'Support Cases'];
const active = () => detailTab() === tab;
return (
@ -166,7 +167,7 @@ export default function CustomerManagementPage() {
<Show when={active()}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" /></Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -175,17 +176,17 @@ export default function CustomerManagementPage() {
<div style="border:1px solid #E5E7EB;border-radius:12px;padding:20px">
<h3 style="font-size:14px;font-weight:700;color:#111827;margin-bottom:16px">Customer Summary</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
<For each={[
{ l: 'Location', v: selectedCustomer()!.location || '—' },
{ l: 'Total Orders', v: selectedCustomer()!.totalOrders || '0' },
{ l: 'Phone', v: selectedCustomer()!.phone || '—' },
{ l: 'Last Active', v: selectedCustomer()!.updatedAt || '—' },
].map(item => (
]}>{item => (
<div style="display:flex;justify-content:space-between">
<span style="font-size:13px;color:#6B7280">{item.l}</span>
<span style="font-size:13px;font-weight:600;color:#111827">{item.v}</span>
</div>
))}
)}</For>
</div>
</div>
</div>
@ -220,11 +221,14 @@ export default function CustomerManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['name_asc', 'name_desc', 'orders_desc', 'orders_asc'] as const).map((s, i) => (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Orders (High-Low)', 'Orders (Low-High)'][i]}
</button>
))}
<For each={['name_asc', 'name_desc', 'orders_desc', 'orders_asc'] as const}>{(s, getI) => {
const i = getI();
return (
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
{['Name (A-Z)', 'Name (Z-A)', 'Orders (High-Low)', 'Orders (Low-High)'][i]}
</button>
);
}}</For>
</div>
</Show>
</div>
@ -239,11 +243,11 @@ export default function CustomerManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(['all', 'active', 'pending', 'inactive'] as const).map((s) => (
<For each={['all', 'active', 'pending', 'inactive'] as const}>{(s) => (
<button type="button" onClick={() => { setStatusFilter(s); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
{s === 'all' ? 'All Status' : s === 'active' ? 'Active' : s === 'pending' ? 'Pending' : 'Inactive'}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -261,9 +265,9 @@ export default function CustomerManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Customer Name', 'Email', 'Location', 'Orders', 'Status', 'Actions'].map(h => (
<For each={['Customer Name', 'Email', 'Location', 'Orders', 'Status', 'Actions']}>{h => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -1,3 +0,0 @@
import DepartmentManagementPage from './department';
export default DepartmentManagementPage;

View file

@ -164,9 +164,9 @@ export default function DepartmentManagementPage() {
sorted.sort((a, b) => {
if (mode === "name_desc") return b.name.localeCompare(a.name);
if (mode === "employees_desc")
return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0);
{return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0);}
if (mode === "employees_asc")
return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0);
{return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0);}
return a.name.localeCompare(b.name);
});
r = sorted;
@ -266,8 +266,7 @@ export default function DepartmentManagementPage() {
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{(
[
<For each={[
{
key: "all",
label: "All Departments",
@ -286,8 +285,7 @@ export default function DepartmentManagementPage() {
},
},
{ key: "view", label: "View Department", action: () => setListTab("view") },
] as const
).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -295,7 +293,7 @@ export default function DepartmentManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
{/* View Department panel */}
@ -409,8 +407,9 @@ export default function DepartmentManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["name_asc", "name_desc", "employees_desc", "employees_asc"] as const).map(
(s, i) => (
<For each={["name_asc", "name_desc", "employees_desc", "employees_asc"] as const}>{(s, getI) => {
const i = getI();
return (
<button
type="button"
onClick={() => {
@ -428,8 +427,8 @@ export default function DepartmentManagementPage() {
][i]
}
</button>
)
)}
);
}}</For>
</div>
</Show>
</div>
@ -456,7 +455,7 @@ export default function DepartmentManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["all", "active", "inactive"] as const).map((s) => (
<For each={["all", "active", "inactive"] as const}>{(s) => (
<button
type="button"
onClick={() => {
@ -468,7 +467,7 @@ export default function DepartmentManagementPage() {
>
{s === "all" ? "All Status" : s === "active" ? "Active" : "Inactive"}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -647,7 +646,7 @@ export default function DepartmentManagementPage() {
}
);
if (!res.ok)
throw new Error(`Request failed (${res.status})`);
{throw new Error(`Request failed (${res.status})`);}
} catch (err: any) {
setError(err?.message || "Failed to update status.");
} finally {
@ -822,7 +821,8 @@ export default function DepartmentManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "settings"] as const).map((tab, i) => {
<For each={["general", "settings"] as const}>{(tab, getI) => {
const i = getI();
const labels = ["General Information", "Department Settings"];
const active = () => formTab() === tab;
return (
@ -837,7 +837,7 @@ export default function DepartmentManagementPage() {
</Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -879,7 +879,7 @@ export default function DepartmentManagementPage() {
Set whether this department is currently active
</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(["ACTIVE", "INACTIVE"] as const).map((s) => (
<For each={["ACTIVE", "INACTIVE"] as const}>{(s) => (
<button
type="button"
onClick={() => setStatus(s)}
@ -887,7 +887,7 @@ export default function DepartmentManagementPage() {
>
{s === "ACTIVE" ? "Active" : "Inactive"}
</button>
))}
)}</For>
</div>
</div>
</div>

View file

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

View file

@ -247,9 +247,9 @@ export default function DesignationManagementPage() {
sorted.sort((a, b) => {
if (mode === "name_desc") return b.name.localeCompare(a.name);
if (mode === "employees_desc")
return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0);
{return Number(b.totalEmployees || 0) - Number(a.totalEmployees || 0);}
if (mode === "employees_asc")
return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0);
{return Number(a.totalEmployees || 0) - Number(b.totalEmployees || 0);}
return a.name.localeCompare(b.name);
});
r = sorted;
@ -386,8 +386,7 @@ export default function DesignationManagementPage() {
<div>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{(
[
<For each={[
{
key: "all",
label: "All Designations",
@ -406,8 +405,7 @@ export default function DesignationManagementPage() {
},
},
{ key: "view", label: "View Designation", action: () => setListTab("view") },
] as const
).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -415,7 +413,7 @@ export default function DesignationManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
{/* View Record panel */}
@ -538,8 +536,9 @@ export default function DesignationManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["name_asc", "name_desc", "employees_desc", "employees_asc"] as const).map(
(s, i) => (
<For each={["name_asc", "name_desc", "employees_desc", "employees_asc"] as const}>{(s, getI) => {
const i = getI();
return (
<button
type="button"
onClick={() => {
@ -557,8 +556,8 @@ export default function DesignationManagementPage() {
][i]
}
</button>
)
)}
);
}}</For>
</div>
</Show>
</div>
@ -588,7 +587,7 @@ export default function DesignationManagementPage() {
<p style="padding:6px 12px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">
Status
</p>
{(["all", "active", "inactive"] as const).map((s) => (
<For each={["all", "active", "inactive"] as const}>{(s) => (
<button
type="button"
onClick={() => {
@ -600,7 +599,7 @@ export default function DesignationManagementPage() {
>
{s === "all" ? "All Status" : s === "active" ? "Active" : "Inactive"}
</button>
))}
)}</For>
<div style="height:1px;background:#F3F4F6;margin:6px 0" />
<p style="padding:6px 12px;font-size:10px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">
Department
@ -802,7 +801,7 @@ export default function DesignationManagementPage() {
}
);
if (!res.ok)
throw new Error(`Request failed (${res.status})`);
{throw new Error(`Request failed (${res.status})`);}
} catch (err: any) {
setError(err?.message || "Failed to update status.");
} finally {
@ -971,7 +970,8 @@ export default function DesignationManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "settings"] as const).map((tab, i) => {
<For each={["general", "settings"] as const}>{(tab, getI) => {
const i = getI();
const labels = ["General Information", "Designation Settings"];
const active = () => formTab() === tab;
return (
@ -986,7 +986,7 @@ export default function DesignationManagementPage() {
</Show>
</button>
);
})}
}}</For>
</div>
<div style="padding:24px">
@ -1034,7 +1034,7 @@ export default function DesignationManagementPage() {
Set whether this designation is currently active
</p>
<div style="margin-top:12px;display:flex;gap:10px">
{(["ACTIVE", "INACTIVE"] as const).map((s) => (
<For each={["ACTIVE", "INACTIVE"] as const}>{(s) => (
<button
type="button"
onClick={() => setStatus(s)}
@ -1042,7 +1042,7 @@ export default function DesignationManagementPage() {
>
{s === "ACTIVE" ? "Active" : "Inactive"}
</button>
))}
)}</For>
</div>
</div>
</div>

View file

@ -568,8 +568,8 @@ export default function EmployeeManagementPage() {
<Show when={view() === "list"}>
{/* Tabs */}
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{(
[
<For
each={[
{
key: "all",
label: "All Employees",
@ -593,16 +593,18 @@ export default function EmployeeManagementPage() {
if (viewingEmp()) setView("detail");
},
},
] as const
).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px" : "color:#6B7280"}`}
>
{tab.label}
</button>
))}
] as const}
>
{(tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px" : "color:#6B7280"}`}
>
{tab.label}
</button>
)}
</For>
</div>
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
@ -640,18 +642,23 @@ export default function EmployeeManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(["joined_desc", "joined_asc", "name_asc", "name_desc"] as const).map((s, i) => (
<button
type="button"
onClick={() => {
setSortBy(s);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? "#FF5E13" : "#374151"};background:${sortBy() === s ? "#FFF1EB" : "transparent"}`}
>
{["Joining (Newest)", "Joining (Oldest)", "Name (A→Z)", "Name (Z→A)"][i]}
</button>
))}
<For each={["joined_desc", "joined_asc", "name_asc", "name_desc"] as const}>
{(s, getI) => {
const i = getI();
return (
<button
type="button"
onClick={() => {
setSortBy(s);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? "#FF5E13" : "#374151"};background:${sortBy() === s ? "#FFF1EB" : "transparent"}`}
>
{["Joining (Newest)", "Joining (Oldest)", "Name (A→Z)", "Name (Z→A)"][i]}
</button>
);
}}
</For>
</div>
</Show>
</div>
@ -678,20 +685,24 @@ export default function EmployeeManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
["all", "ACTIVE", "PROBATION", "ON_LEAVE", "SUSPENDED", "INACTIVE"] as const
).map((s) => (
<button
type="button"
onClick={() => {
setStatusFilter(s as any);
setFilterMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? "#FF5E13" : "#374151"};background:${statusFilter() === s ? "#FFF1EB" : "transparent"}`}
>
{s === "all" ? "All Statuses" : s.replace("_", " ").toUpperCase()}
</button>
))}
<For
each={
["all", "ACTIVE", "PROBATION", "ON_LEAVE", "SUSPENDED", "INACTIVE"] as const
}
>
{(s) => (
<button
type="button"
onClick={() => {
setStatusFilter(s as any);
setFilterMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? "#FF5E13" : "#374151"};background:${statusFilter() === s ? "#FFF1EB" : "transparent"}`}
>
{s === "all" ? "All Statuses" : s.replace("_", " ").toUpperCase()}
</button>
)}
</For>
</div>
</Show>
</div>
@ -832,7 +843,7 @@ export default function EmployeeManagementPage() {
type="button"
onClick={async () => {
if (!confirm(`Are you sure you want to delete ${row.name}?`))
return;
{return;}
setOpenMenuId(null);
try {
const accessToken =
@ -897,22 +908,25 @@ export default function EmployeeManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["basic", "work", "access"] as const).map((tab, i) => {
const labels = ["Basic Information", "Work Information", "Access & Permissions"];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
<For each={["basic", "work", "access"] as const}>
{(tab, getI) => {
const labels = ["Basic Information", "Work Information", "Access & Permissions"];
const i = getI();
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
}}
</For>
</div>
<div style="padding:24px">
@ -1209,22 +1223,25 @@ export default function EmployeeManagementPage() {
{/* Sub-tabs */}
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(["profile", "activity", "access"] as const).map((tab, i) => {
const labels = ["Profile", "Activity Logs", "Access History"];
const active = () => detailTab() === tab;
return (
<button
type="button"
onClick={() => setDetailTab(tab)}
style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
<For each={["profile", "activity", "access"] as const}>
{(tab, getI) => {
const labels = ["Profile", "Activity Logs", "Access History"];
const i = getI();
const active = () => detailTab() === tab;
return (
<button
type="button"
onClick={() => setDetailTab(tab)}
style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
}}
</For>
</div>
<div style="padding:24px">
@ -1235,17 +1252,21 @@ export default function EmployeeManagementPage() {
Personal Information
</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
{ l: "Full Name", v: viewingEmp()!.name },
{ l: "Email", v: viewingEmp()!.email },
{ l: "Phone", v: viewingEmp()!.phone || "—" },
{ l: "Gender", v: viewingEmp()!.gender || "—" },
].map((item) => (
<div>
<p style="font-size:11px;color:#9CA3AF">{item.l}</p>
<p style="font-size:14px;font-weight:600;color:#111827">{item.v}</p>
</div>
))}
<For
each={[
{ l: "Full Name", v: viewingEmp()!.name },
{ l: "Email", v: viewingEmp()!.email },
{ l: "Phone", v: viewingEmp()!.phone || "—" },
{ l: "Gender", v: viewingEmp()!.gender || "—" },
]}
>
{(item) => (
<div>
<p style="font-size:11px;color:#9CA3AF">{item.l}</p>
<p style="font-size:14px;font-weight:600;color:#111827">{item.v}</p>
</div>
)}
</For>
</div>
</div>
<div>
@ -1253,17 +1274,21 @@ export default function EmployeeManagementPage() {
Work Details
</h3>
<div style="display:flex;flex-direction:column;gap:12px">
{[
{ l: "Department", v: viewingEmp()!.department || "—" },
{ l: "Designation", v: viewingEmp()!.designation || "—" },
{ l: "Internal Role", v: viewingEmp()!.internalRole || "—" },
{ l: "Employment Type", v: viewingEmp()!.employmentType || "—" },
].map((item) => (
<div>
<p style="font-size:11px;color:#9CA3AF">{item.l}</p>
<p style="font-size:14px;font-weight:600;color:#111827">{item.v}</p>
</div>
))}
<For
each={[
{ l: "Department", v: viewingEmp()!.department || "—" },
{ l: "Designation", v: viewingEmp()!.designation || "—" },
{ l: "Internal Role", v: viewingEmp()!.internalRole || "—" },
{ l: "Employment Type", v: viewingEmp()!.employmentType || "—" },
]}
>
{(item) => (
<div>
<p style="font-size:11px;color:#9CA3AF">{item.l}</p>
<p style="font-size:14px;font-weight:600;color:#111827">{item.v}</p>
</div>
)}
</For>
</div>
</div>
</div>

View file

@ -278,7 +278,7 @@ function rolePreviewPath(roleKey: string): string {
const key = String(roleKey || "").toUpperCase();
if (key.includes("COMPANY")) return "/employers/dashboard";
if (key.includes("CUSTOMER") || key.includes("SERVICE_SEEKER"))
return "/users/customer/dashboard";
{return "/users/customer/dashboard";}
if (key.includes("JOB_SEEKER") || key.includes("JOBSEEKER")) return "/users/candidate/dashboard";
if (key.includes("PHOTOGRAPHER")) return "/users/photographer/dashboard";
if (key.includes("MAKEUP")) return "/users/makeup/dashboard";
@ -286,7 +286,7 @@ function rolePreviewPath(roleKey: string): string {
if (key.includes("DEVELOPER")) return "/users/developers/dashboard";
if (key.includes("VIDEO")) return "/users/video-editors/dashboard";
if (key.includes("UGC") || (key.includes("CONTENT") && key.includes("CREATOR")))
return "/users/ugc-content-creators/dashboard";
{return "/users/ugc-content-creators/dashboard";}
if (key.includes("FITNESS")) return "/users/fitness-trainers/dashboard";
if (key.includes("GRAPHIC")) return "/users/graphic-designers/dashboard";
if (key.includes("SOCIAL")) return "/users/social-media-managers/dashboard";
@ -432,7 +432,7 @@ export default function ExternalDashboardManagementPage() {
const key = role.key.toUpperCase();
if (key.includes("COMPANY")) map[role.id] = "COMPANY";
else if (key.includes("CUSTOMER") || key.includes("SERVICE_SEEKER"))
map[role.id] = "CUSTOMER";
{map[role.id] = "CUSTOMER";}
else if (key.includes("JOB_SEEKER") || key.includes("JOBSEEKER")) map[role.id] = "JOB_SEEKER";
else map[role.id] = "PROFESSIONAL";
}
@ -590,9 +590,7 @@ export default function ExternalDashboardManagementPage() {
setRoles(
roleRows
.filter((r: any) => {
return r?.status === "ACTIVE" || r?.status === "INACTIVE";
})
.filter((r: any) => r?.status === "ACTIVE" || r?.status === "INACTIVE")
.map((r: any) => ({
id: String(r?.id || ""),
key: String(r?.code || r?.key || "").toUpperCase(),
@ -873,8 +871,8 @@ export default function ExternalDashboardManagementPage() {
</div>
<div style="display:flex;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 16px;background:#FAFAFA">
{(["general", "tabs", "sidebar", "fields", "theme", "preview", "full_preview"] as const).map(
(tab) => (
<For each={["general", "tabs", "sidebar", "fields", "theme", "preview", "full_preview"] as const}>
{(tab) => (
<button
type="button"
onClick={() => setFormTab(tab)}
@ -885,8 +883,8 @@ export default function ExternalDashboardManagementPage() {
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13" />
</Show>
</button>
)
)}
)}
</For>
</div>
<div style="padding:24px">
@ -1389,25 +1387,29 @@ export default function ExternalDashboardManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
[
["name_asc", "Name (A-Z)"],
["name_desc", "Name (Z-A)"],
["updated_desc", "Updated (Newest)"],
["updated_asc", "Updated (Oldest)"],
] as const
).map(([key, label]) => (
<button
type="button"
onClick={() => {
setSortBy(key);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === key ? "#FF5E13" : "#374151"};background:${sortBy() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
))}
<For
each={
[
["name_asc", "Name (A-Z)"],
["name_desc", "Name (Z-A)"],
["updated_desc", "Updated (Newest)"],
["updated_asc", "Updated (Oldest)"],
] as const
}
>
{([key, label]) => (
<button
type="button"
onClick={() => {
setSortBy(key);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === key ? "#FF5E13" : "#374151"};background:${sortBy() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
)}
</For>
</div>
</Show>
</div>
@ -1434,24 +1436,28 @@ export default function ExternalDashboardManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
[
["all", "All Status"],
["active", "Active"],
["inactive", "Inactive"],
] as const
).map(([key, label]) => (
<button
type="button"
onClick={() => {
setStatusFilter(key);
setFilterMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === key ? "#FF5E13" : "#374151"};background:${statusFilter() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
))}
<For
each={
[
["all", "All Status"],
["active", "Active"],
["inactive", "Inactive"],
] as const
}
>
{([key, label]) => (
<button
type="button"
onClick={() => {
setStatusFilter(key);
setFilterMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === key ? "#FF5E13" : "#374151"};background:${statusFilter() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
)}
</For>
</div>
</Show>
</div>
@ -1479,11 +1485,13 @@ export default function ExternalDashboardManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{["Name", "Code", "Role", "Widgets", "Status", "Updated", "Actions"].map((h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
))}
<For each={["Name", "Code", "Role", "Widgets", "Status", "Updated", "Actions"]}>
{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
)}
</For>
</tr>
</thead>
<tbody>

View file

@ -220,18 +220,14 @@ export default function ExternalRoleManagementPage() {
}
};
const getPermission = (moduleKey: string): RolePermission | undefined => {
return rolePermissions().find((p) => p.module_key === moduleKey);
};
const getPermission = (moduleKey: string): RolePermission | undefined => rolePermissions().find((p) => p.module_key === moduleKey);
const hasAnyPermission = (moduleKey: string) => {
const p = getPermission(moduleKey);
return p && (p.can_view || p.can_list || p.can_create || p.can_update || p.can_delete);
};
const permissionKeyFor = (moduleKey: string, action: string) => {
return `${moduleKey}_${action.toLowerCase()}`;
};
const permissionKeyFor = (moduleKey: string, action: string) => `${moduleKey}_${action.toLowerCase()}`;
const isAllSelectedForAction = (action: PermissionAction) => {
const actionKey = action.toLowerCase();
@ -439,11 +435,13 @@ export default function ExternalRoleManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{["Role Name", "Key", "Persona Type", "Users", "Status", "Actions"].map((h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
))}
<For each={["Role Name", "Key", "Persona Type", "Users", "Status", "Actions"]}>
{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
)}
</For>
</tr>
</thead>
<tbody>
@ -535,22 +533,25 @@ export default function ExternalRoleManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "permissions", "settings"] as const).map((tab, i) => {
const labels = ["General Information", "Module Access", "Role Settings"];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
<For each={["general", "permissions", "settings"] as const}>
{(tab, getI) => {
const labels = ["General Information", "Module Access", "Role Settings"];
const i = getI();
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
}}
</For>
</div>
<div style="padding:24px">

View file

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

View file

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

View file

@ -1,18 +0,0 @@
import { A } from '@solidjs/router';
export default function HelpSupportBridgePage() {
return (
<div class="flex flex-col -mx-6 -mt-6 min-h-full">
<div class="bg-white border-b border-gray-200 px-6 py-4">
<h1 class="text-xl font-semibold text-gray-900">Support Bridge</h1>
<p class="text-sm text-gray-500 mt-0.5">This legacy help bridge now routes through the unified Support Management module.</p>
</div>
<div class="p-6">
<div class="table-card p-5 flex items-center gap-4">
<p class="text-sm text-slate-600 flex-1">Use the Support Management module to handle all support tickets and help requests.</p>
<A class="btn-primary" href="/admin/support">Open Support Management</A>
</div>
</div>
</div>
);
}

View file

@ -271,22 +271,25 @@ export default function AdminHomePage() {
kpi_credits_purchased: "credits",
};
if (metrics.loading) return { state: "pending", statusLabel: "Loading..." };
// Guard against error state — accessing metrics() when errored throws.
if (metrics.error) return { state: "empty", statusLabel: "No Data" };
const m = metrics()?.kpis?.find((k: any) => k.id === idMap[key]);
if (m) return { state: "live", statusLabel: "Live Data", data: m };
return { state: "empty", statusLabel: "No Data" };
}
if (metrics.loading) return { state: "pending", statusLabel: "Loading..." };
if (metrics.error) return { state: "empty", statusLabel: "No Data" };
const m = metrics();
if (key === "chart_leads_trend") {
const data = m?.trend_series;
if (data && data.length > 0)
return { state: "live", statusLabel: "Live Data", data: { trend_series: data } };
{return { state: "live", statusLabel: "Live Data", data: { trend_series: data } };}
return { state: "empty", statusLabel: "No Data" };
}
if (key === "chart_revenue_overview") {
const data = m?.rev_series;
if (data && data.length > 0)
return { state: "live", statusLabel: "Live Data", data: { rev_series: data } };
{return { state: "live", statusLabel: "Live Data", data: { rev_series: data } };}
return { state: "empty", statusLabel: "No Data" };
}
const meta = WIDGET_META[key];
@ -611,8 +614,8 @@ export default function AdminHomePage() {
<Show when={isOpenMenu()}>
<div class="absolute right-0 top-10 z-20 w-40 rounded-lg border border-[#E5E7EB] bg-white p-1 shadow-md">
{["Edit Widget", "Duplicate Widget", "Hide Widget", "Remove Widget"].map(
(item) => (
<For each={["Edit Widget", "Duplicate Widget", "Hide Widget", "Remove Widget"]}>
{(item) => (
<button
type="button"
class="block w-full rounded-md px-2 py-2 text-left text-xs font-medium text-[#111827] hover:bg-[#F9FAFB]"
@ -620,8 +623,8 @@ export default function AdminHomePage() {
>
{item}
</button>
)
)}
)}
</For>
</div>
</Show>
</div>

View file

@ -257,12 +257,12 @@ export default function InternalDashboardManagementPage() {
</div>
<div style="display:flex;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 16px;background:#FAFAFA">
{(['general', 'tabs', 'sidebar', 'fields', 'preview'] as const).map((tab) => (
<For each={['general', 'tabs', 'sidebar', 'fields', 'preview'] as const}>{(tab) => (
<button type="button" onClick={() => setFormTab(tab)} style={`position:relative;padding:12px 10px;font-size:12px;font-weight:700;border:none;background:none;cursor:pointer;color:${formTab() === tab ? '#FF5E13' : '#6B7280'}`}>
{tab.toUpperCase()}
<Show when={formTab() === tab}><span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13" /></Show>
</button>
))}
)}</For>
</div>
<div style="padding:24px">
@ -353,14 +353,14 @@ export default function InternalDashboardManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
['name_asc', 'Name (A-Z)'],
['name_desc', 'Name (Z-A)'],
['updated_desc', 'Updated (Newest)'],
['updated_asc', 'Updated (Oldest)'],
] as const).map(([key, label]) => (
] as const}>{([key, label]) => (
<button type="button" onClick={() => { setSortBy(key); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === key ? '#FF5E13' : '#374151'};background:${sortBy() === key ? '#FFF1EB' : 'transparent'}`}>{label}</button>
))}
)}</For>
</div>
</Show>
</div>
@ -371,13 +371,13 @@ export default function InternalDashboardManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
['all', 'All Status'],
['active', 'Active'],
['inactive', 'Inactive'],
] as const).map(([key, label]) => (
] as const}>{([key, label]) => (
<button type="button" onClick={() => { setStatusFilter(key); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === key ? '#FF5E13' : '#374151'};background:${statusFilter() === key ? '#FFF1EB' : 'transparent'}`}>{label}</button>
))}
)}</For>
</div>
</Show>
</div>
@ -391,9 +391,9 @@ export default function InternalDashboardManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Name', 'Code', 'Role', 'Widgets', 'Status', 'Updated', 'Actions'].map((h) => (
<For each={['Name', 'Code', 'Role', 'Widgets', 'Status', 'Updated', 'Actions']}>{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -140,11 +140,11 @@ export default function JobsManagementPage() {
<Show when={view() === 'list'}>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{([
<For each={[
{ key: 'all', label: 'All Jobs', action: () => setListTab('all') },
{ key: 'active', label: 'Active', action: () => setListTab('active') },
{ key: 'pending', label: 'Pending Approval', action: () => setListTab('pending') },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={tab.action}
@ -152,7 +152,7 @@ export default function JobsManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
@ -173,16 +173,16 @@ export default function JobsManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:190px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'posted_newest', label: 'Posted (Newest)' },
{ key: 'posted_oldest', label: 'Posted (Oldest)' },
{ key: 'title_asc', label: 'Title (A-Z)' },
{ key: 'title_desc', label: 'Title (Z-A)' },
] as const).map((item) => (
] as const}>{(item) => (
<button type="button" onClick={() => { setSortBy(item.key); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -196,17 +196,17 @@ export default function JobsManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'all', label: 'All Status' },
{ key: 'active', label: 'Active' },
{ key: 'pending', label: 'Pending Approval' },
{ key: 'draft', label: 'Draft' },
{ key: 'closed', label: 'Closed' },
] as const).map((item) => (
] as const}>{(item) => (
<button type="button" onClick={() => { setStatusFilter(item.key); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'}`}>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -223,9 +223,9 @@ export default function JobsManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Job Title', 'Company', 'Rate', 'Location', 'Status', 'Actions'].map(h => (
<For each={['Job Title', 'Company', 'Rate', 'Location', 'Status', 'Actions']}>{h => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{h}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -109,16 +109,16 @@ export default function LeadsPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:42px;z-index:30;min-width:180px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'newest', label: 'Newest First' },
{ key: 'oldest', label: 'Oldest First' },
{ key: 'title_asc', label: 'Title (A-Z)' },
{ key: 'title_desc', label: 'Title (Z-A)' },
] as const).map((item) => (
] as const}>{(item) => (
<button type="button" onClick={() => { setSortBy(item.key); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === item.key ? '#FF5E13' : '#374151'};background:${sortBy() === item.key ? '#FFF1EB' : 'transparent'}`}>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -133,16 +133,16 @@ export default function LeadsPage() {
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">Status</p>
{([
<For each={[
{ key: '', label: 'All Statuses' },
{ key: 'OPEN', label: 'Open' },
{ key: 'ACTIVE', label: 'Active' },
{ key: 'PENDING', label: 'Pending' },
{ key: 'CLOSED', label: 'Closed' },
{ key: 'CANCELLED', label: 'Cancelled' },
] as const).map((item) => (
] as const}>{(item) => (
<button type="button" onClick={() => { setStatusFilter(item.key); setFilterMenuOpen(false); }} style={`display:block;width:100%;border:none;background:${statusFilter() === item.key ? '#FFF1EB' : 'transparent'};color:${statusFilter() === item.key ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;text-align:left;font-size:13px;cursor:pointer`}>{item.label}</button>
))}
)}</For>
<div style="height:1px;background:#F3F4F6;margin:6px 0" />
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">Role</p>
<button type="button" onClick={() => { setRoleFilter(''); setFilterMenuOpen(false); }} style={`display:block;width:100%;border:none;background:${roleFilter() === '' ? '#FFF1EB' : 'transparent'};color:${roleFilter() === '' ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;text-align:left;font-size:13px;cursor:pointer`}>All Roles</button>

View file

@ -289,12 +289,12 @@ export default function PricingPage() {
let r = packages() ?? [];
const q = search().toLowerCase();
if (q)
r = r.filter(
{r = r.filter(
(p) =>
p.name.toLowerCase().includes(q) ||
p.package_type.toLowerCase().includes(q) ||
(p.applicable_roles ?? []).some((rl) => rl.toLowerCase().includes(q)),
);
);}
if (typeFilter() !== 'all') r = r.filter((p) => p.package_type === typeFilter());
if (statusFilter() === 'active') r = r.filter((p) => p.is_active);
if (statusFilter() === 'inactive') r = r.filter((p) => !p.is_active);
@ -428,9 +428,7 @@ export default function PricingPage() {
});
};
const getTypeLabel = (type: string) => {
return PACKAGE_TYPES.find((t) => t.value === type)?.label || type;
};
const getTypeLabel = (type: string) => PACKAGE_TYPES.find((t) => t.value === type)?.label || type;
return (
<div class="w-full space-y-6 pb-8">
@ -448,7 +446,7 @@ export default function PricingPage() {
</Show>
<div class="bg-white border-b border-gray-200 px-6 flex items-center gap-8 sticky top-0 z-10">
{(["packages", "create", "ai_packages"] as const).map((t) => (
<For each={["packages", "create", "ai_packages"] as const}>{(t) => (
<button
type="button"
class={
@ -460,7 +458,7 @@ export default function PricingPage() {
>
{t === "packages" ? "Packages" : t === "create" ? "Create Package" : "AI Credit Packages"}
</button>
))}
)}</For>
</div>
<div>

View file

@ -1,5 +1,5 @@
import { A, useNavigate, useSearchParams } from '@solidjs/router';
import { createMemo, createResource, Show } from 'solid-js';
import { For, createMemo, createResource, Show } from 'solid-js';
import ExternalRoleTabs from '~/components/admin/ExternalRoleTabs';
const API = '';
@ -104,7 +104,7 @@ export default function RoleUiConfigsViewPage() {
</Show>
<div style="margin-top:12px;display:flex;flex-direction:column;gap:10px">
{(rows() || []).map((item) => {
<For each={rows() || []}>{(item) => {
const isActive = roleKey().toLowerCase() === item.roleKey.toLowerCase();
return (
<button
@ -122,7 +122,7 @@ export default function RoleUiConfigsViewPage() {
<p class="notice" style="margin-top:8px">{item.enabledModules.length} modules profile flow {item.onboardingSchemaId || 'Dashboard-first'}</p>
</button>
);
})}
}}</For>
</div>
</section>
@ -169,7 +169,7 @@ export default function RoleUiConfigsViewPage() {
<p class="kv-value">
<Show when={approvalFlags().length > 0} fallback={'None'}>
<span style="display:inline-flex;gap:6px;flex-wrap:wrap">
{approvalFlags().map((flag) => <span class="status-pill status-pending">{flag}</span>)}
<For each={approvalFlags()}>{(flag) => <span class="status-pill status-pending">{flag}</span>}</For>
</span>
</Show>
</p>
@ -181,7 +181,7 @@ export default function RoleUiConfigsViewPage() {
<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">
{selected()!.enabledModules.map((moduleKey) => <span class="meta-chip">{moduleKey}</span>)}
<For each={selected()!.enabledModules}>{(moduleKey) => <span class="meta-chip">{moduleKey}</span>}</For>
</div>
</Show>
</section>
@ -193,20 +193,20 @@ export default function RoleUiConfigsViewPage() {
<thead>
<tr>
<th>Module</th>
{permissionActions.map((action) => <th class="text-right">{action}</th>)}
<For each={permissionActions}>{(action) => <th class="text-right">{action}</th>}</For>
</tr>
</thead>
<tbody>
{selected()!.enabledModules.map((moduleKey) => (
<For each={selected()!.enabledModules}>{(moduleKey) => (
<tr>
<td>{moduleKey}</td>
{permissionActions.map((action) => (
<For each={permissionActions}>{(action) => (
<td class="text-right">
<input type="checkbox" checked={(selected()!.permissions[moduleKey] || []).includes(action)} disabled />
</td>
))}
)}</For>
</tr>
))}
)}</For>
</tbody>
</table>
</div>

View file

@ -245,13 +245,11 @@ export default function EditInternalRolePage() {
<Show when={!role.loading}>
{/* Inner sub-tabs */}
<div class="flex border-b border-[#e5e7eb] px-6 gap-6">
{(
[
{ key: 'general', label: 'General Information' },
{ key: 'module', label: 'Module Access' },
{ key: 'settings', label: 'Role Settings' },
] as const
).map((t) => (
<For each={[
{ key: 'general', label: 'General Information' },
{ key: 'module', label: 'Module Access' },
{ key: 'settings', label: 'Role Settings' },
] as const}>{(t) => (
<button
onClick={() => setSubTab(t.key)}
class={`relative py-3.5 text-[13px] font-medium transition-colors ${
@ -265,7 +263,7 @@ export default function EditInternalRolePage() {
<span class="absolute bottom-0 left-0 right-0 h-[2px] bg-[#0D0D2A] rounded-t" />
</Show>
</button>
))}
)}</For>
</div>
{/* Error banner */}

View file

@ -270,26 +270,26 @@ export default function CreateInternalRolePage() {
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm overflow-hidden">
{/* Sub-tabs */}
<div class="flex items-center gap-1 border-b border-[#F3F4F6] px-6">
{(
[
{ key: "general", label: "General Information" },
{ key: "module", label: "Module Access" },
{ key: "settings", label: "Role Settings" },
] as const
).map((t) => (
<button
type="button"
onClick={() => setSubTab(t.key)}
class={`relative px-4 py-4 text-[13px] font-semibold transition-colors ${
subTab() === t.key ? "text-[#111827]" : "text-[#9CA3AF] hover:text-[#6B7280]"
}`}
>
{t.label}
<Show when={subTab() === t.key}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</Show>
</button>
))}
<For each={[
{ key: "general", label: "General Information" },
{ key: "module", label: "Module Access" },
{ key: "settings", label: "Role Settings" },
] as const}>
{(t) => (
<button
type="button"
onClick={() => setSubTab(t.key)}
class={`relative px-4 py-4 text-[13px] font-semibold transition-colors ${
subTab() === t.key ? "text-[#111827]" : "text-[#9CA3AF] hover:text-[#6B7280]"
}`}
>
{t.label}
<Show when={subTab() === t.key}>
<span class="absolute inset-x-0 bottom-0 h-[2px] rounded-t-full bg-[#FF5E13]" />
</Show>
</button>
)}
</For>
</div>
{/* Error banner */}
@ -412,24 +412,26 @@ export default function CreateInternalRolePage() {
<td class="px-5 py-3.5 text-[13px] font-medium text-[#0D0D2A]">
{module}
</td>
{ACTIONS.map((action) => {
const p = () => byAction()[action];
return (
<td class="px-4 py-3.5 text-center">
<Show
when={p()}
fallback={<span class="text-[#d1d5db] text-xs"></span>}
>
<input
type="checkbox"
checked={selectedKeys().has(p()!.key)}
onChange={() => toggleKey(p()!.key)}
class="h-4 w-4 accent-[#FF5E13] cursor-pointer"
/>
</Show>
</td>
);
})}
<For each={ACTIONS}>
{(action) => {
const p = () => byAction()[action];
return (
<td class="px-4 py-3.5 text-center">
<Show
when={p()}
fallback={<span class="text-[#d1d5db] text-xs"></span>}
>
<input
type="checkbox"
checked={selectedKeys().has(p()!.key)}
onChange={() => toggleKey(p()!.key)}
class="h-4 w-4 accent-[#FF5E13] cursor-pointer"
/>
</Show>
</td>
);
}}
</For>
<td class="px-4 py-3.5 text-center">
<input
type="checkbox"

View file

@ -281,9 +281,9 @@ export default function RoleManagementPage() {
sorted.sort((a, b) => {
if (sortBy() === "name_desc") return b.name.localeCompare(a.name);
if (sortBy() === "users_desc")
return Number(b.usersAssigned || 0) - Number(a.usersAssigned || 0);
{return Number(b.usersAssigned || 0) - Number(a.usersAssigned || 0);}
if (sortBy() === "users_asc")
return Number(a.usersAssigned || 0) - Number(b.usersAssigned || 0);
{return Number(a.usersAssigned || 0) - Number(b.usersAssigned || 0);}
return a.name.localeCompare(b.name);
});
return sorted;
@ -585,35 +585,35 @@ export default function RoleManagementPage() {
{/* ── LIST VIEW ── */}
<Show when={view() === "list"}>
<div style="margin-top:24px;display:flex;align-items:center;gap:24px;border-bottom:1px solid #E5E7EB">
{(
[
{
key: "all",
label: "All Roles",
action: () => {
setListTab("all");
void load();
},
<For each={[
{
key: "all",
label: "All Roles",
action: () => {
setListTab("all");
void load();
},
{
key: "create",
label: "Create Role",
action: () => {
setListTab("create");
openCreate();
},
},
{
key: "create",
label: "Create Role",
action: () => {
setListTab("create");
openCreate();
},
{ key: "view", label: "View Role", action: () => setListTab("view") },
] as const
).map((tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px" : "color:#6B7280"}`}
>
{tab.label}
</button>
))}
},
{ key: "view", label: "View Role", action: () => setListTab("view") },
] as const}>
{(tab) => (
<button
type="button"
onClick={tab.action}
style={`padding-bottom:12px;font-size:14px;font-weight:500;background:none;border:none;cursor:pointer;${listTab() === tab.key ? "color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px" : "color:#6B7280"}`}
>
{tab.label}
</button>
)}
</For>
</div>
<Show when={error()}>
@ -659,25 +659,25 @@ export default function RoleManagementPage() {
</button>
<Show when={sortMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
[
["name_asc", "Name (A-Z)"],
["name_desc", "Name (Z-A)"],
["users_desc", "Users (High-Low)"],
["users_asc", "Users (Low-High)"],
] as const
).map(([key, label]) => (
<button
type="button"
onClick={() => {
setSortBy(key);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === key ? "#FF5E13" : "#374151"};background:${sortBy() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
))}
<For each={[
["name_asc", "Name (A-Z)"],
["name_desc", "Name (Z-A)"],
["users_desc", "Users (High-Low)"],
["users_asc", "Users (Low-High)"],
] as const}>
{([key, label]) => (
<button
type="button"
onClick={() => {
setSortBy(key);
setSortMenuOpen(false);
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === key ? "#FF5E13" : "#374151"};background:${sortBy() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
)}
</For>
</div>
</Show>
</div>
@ -704,25 +704,25 @@ export default function RoleManagementPage() {
</button>
<Show when={filterMenuOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
[
["all", "All Status"],
["ACTIVE", "Active"],
["INACTIVE", "Inactive"],
] as const
).map(([key, label]) => (
<button
type="button"
onClick={() => {
setStatusFilter(key as any);
setFilterMenuOpen(false);
void load();
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === key ? "#FF5E13" : "#374151"};background:${statusFilter() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
))}
<For each={[
["all", "All Status"],
["ACTIVE", "Active"],
["INACTIVE", "Inactive"],
] as const}>
{([key, label]) => (
<button
type="button"
onClick={() => {
setStatusFilter(key as any);
setFilterMenuOpen(false);
void load();
}}
style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === key ? "#FF5E13" : "#374151"};background:${statusFilter() === key ? "#FFF1EB" : "transparent"}`}
>
{label}
</button>
)}
</For>
</div>
</Show>
</div>
@ -751,7 +751,7 @@ export default function RoleManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{[
<For each={[
"Role Name",
"Role Key",
"Department",
@ -759,11 +759,13 @@ export default function RoleManagementPage() {
"Permissions",
"Status",
"Actions",
].map((h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
))}
]}>
{(h) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">
{h}
</th>
)}
</For>
</tr>
</thead>
<tbody>
@ -947,22 +949,25 @@ export default function RoleManagementPage() {
<div style="margin-top:24px;border-radius:16px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px">
{(["general", "permissions", "settings"] as const).map((tab, i) => {
const labels = ["General Information", "Module Access", "Role Settings"];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
<For each={["general", "permissions", "settings"] as const}>
{(tab, getI) => {
const i = getI();
const labels = ["General Information", "Module Access", "Role Settings"];
const active = () => formTab() === tab;
return (
<button
type="button"
onClick={() => setFormTab(tab)}
style={`position:relative;padding:14px 8px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
}}
</For>
</div>
<div style="padding:24px">
@ -1017,15 +1022,17 @@ export default function RoleManagementPage() {
<label style="display:block">
<span style="font-size:13px;font-weight:600;color:#374151">Status</span>
<div style="margin-top:6px;display:flex;gap:8px">
{(["ACTIVE", "INACTIVE"] as const).map((s) => (
<button
type="button"
onClick={() => setStatus(s)}
style={`flex:1;height:40px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? "#FF5E13" : "#E5E7EB"};background:${status() === s ? "#FFF3EE" : "white"};color:${status() === s ? "#FF5E13" : "#6B7280"}`}
>
{s === "ACTIVE" ? "Active" : "Inactive"}
</button>
))}
<For each={["ACTIVE", "INACTIVE"] as const}>
{(s) => (
<button
type="button"
onClick={() => setStatus(s)}
style={`flex:1;height:40px;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid ${status() === s ? "#FF5E13" : "#E5E7EB"};background:${status() === s ? "#FFF3EE" : "white"};color:${status() === s ? "#FF5E13" : "#6B7280"}`}
>
{s === "ACTIVE" ? "Active" : "Inactive"}
</button>
)}
</For>
</div>
</label>
</div>
@ -1206,22 +1213,25 @@ export default function RoleManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:4px;border-bottom:1px solid #E5E7EB;padding:0 24px;background:#FAFAFA">
{(["permissions", "users", "logs"] as const).map((tab, i) => {
const labels = ["Permissions", "Assigned Users", "Activity Logs"];
const active = () => detailTab() === tab;
return (
<button
type="button"
onClick={() => setDetailTab(tab)}
style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
})}
<For each={["permissions", "users", "logs"] as const}>
{(tab, getI) => {
const i = getI();
const labels = ["Permissions", "Assigned Users", "Activity Logs"];
const active = () => detailTab() === tab;
return (
<button
type="button"
onClick={() => setDetailTab(tab)}
style={`position:relative;padding:14px 12px;font-size:13px;font-weight:600;background:none;border:none;cursor:pointer;color:${active() ? "#FF5E13" : "#6B7280"}`}
>
{labels[i]}
<Show when={active()}>
<span style="position:absolute;left:0;right:0;bottom:0;height:2px;background:#FF5E13;border-radius:2px 2px 0 0" />
</Show>
</button>
);
}}
</For>
</div>
<div style="padding:24px">

View file

@ -128,24 +128,26 @@ export default function ExternalRolesPage() {
{/* Stat cards */}
<div class="grid grid-cols-2 gap-5 lg:grid-cols-4">
{([
<For each={[
{ label: 'Total Roles', value: () => stats().total, Icon: Globe, bg: 'bg-[#FFF1EB]', color: 'text-[#FF5E13]' },
{ label: 'Active Roles', value: () => stats().active, Icon: ShieldCheck, bg: 'bg-[#ECFDF5]', color: 'text-[#059669]' },
{ label: 'Inactive Roles', value: () => stats().inactive, Icon: ShieldOff, bg: 'bg-[#FEF2F2]', color: 'text-[#DC2626]' },
{ label: 'Total Modules', value: () => roles().reduce((a, r) => a + r.enabledModules.length, 0), Icon: Layers, bg: 'bg-[#EFF6FF]', color: 'text-[#2563EB]' },
] as const).map(s => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class="flex items-start justify-between">
<div>
<p class="text-[12px] font-medium text-[#6B7280]">{s.label}</p>
<p class="mt-3 text-[32px] font-bold leading-none text-[#111827]">{s.value()}</p>
</div>
<div class={`flex h-11 w-11 items-center justify-center rounded-xl ${s.bg}`}>
<s.Icon size={20} class={s.color} />
]}>
{(s) => (
<div class="rounded-2xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
<div class="flex items-start justify-between">
<div>
<p class="text-[12px] font-medium text-[#6B7280]">{s.label}</p>
<p class="mt-3 text-[32px] font-bold leading-none text-[#111827]">{s.value()}</p>
</div>
<div class={`flex h-11 w-11 items-center justify-center rounded-xl ${s.bg}`}>
<s.Icon size={20} class={s.color} />
</div>
</div>
</div>
</div>
))}
)}
</For>
</div>
{/* Table card */}

View file

@ -182,9 +182,9 @@ export default function SupportPage() {
const sorted = [...all];
sorted.sort((a, b) => {
if (sortBy() === "oldest")
return new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime();
{return new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime();}
if (sortBy() === "priority")
return (priorityRank[b.priority] || 0) - (priorityRank[a.priority] || 0);
{return (priorityRank[b.priority] || 0) - (priorityRank[a.priority] || 0);}
return new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime();
});
return sorted;

View file

@ -307,29 +307,31 @@ export default function TaxPage() {
<tr><td colspan="5" style="text-align:center;padding:32px;color:#94a3b8">No records found.</td></tr>
</Show>
<Show when={!taxes.loading && !taxes.error && filteredTaxes().length > 0}>
{filteredTaxes().map((item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{item.name}</td>
<td class="text-slate-500">{item.rate}%</td>
<td class="text-slate-500">{item.description || '—'}</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.is_active !== false ? 'active' : ''}`}>
{item.is_active !== false ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<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"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
{deleting() === item.id ? 'Deleting...' : 'Delete'}
</button>
</div>
</td>
</tr>
))}
<For each={filteredTaxes()}>
{(item) => (
<tr class="hover:bg-slate-50">
<td class="font-semibold text-slate-900">{item.name}</td>
<td class="text-slate-500">{item.rate}%</td>
<td class="text-slate-500">{item.description || '—'}</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.is_active !== false ? 'active' : ''}`}>
{item.is_active !== false ? 'Active' : 'Inactive'}
</span>
</td>
<td>
<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"
disabled={deleting() === item.id}
onClick={() => handleDelete(item.id, item.name)}
>
{deleting() === item.id ? 'Deleting...' : 'Delete'}
</button>
</div>
</td>
</tr>
)}
</For>
</Show>
</tbody>
</table>

View file

@ -1,5 +1,5 @@
import { A, useNavigate, useParams } from "@solidjs/router";
import { createMemo, createResource, createSignal, Show } from "solid-js";
import { createMemo, createResource, createSignal, For, Show } from "solid-js";
const API = "";
@ -188,9 +188,9 @@ export default function EditUserPage() {
>
<option value="">Select role</option>
<Show when={!roles.loading}>
{roles()?.map((r) => (
<option value={r.id}>{r.name}</option>
))}
<For each={roles() ?? []}>
{(r) => <option value={r.id}>{r.name}</option>}
</For>
</Show>
</select>
</div>

View file

@ -193,20 +193,20 @@ export default function UsersManagementPage() {
if (listTab() === "no_role") list = list.filter((u) => u.registeredRoles.length === 0);
if (listTab() === "registered") list = list.filter((u) => u.registeredRoles.length > 0);
if (listTab() === "pending")
list = list.filter(
{list = list.filter(
(u) =>
u.registeredRoles.some((r) => r.status === "PENDING") ||
u.verificationStatus === "PENDING" ||
u.verificationStatus === "IN_REVIEW"
);
);}
if (listTab() === "approved") list = list.filter((u) => u.verificationStatus === "VERIFIED");
if (listTab() === "suspended") list = list.filter((u) => u.accountStatus === "SUSPENDED");
if (statusFilter() === "active") list = list.filter((u) => u.accountStatus === "ACTIVE");
if (statusFilter() === "pending")
list = list.filter(
{list = list.filter(
(u) => u.verificationStatus === "PENDING" || u.verificationStatus === "IN_REVIEW"
);
);}
if (statusFilter() === "suspended") list = list.filter((u) => u.accountStatus === "SUSPENDED");
if (statusFilter() === "blocked") list = list.filter((u) => u.accountStatus === "BLOCKED");
@ -290,7 +290,7 @@ export default function UsersManagementPage() {
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: nextStatus as any } : r))
);
if (selectedUser()?.id === user.id)
setSelectedUser({ ...user, accountStatus: nextStatus as any });
{setSelectedUser({ ...user, accountStatus: nextStatus as any });}
try {
const r = await fetch(`/api/admin/users/${user.id}/status`, {
@ -310,7 +310,7 @@ export default function UsersManagementPage() {
prev.map((r) => (r.id === user.id ? { ...r, accountStatus: previousStatus } : r))
);
if (selectedUser()?.id === user.id)
setSelectedUser({ ...user, accountStatus: previousStatus });
{setSelectedUser({ ...user, accountStatus: previousStatus });}
setActionError(`Failed to update status for ${user.name}. Please try again.`);
}
};
@ -346,14 +346,14 @@ export default function UsersManagementPage() {
<Show when={listTab() !== "view"}>
<div style="display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:14px">
{[
<For each={[
{ label: "Total Users", value: metrics().totalUsers, accent: "#111827" },
{ label: "No Role Users", value: metrics().noRoleUsers, accent: "#111827" },
{ label: "Active Role Users", value: metrics().activeRoleUsers, accent: "#111827" },
{ label: "Pending Roles", value: metrics().pendingRoles, accent: "#111827" },
{ label: "Suspended", value: metrics().suspended, accent: "#111827" },
{ label: "New This Month", value: metrics().newThisMonth, accent: "#111827" },
].map((card) => (
]}>{(card) => (
<div style="border:1px solid #E5E7EB;border-radius:14px;background:white;padding:16px 18px;min-height:100px">
<p style="font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:#667085">
{card.label}
@ -364,13 +364,12 @@ export default function UsersManagementPage() {
{card.value}
</p>
</div>
))}
)}</For>
</div>
</Show>
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
{(
[
<For each={[
{ key: "all", label: `All Users (${rows().length})` },
{ key: "no_role", label: `No Role Users (${metrics().noRoleUsers})` },
{ key: "registered", label: `Registered Role Users (${metrics().activeRoleUsers})` },
@ -378,8 +377,7 @@ export default function UsersManagementPage() {
{ key: "approved", label: "Approved Users" },
{ key: "suspended", label: `Suspended (${metrics().suspended})` },
{ key: "view", label: "View User" },
] as const
).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={() => setListTab(tab.key)}
@ -387,7 +385,7 @@ export default function UsersManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<Show when={listTab() === "view"}>
@ -509,8 +507,7 @@ export default function UsersManagementPage() {
</div>
<div style="display:flex;align-items:center;gap:22px;min-height:44px;border-bottom:1px solid #E5E7EB;padding:0 22px;overflow:auto">
{(
[
<For each={[
{ key: "overview", label: "Overview" },
{ key: "personal", label: "Personal Details" },
{ key: "roles", label: "Registered Roles" },
@ -518,8 +515,7 @@ export default function UsersManagementPage() {
{ key: "verification", label: "Verification & Approval" },
{ key: "activity", label: "Activity" },
{ key: "notes", label: "Notes" },
] as const
).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={() => setDetailTab(tab.key)}
@ -527,7 +523,7 @@ export default function UsersManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<div style="padding:22px">
@ -721,14 +717,12 @@ export default function UsersManagementPage() {
</button>
<Show when={sortOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{(
[
<For each={[
{ key: "newest", label: "Newest First" },
{ key: "oldest", label: "Oldest First" },
{ key: "name_asc", label: "Name (A-Z)" },
{ key: "name_desc", label: "Name (Z-A)" },
] as const
).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => {
@ -739,7 +733,7 @@ export default function UsersManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -770,15 +764,13 @@ export default function UsersManagementPage() {
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">
Status
</p>
{(
[
<For each={[
{ key: "all", label: "All Status" },
{ key: "active", label: "Active" },
{ key: "pending", label: "Pending" },
{ key: "suspended", label: "Suspended" },
{ key: "blocked", label: "Blocked" },
] as const
).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => {
@ -789,21 +781,19 @@ export default function UsersManagementPage() {
>
{item.label}
</button>
))}
)}</For>
<div style="height:1px;background:#F3F4F6;margin:6px 0" />
<p style="font-size:11px;color:#9CA3AF;text-transform:uppercase;letter-spacing:0.05em;padding:4px 8px">
Role Group
</p>
{(
[
<For each={[
{ key: "all", label: "All Roles" },
{ key: "no_role", label: "No Role Users" },
{ key: "professional", label: "Professionals" },
{ key: "company", label: "Companies" },
{ key: "jobseeker", label: "Job Seekers" },
{ key: "customer", label: "Service Seekers" },
] as const
).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => {
@ -814,7 +804,7 @@ export default function UsersManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>

View file

@ -1,8 +0,0 @@
import { useNavigate } from '@solidjs/router';
import { onMount } from 'solid-js';
export default function VerificationStatusPage() {
const navigate = useNavigate();
onMount(() => navigate('/admin/verification', { replace: true }));
return null;
}

View file

@ -391,14 +391,14 @@ export default function VerificationReviewDetailPage() {
</div>
<div style="margin-top:12px;display:flex;align-items:center;gap:20px;border-bottom:1px solid #E5E7EB;overflow:auto">
{([
<For each={[
{ key: 'overview', label: 'Overview' },
{ key: 'submitted', label: 'Submitted Details' },
{ key: 'documents', label: 'Documents' },
{ key: 'missing', label: 'Missing Information' },
{ key: 'requested', label: `Requested Actions (${requestCount()})` },
{ key: 'activity', label: 'Activity Log' },
] as const).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => setTab(item.key)}
@ -406,7 +406,7 @@ export default function VerificationReviewDetailPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
<div style="margin-top:12px;display:grid;grid-template-columns:2fr 1fr;gap:12px;align-items:start">
@ -535,7 +535,7 @@ export default function VerificationReviewDetailPage() {
<Show when={activeAction() === 'none' && tab() === 'documents'}>
<div class="table-card" style="border-radius:14px;overflow:hidden">
<table class="data-table w-full text-sm">
<thead><tr>{['Document', 'File', 'Status'].map((h) => <th>{h}</th>)}</tr></thead>
<thead><tr><For each={['Document', 'File', 'Status']}>{(h) => <th>{h}</th>}</For></tr></thead>
<tbody>
<Show when={uploadedDocs().length > 0} fallback={<tr><td colSpan={3} style="text-align:center;padding:24px;color:#94A3B8">No documents uploaded yet.</td></tr>}>
<For each={uploadedDocs()}>{(doc) => (

View file

@ -644,10 +644,10 @@ export default function VerificationManagementPage() {
</Show>
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
{([
<For each={[
{ key: 'all', label: 'All Verifications' },
{ key: 'view', label: 'View Verification' },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={() => setListTab(tab.key)}
@ -655,12 +655,12 @@ export default function VerificationManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
<Show when={listTab() === 'all'}>
<div style="display:flex;align-items:center;gap:24px;min-height:44px;border-bottom:1px solid #E5E7EB;overflow:auto">
{([
<For each={[
{ key: 'all', label: `All Verifications (${tabCounts().all})` },
{ key: 'profile', label: `Profile Approvals (${tabCounts().profile})` },
{ key: 'portfolio', label: `Portfolio Approvals (${tabCounts().portfolio})` },
@ -669,7 +669,7 @@ export default function VerificationManagementPage() {
{ key: 'service_profile', label: `Service Seeker Profile (${tabCounts().serviceProfile})` },
{ key: 'service_requirement', label: `Service Seeker Requirements (${tabCounts().serviceRequirement})` },
{ key: 'job', label: `Job Approvals (${tabCounts().job})` },
] as const).map((tab) => (
] as const}>{(tab) => (
<button
type="button"
onClick={() => setCategoryTab(tab.key)}
@ -677,7 +677,7 @@ export default function VerificationManagementPage() {
>
{tab.label}
</button>
))}
)}</For>
</div>
</Show>
@ -702,11 +702,11 @@ export default function VerificationManagementPage() {
</button>
<Show when={sortOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'latest', label: 'Latest Submitted' },
{ key: 'oldest', label: 'Oldest Submitted' },
{ key: 'priority', label: 'Priority (High-Low)' },
] as const).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => { setSortBy(item.key); setSortOpen(false); }}
@ -714,7 +714,7 @@ export default function VerificationManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -730,7 +730,7 @@ export default function VerificationManagementPage() {
</button>
<Show when={filterOpen()}>
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:200px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
{([
<For each={[
{ key: 'ALL', label: 'All Status' },
{ key: 'PENDING', label: 'Pending' },
{ key: 'UNDER_REVIEW', label: 'Under Review' },
@ -738,7 +738,7 @@ export default function VerificationManagementPage() {
{ key: 'REVISION_REQUESTED', label: 'Revision Requested' },
{ key: 'APPROVED', label: 'Approved' },
{ key: 'REJECTED', label: 'Rejected' },
] as const).map((item) => (
] as const}>{(item) => (
<button
type="button"
onClick={() => { setStatusFilter(item.key); setFilterOpen(false); }}
@ -746,7 +746,7 @@ export default function VerificationManagementPage() {
>
{item.label}
</button>
))}
)}</For>
</div>
</Show>
</div>
@ -765,9 +765,9 @@ export default function VerificationManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Actions'].map((header) => (
<For each={['Submission ID', 'Type', 'Applicant Name', 'Role', 'Submitted On', 'Status', 'Priority', 'Actions']}>{(header) => (
<th style="padding:10px 20px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
))}
)}</For>
</tr>
</thead>
<tbody>
@ -933,9 +933,9 @@ export default function VerificationManagementPage() {
<table class="min-w-full">
<thead>
<tr style="background:#0D0D2A;text-align:left">
{['Document', 'State', 'View', 'Request'].map((header) => (
<For each={['Document', 'State', 'View', 'Request']}>{(header) => (
<th style="padding:10px 16px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#FFFFFF;white-space:nowrap">{header}</th>
))}
)}</For>
</tr>
</thead>
<tbody>

View file

@ -1,13 +0,0 @@
import { onMount } from 'solid-js';
import { useNavigate, useParams } from '@solidjs/router';
export default function WorkspaceMenuAliasPage() {
const navigate = useNavigate();
const params = useParams();
onMount(() => navigate('/admin', { replace: true }));
return (
<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>
);
}

View file

@ -41,12 +41,14 @@ export default function LoginPage() {
// same server-side captcha challenge the public site uses — generated by
// the users service at /api/auth/captcha and verified against shared Redis.
// Routed through /api/gateway/* (this app's generic gateway proxy) rather
// than a bare /api/auth/* path — there is no server route for that prefix,
// so it would otherwise silently fall through to the SPA's index.html.
// The admin is a plain Vite SPA — /api/* routes are proxied directly to the
// gateway (vite.config.ts proxy: /api → http://localhost:9100), so /api/auth/captcha
// reaches the users service correctly. The /api/gateway prefix is only needed
// in the SolidStart frontend where the middleware otherwise falls through to Nitro.
const loadCaptcha = async () => {
setCaptchaInput('');
try {
const r = await fetch('/api/gateway/auth/captcha', { method: 'POST', headers: { Accept: 'application/json' }, credentials: 'include' });
const r = await fetch('/api/auth/captcha', { method: 'POST', headers: { Accept: 'application/json' }, credentials: 'include' });
const payload = await r.json().catch(() => ({}));
setCaptchaId(String(payload?.captcha_id || ''));
setCaptchaChallenge(String(payload?.challenge || ''));

View file

@ -5,8 +5,6 @@ import '../../app.css';
import AdminHomePage from '../../routes/admin/index';
import ApprovalPage from '../../routes/admin/approval';
import VerificationPage from '../../routes/admin/verification';
import DepartmentManagementPage from '../../routes/admin/department-management';
import DesignationManagementPage from '../../routes/admin/designation-management';
import EmployeesPage from '../../routes/admin/employees';
import InternalDashboardManagementPage from '../../routes/admin/internal-dashboard-management';
import ExternalDashboardManagementPage from '../../routes/admin/external-dashboard-management';
@ -47,14 +45,6 @@ export const Approval: Story = {
render: () => renderRoute('/admin/approval', ApprovalPage),
};
export const DepartmentManagement: Story = {
render: () => renderRoute('/admin/department-management', DepartmentManagementPage),
};
export const DesignationManagement: Story = {
render: () => renderRoute('/admin/designation-management', DesignationManagementPage),
};
export const EmployeeManagement: Story = {
render: () => renderRoute('/admin/employees', EmployeesPage),
};

View file

@ -5,26 +5,18 @@ import { http, HttpResponse } from 'msw';
// Mock API responses globally for frontend tests
const server = setupServer(
http.get('/api/admin/companies', () => {
return HttpResponse.json([
http.get('/api/admin/companies', () => HttpResponse.json([
{ id: '1', company_name: 'Test Co', status: 'ACTIVE' }
]);
}),
http.get('/api/admin/users', () => {
return HttpResponse.json([
])),
http.get('/api/admin/users', () => HttpResponse.json([
{ id: '1', full_name: 'Admin User', email: 'admin@example.com', status: 'ACTIVE' }
]);
}),
http.get('/api/admin/jobs', () => {
return HttpResponse.json([
])),
http.get('/api/admin/jobs', () => HttpResponse.json([
{ id: '1', title: 'Developer', status: 'OPEN' }
]);
}),
http.get('/api/admin/leads', () => {
return HttpResponse.json([
])),
http.get('/api/admin/leads', () => HttpResponse.json([
{ id: '1', title: 'Need a developer', status: 'PENDING' }
]);
})
]))
);
beforeAll(() => server.listen());