Merge remote-tracking branch 'forgejo/high-performance' into high-performance
Some checks failed
build-and-release / build (push) Failing after 18s
Some checks failed
build-and-release / build (push) Failing after 18s
This commit is contained in:
commit
b9c209aa67
47 changed files with 2402 additions and 935 deletions
|
|
@ -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
1520
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { Title } from "@solidjs/meta";
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<main>
|
||||
<Title>About</Title>
|
||||
<h1>About</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function ApprovalManagementLegacyRedirectPage() {
|
||||
return <Navigate href="/admin/approval" />;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
import { Navigate } from '@solidjs/router';
|
||||
|
||||
export default function ApprovalsLegacyRedirectPage() {
|
||||
return <Navigate href="/admin/approval" />;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import DepartmentManagementPage from './department';
|
||||
|
||||
export default DepartmentManagementPage;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
import Designation from "./designation";
|
||||
export default Designation;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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) => (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 || ''));
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue