nxtgauge-frontend-solid/src/routes/dashboard.tsx
Ashwin Kumar 30750f3797 docs: clarify real data implementations are wired to backend APIs
All job seeker pages are already connected to real APIs:
- Jobs: /api/jobseeker/jobs (real company job postings)
- Applications: /api/jobseeker/applications (my applied jobs)
- Saved Jobs: Custom data storage for bookmarked jobs
- Apply: POST /api/jobseeker/jobs/{id}/apply

Dashboard shows real data from backend, not mock preview.
2026-04-10 01:21:36 +02:00

736 lines
22 KiB
TypeScript

import {
Match,
Show,
Switch,
createEffect,
createMemo,
createResource,
createSignal,
onMount,
} from "solid-js";
import { useNavigate } from "@solidjs/router";
import { useAuth, RequireAuth } from "~/lib/auth";
import DashboardDesignPreview from "~/components/admin/DashboardDesignPreview";
import DashboardShell from "~/components/DashboardShell";
import ProfilePage from "~/components/dashboard/ProfilePage";
import PortfolioPage from "~/components/dashboard/PortfolioPage";
import VerificationStatusPage from "~/components/dashboard/VerificationStatusPage";
import CompanyJobsPage from "~/components/dashboard/CompanyJobsPage";
import CompanyApplicationsPage from "~/components/dashboard/CompanyApplicationsPage";
import SettingsPage from "~/components/dashboard/SettingsPage";
import MyDashboardPage from "~/components/dashboard/MyDashboardPage";
import CustomerRequirementsPage from "~/components/dashboard/CustomerRequirementsPage";
import CustomerResponsesPage from "~/components/dashboard/CustomerResponsesPage";
import CompanyShortlistedCandidatesPage from "~/components/dashboard/CompanyShortlistedCandidatesPage";
import JobSeekerApplicationsPage from "~/components/dashboard/JobSeekerApplicationsPage";
import JobSeekerSavedJobsPage from "~/components/dashboard/JobSeekerSavedJobsPage";
import JobSeekerJobsPage from "~/components/dashboard/JobSeekerJobsPage";
import ProfessionalLeadsPage from "~/components/dashboard/ProfessionalLeadsPage";
import ProfessionalResponsesPage from "~/components/dashboard/ProfessionalResponsesPage";
import CreditsPage from "~/components/dashboard/CreditsPage";
import ExploreServicesPage from "~/components/dashboard/ExploreServicesPage";
import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage";
import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage";
import LogoutPage from "~/components/dashboard/LogoutPage";
import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
// Sidebar items that have real data implementations (wired to backend APIs)
// These show real components instead of DashboardDesignPreview mock
const BASE_REAL_PAGES = ["my dashboard", "my profile", "my portfolio", "verification", "settings"];
const COMMON_REAL_PAGES = [
"credits",
"explore nxtgauge",
"help center",
"switch services",
"logout",
];
const COMPANY_REAL_PAGES = ["jobs", "applications", "shortlisted candidates"];
const CUSTOMER_REAL_PAGES = ["my requirements", "received responses", "shortlisted responses"];
const JOB_SEEKER_REAL_PAGES = ["jobs", "my applications", "saved jobs", "my portfolio"];
const PROFESSIONAL_REAL_PAGES = ["leads", "my responses"];
type RoleKey =
| "COMPANY"
| "CUSTOMER"
| "JOB_SEEKER"
| "PHOTOGRAPHER"
| "MAKEUP_ARTIST"
| "TUTOR"
| "DEVELOPER"
| "VIDEO_EDITOR"
| "UGC_CONTENT_CREATOR"
| "GRAPHIC_DESIGNER"
| "SOCIAL_MEDIA_MANAGER"
| "FITNESS_TRAINER"
| "CATERING_SERVICES";
type RuntimeBundle = {
role: RoleKey;
status: "ACTIVE" | "INACTIVE";
sidebarItems: string[];
tabs: string[];
widgets: string[];
fields: string[];
verificationStatus?: string;
source: "dashboard-config";
};
const API_GATEWAY = "/api/gateway";
const SERVER_API_BASE = (process.env.PUBLIC_API_URL || "http://localhost:8080/api").replace(
/\/+$/,
""
);
const ROLE_OPTIONS: RoleKey[] = [
"COMPANY",
"CUSTOMER",
"JOB_SEEKER",
"PHOTOGRAPHER",
"MAKEUP_ARTIST",
"TUTOR",
"DEVELOPER",
"VIDEO_EDITOR",
"UGC_CONTENT_CREATOR",
"GRAPHIC_DESIGNER",
"SOCIAL_MEDIA_MANAGER",
"FITNESS_TRAINER",
"CATERING_SERVICES",
];
const ROLE_BASED_SIDEBAR: Record<RoleKey, string[]> = {
PHOTOGRAPHER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
MAKEUP_ARTIST: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
TUTOR: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
DEVELOPER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
VIDEO_EDITOR: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
UGC_CONTENT_CREATOR: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
GRAPHIC_DESIGNER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
SOCIAL_MEDIA_MANAGER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
FITNESS_TRAINER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
CATERING_SERVICES: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Leads",
"My Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
COMPANY: [
"My Dashboard",
"My Profile",
"Jobs",
"Applications",
"Shortlisted Candidates",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
JOB_SEEKER: [
"My Dashboard",
"My Profile",
"My Portfolio",
"Jobs",
"My Applications",
"Saved Jobs",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
CUSTOMER: [
"My Dashboard",
"My Profile",
"My Requirements",
"Received Responses",
"Shortlisted Responses",
"Credits",
"Explore Nxtgauge",
"Verification",
"Help Center",
"Settings",
"Switch Services",
"Logout",
],
};
const ROLE_PREFIXES: Record<string, string> = {
PHOTOGRAPHER: "photographers",
MAKEUP_ARTIST: "makeup-artists",
TUTOR: "tutors",
DEVELOPER: "developers",
VIDEO_EDITOR: "video-editors",
GRAPHIC_DESIGNER: "graphic-designers",
SOCIAL_MEDIA_MANAGER: "social-media-managers",
FITNESS_TRAINER: "fitness-trainers",
CATERING_SERVICES: "catering-services",
UGC_CONTENT_CREATOR: "ugc-content-creators",
CUSTOMER: "customers",
COMPANY: "companies",
JOB_SEEKER: "jobseeker",
};
function getNameFromStorage(): string {
if (typeof window === "undefined") return "User";
const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"];
for (const key of keys) {
try {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const name =
parsed?.name || parsed?.first_name || parsed?.user?.name || parsed?.user?.first_name;
if (name) return String(name);
} catch {
/* ignore */
}
}
return "User";
}
const EXPLORE_ROLES = [
{ key: "PHOTOGRAPHER", name: "Photographer" },
{ key: "MAKEUP_ARTIST", name: "Makeup Artist" },
{ key: "TUTOR", name: "Tutor" },
{ key: "DEVELOPER", name: "Developer" },
{ key: "VIDEO_EDITOR", name: "Video Editor" },
{ key: "UGC_CONTENT_CREATOR", name: "UGC Content Creator" },
{ key: "GRAPHIC_DESIGNER", name: "Graphic Designer" },
{ key: "SOCIAL_MEDIA_MANAGER", name: "Social Media Manager" },
{ key: "FITNESS_TRAINER", name: "Fitness Trainer" },
{ key: "CATERING_SERVICES", name: "Catering Services" },
];
function normalizeRole(value: string): RoleKey {
const up = String(value || "")
.trim()
.toUpperCase()
.replace(/\s+/g, "_");
return (ROLE_OPTIONS.find((r) => r === up) || "JOB_SEEKER") as RoleKey;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((item) => String(item || "").trim()).filter(Boolean);
}
function getInitialRoleFromStorage(): RoleKey {
if (typeof window === "undefined") return "JOB_SEEKER";
const keys = ["nxtgauge_signup_profile_v1", "nxtgauge_auth_user", "nxtgauge_user"];
for (const key of keys) {
try {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const found = normalizeRole(
String(
parsed?.roleKey ||
parsed?.role ||
parsed?.active_role ||
parsed?.user?.active_role ||
parsed?.user?.roles?.[0] ||
""
)
);
if (ROLE_OPTIONS.includes(found)) return found;
} catch {
// ignore invalid payload
}
}
return "JOB_SEEKER";
}
async function fetchJson(path: string): Promise<any | null> {
try {
const isServer = typeof window === "undefined";
const target = isServer ? `${SERVER_API_BASE}${path}` : `${API_GATEWAY}${path}`;
const res = await fetch(target, { credentials: "include" });
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
if (typeof window === "undefined") {
return null;
}
const runtime = await fetchJson("/api/runtime-config");
if (runtime) {
const runtimeRole = normalizeRole(String(runtime?.role || runtime?.user?.active_role || role));
const runtimeSidebar = asStringArray(
runtime?.dashboard_config?.sidebar_items ??
runtime?.dashboard_config?.sidebarItems ??
runtime?.sidebar_items ??
runtime?.sidebarItems
);
const runtimeTabs = asStringArray(runtime?.dashboard_config?.tabs ?? runtime?.tabs);
const runtimeWidgetsRaw = Array.isArray(runtime?.dashboard_config?.widgets)
? runtime.dashboard_config.widgets
: Array.isArray(runtime?.widgets)
? runtime.widgets
: [];
const runtimeWidgets = runtimeWidgetsRaw
.map((item: any) =>
String(typeof item === "string" ? item : item?.key || item?.id || "").trim()
)
.filter(Boolean);
const runtimeFields = asStringArray(runtime?.dashboard_config?.fields ?? runtime?.fields);
return {
role: runtimeRole,
status: "ACTIVE",
sidebarItems: runtimeSidebar,
tabs: runtimeTabs,
widgets: runtimeWidgets,
fields: runtimeFields,
verificationStatus:
String(
runtime?.verification_status || runtime?.user?.verification_status || ""
).toUpperCase() || undefined,
source: "dashboard-config",
};
}
let payload = await fetchJson(
`/api/config/dashboard/by-key/${encodeURIComponent(role)}?audience=EXTERNAL`
);
if (!payload) {
const listPayload = await fetchJson("/api/admin/dashboard-config?audience=EXTERNAL");
const rows = Array.isArray(listPayload)
? listPayload
: Array.isArray(listPayload?.items)
? listPayload.items
: [];
const matched = rows.find(
(row: any) => String(row?.role_key || row?.config_json?.role_key || "").toUpperCase() === role
);
if (matched) payload = matched;
}
const config = (payload?.config_json || payload || null) as Record<string, unknown> | null;
if (!config) return null;
const sidebarItems = asStringArray(
(config as any)?.sidebar_items ?? (config as any)?.sidebarItems
);
const tabs = asStringArray((config as any)?.tabs);
const widgetsRaw = Array.isArray((config as any)?.widgets) ? (config as any).widgets : [];
const widgets = widgetsRaw
.map((item: any) =>
String(typeof item === "string" ? item : item?.key || item?.id || "").trim()
)
.filter(Boolean);
const fields = asStringArray((config as any)?.fields);
return {
role,
status: payload?.is_active === false ? "INACTIVE" : "ACTIVE",
sidebarItems,
tabs,
widgets,
fields,
source: "dashboard-config",
};
}
function mergeSidebar(
role: RoleKey,
runtimeSidebar: string[],
verificationStatus?: string
): string[] {
const base = ROLE_BASED_SIDEBAR[role] || [
"My Dashboard",
"My Profile",
"Switch Services",
"Logout",
];
const fromRuntime = runtimeSidebar.filter(Boolean);
const source = fromRuntime.length > 0 ? fromRuntime : base;
const map = new Map<string, string>();
for (const item of source) {
const key = item.trim().toLowerCase();
if (!map.has(key)) map.set(key, item);
}
let merged = Array.from(map.values());
const status = String(verificationStatus || "").toUpperCase();
const approved = status === "APPROVED";
if (!approved && status) {
const restricted = new Set([
"my profile",
"help center",
"settings",
"verification",
"logout",
...(PROFESSIONAL_ROLE_SET.has(role) || role === "JOB_SEEKER"
? ["my portfolio", "credits"]
: []),
]);
merged = merged.filter((item) => restricted.has(item.trim().toLowerCase()));
}
return merged;
}
export default function RuntimeDashboardPage() {
const navigate = useNavigate();
const auth = useAuth();
const [hydrated, setHydrated] = createSignal(false);
const [role, setRole] = createSignal<RoleKey>("JOB_SEEKER");
const [activeSidebar, setActiveSidebar] = createSignal("My Dashboard");
const [activeTab, setActiveTab] = createSignal("overview");
const [userName, setUserName] = createSignal("User");
const [userId, setUserId] = createSignal("");
onMount(() => {
setHydrated(true);
const storedRole = getInitialRoleFromStorage();
setRole(storedRole);
setUserName(getNameFromStorage());
if (auth.user()) {
const u = auth.user()!;
if (u.full_name) setUserName(u.full_name);
if (u.id) setUserId(u.id);
if (u.active_role) setRole(normalizeRole(u.active_role));
}
});
createEffect(() => {
const u = auth.user();
if (u) {
if (u.full_name && userName() === "User") setUserName(u.full_name);
if (u.id && !userId()) setUserId(u.id);
if (u.active_role) setRole(normalizeRole(u.active_role));
}
});
const [bundle] = createResource(() => role(), loadRoleBundle);
const sidebarItems = createMemo(() =>
mergeSidebar(role(), bundle()?.sidebarItems || [], bundle()?.verificationStatus)
);
createEffect(() => {
const runtimeRole = bundle()?.role;
if (runtimeRole && runtimeRole !== role()) setRole(runtimeRole);
});
createEffect(() => {
const first = sidebarItems()[0] || "My Dashboard";
const current = activeSidebar();
const exists = sidebarItems().some((item) => item.toLowerCase() === current.toLowerCase());
if (!exists) setActiveSidebar(first);
});
const tabs = createMemo(() => {
const fromRuntime = bundle()?.tabs || [];
return fromRuntime.length > 0 ? fromRuntime : ["overview"];
});
createEffect(() => {
role();
const firstTab = tabs()[0] || "overview";
setActiveTab((prev) => (prev ? prev : firstTab));
});
const loading = createMemo(() => !hydrated() || bundle.loading);
const ready = createMemo(() => hydrated() && !bundle.loading);
const liveData = createMemo(() => {
const prefix = ROLE_PREFIXES[role()];
if (!prefix) return undefined;
return { userName: userName(), userId: userId(), rolePrefix: prefix };
});
const isRealPage = createMemo(() => {
const key = activeSidebar().toLowerCase();
if (BASE_REAL_PAGES.includes(key)) return true;
if (COMMON_REAL_PAGES.includes(key)) return true;
if (role() === "COMPANY" && COMPANY_REAL_PAGES.includes(key)) return true;
if (role() === "CUSTOMER" && CUSTOMER_REAL_PAGES.includes(key)) return true;
if (role() === "JOB_SEEKER" && JOB_SEEKER_REAL_PAGES.includes(key)) return true;
if (PROFESSIONAL_ROLE_SET.has(role()) && PROFESSIONAL_REAL_PAGES.includes(key)) return true;
return false;
});
return (
<RequireAuth>
<main style={{ "min-height": "100vh", background: "#F3F4F6" }}>
<Show when={loading()}>
<div style={cardStyle}>Loading dashboard</div>
</Show>
<Show when={ready()}>
{/* ── Real pages: DashboardShell + actual components ── */}
<Show when={isRealPage()}>
<DashboardShell
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
roleKey={role()}
userName={userName()}
>
<Switch>
<Match when={activeSidebar().toLowerCase() === "my dashboard"}>
<MyDashboardPage roleKey={role()} userName={userName()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "my profile"}>
<ProfilePage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "my portfolio"}>
<PortfolioPage
roleKey={role()}
runtimeTabs={bundle()?.tabs || []}
runtimeFields={bundle()?.fields || []}
/>
</Match>
<Match when={activeSidebar().toLowerCase() === "verification"}>
<VerificationStatusPage roleKey={role()} onNavigate={setActiveSidebar} />
</Match>
<Match when={activeSidebar().toLowerCase() === "settings"}>
<SettingsPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "credits"}>
<CreditsPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "explore nxtgauge"}>
<ExploreServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "help center"}>
<HelpCenterDashboardPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "switch services"}>
<SwitchServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "logout"}>
<LogoutPage />
</Match>
<Match when={role() === "COMPANY" && activeSidebar().toLowerCase() === "jobs"}>
<CompanyJobsPage />
</Match>
<Match
when={role() === "COMPANY" && activeSidebar().toLowerCase() === "applications"}
>
<CompanyApplicationsPage />
</Match>
<Match
when={
role() === "COMPANY" &&
activeSidebar().toLowerCase() === "shortlisted candidates"
}
>
<CompanyShortlistedCandidatesPage />
</Match>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "my requirements"
}
>
<CustomerRequirementsPage />
</Match>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "received responses"
}
>
<CustomerResponsesPage mode="received" />
</Match>
<Match
when={
role() === "CUSTOMER" &&
activeSidebar().toLowerCase() === "shortlisted responses"
}
>
<CustomerResponsesPage mode="shortlisted" />
</Match>
<Match
when={
role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "my applications"
}
>
<JobSeekerApplicationsPage />
</Match>
<Match when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "jobs"}>
<JobSeekerJobsPage />
</Match>
<Match
when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "saved jobs"}
>
<JobSeekerSavedJobsPage />
</Match>
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) && activeSidebar().toLowerCase() === "leads"
}
>
<ProfessionalLeadsPage roleKey={role()} />
</Match>
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) &&
activeSidebar().toLowerCase() === "my responses"
}
>
<ProfessionalResponsesPage roleKey={role()} />
</Match>
</Switch>
</DashboardShell>
</Show>
{/* ── All other views: DashboardDesignPreview mock ── */}
<Show when={!isRealPage()}>
<DashboardDesignPreview
status={bundle()?.status ?? "ACTIVE"}
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
tabs={tabs()}
activeTab={activeTab()}
onTabSelect={setActiveTab}
widgets={bundle()?.widgets || []}
fields={bundle()?.fields || []}
mode="customer_external"
roleKey={role()}
exploreRoles={EXPLORE_ROLES}
hidePreviewHeader
liveData={liveData()}
/>
</Show>
</Show>
</main>
</RequireAuth>
);
}
const cardStyle = {
border: "1px solid #E5E7EB",
"border-radius": "12px",
padding: "16px",
background: "#fff",
color: "#111827",
} as const;