fix: MyDashboardPage reads role from URL directly, skips stale prop closure; loadRoleBundle uses auth; no fallback config fetch

This commit is contained in:
Tracewebstudio Dev 2026-04-22 01:13:56 +02:00
parent f1e46e35e7
commit 188de040ae
3 changed files with 64 additions and 58 deletions

View file

@ -1,6 +1,6 @@
import { For, Show, createMemo, createSignal, onMount } from 'solid-js';
import { BTN_GHOST, CARD } from '~/components/DashboardShell';
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
import { normalizeRole, PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
import { readJobSeekerProfile } from '~/lib/job-seeker-custom-data';
const API = '/api/gateway';
@ -31,13 +31,29 @@ export default function MyDashboardPage(props: Props) {
const roleLabel = createMemo(() => String(props.roleKey || '').replace(/_/g, ' '));
const getEffectiveRole = (): RoleKey => {
if (typeof window === 'undefined') return props.roleKey;
const urlParams = new URLSearchParams(window.location.search);
const urlRole = urlParams.get('role');
if (urlRole) {
const normalized = normalizeRole(urlRole);
if (normalized) return normalized;
}
return props.roleKey;
};
const loadData = async () => {
const effectiveRole = getEffectiveRole();
console.log('[MyDashboardPage] loadData called, effectiveRole:', effectiveRole, 'props.roleKey:', props.roleKey);
setLoading(true);
setErr('');
const next: Metric[] = [];
const roleKey = effectiveRole;
try {
if (props.roleKey === 'COMPANY') {
if (roleKey === 'COMPANY') {
const [jobsRes, appsRes] = await Promise.all([
apiFetch('/api/companies/jobs?page=1&limit=100'),
apiFetch('/api/companies/jobs?page=1&limit=1'),
@ -52,7 +68,7 @@ export default function MyDashboardPage(props: Props) {
{ title: 'Latest Sync', value: appsRes.ok ? 'Live' : 'Partial', hint: 'Dashboard data status' },
);
if (!jobsRes.ok && !appsRes.ok) setErr('Some company metrics could not be loaded.');
} else if (props.roleKey === 'CUSTOMER') {
} else if (roleKey === 'CUSTOMER') {
const reqRes = await apiFetch('/api/customers/requirements?page=1&limit=100');
const reqJson = await reqRes.json().catch(() => ({}));
const reqs = Array.isArray(reqJson?.data) ? reqJson.data : [];
@ -63,7 +79,7 @@ export default function MyDashboardPage(props: Props) {
{ title: 'Drafts', value: String(reqs.filter((r: any) => String(r.status || '').toUpperCase() === 'DRAFT').length), hint: 'Not yet submitted' },
);
if (!reqRes.ok) setErr('Some customer metrics could not be loaded.');
} else if (props.roleKey === 'JOB_SEEKER') {
} else if (roleKey === 'JOB_SEEKER') {
const [jobsRes, appsRes, profile] = await Promise.all([
apiFetch('/api/jobseeker/jobs?page=1&limit=100'),
apiFetch('/api/jobseeker/applications?page=1&limit=100'),
@ -77,8 +93,8 @@ export default function MyDashboardPage(props: Props) {
? (profile.custom_data as Record<string, unknown>)
: {};
const savedJobs = Array.isArray(customData.saved_jobs) ? customData.saved_jobs : [];
const portfolio = (customData.job_seeker_portfolio && typeof customData.job_seeker_portfolio === 'object')
? (customData.job_seeker_portfolio as Record<string, unknown>)
const portfolio = (customData.job_seeker_portfolio && typeof profile.job_seeker_portfolio === 'object')
? (profile.job_seeker_portfolio as Record<string, unknown>)
: {};
const profileStatus = String(profile?.status || 'NOT_SUBMITTED').replace(/_/g, ' ');
const portfolioDone = Boolean(
@ -96,8 +112,8 @@ export default function MyDashboardPage(props: Props) {
{ title: 'Portfolio', value: portfolioDone ? 'Complete' : 'Incomplete', hint: 'Education/work/skills sections' },
);
if (!jobsRes.ok && !appsRes.ok && !profile) setErr('Some job seeker metrics could not be loaded.');
} else if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) {
const prefix = ROLE_PREFIXES[props.roleKey];
} else if (PROFESSIONAL_ROLE_SET.has(roleKey)) {
const prefix = ROLE_PREFIXES[roleKey];
const [marketRes, reqRes, walletRes] = await Promise.all([
apiFetch(`/api/${prefix}/marketplace?page=1&limit=100`),
apiFetch(`/api/${prefix}/leads/requests/me?page=1&limit=100`),
@ -133,7 +149,9 @@ export default function MyDashboardPage(props: Props) {
}
};
onMount(loadData);
onMount(() => {
setTimeout(() => loadData(), 0);
});
return (
<div style={{ display: 'grid', gap: '14px', 'max-width': '980px' }}>

View file

@ -42,3 +42,27 @@ export const PROFESSIONAL_ROLE_SET = new Set<RoleKey>([
'CATERING_SERVICES',
]);
const ALL_ROLE_KEYS: RoleKey[] = [
'COMPANY',
'CUSTOMER',
'JOB_SEEKER',
'PHOTOGRAPHER',
'MAKEUP_ARTIST',
'TUTOR',
'DEVELOPER',
'VIDEO_EDITOR',
'UGC_CONTENT_CREATOR',
'GRAPHIC_DESIGNER',
'SOCIAL_MEDIA_MANAGER',
'FITNESS_TRAINER',
'CATERING_SERVICES',
];
export function normalizeRole(value: string): RoleKey {
const up = String(value || '')
.trim()
.toUpperCase()
.replace(/\s+/g, '_');
return (ALL_ROLE_KEYS.find((r) => r === up) || 'JOB_SEEKER') as RoleKey;
}

View file

@ -437,11 +437,16 @@ function resolveRoleForDashboard(rawRole: string | null | undefined): RoleKey {
return normalized;
}
async function fetchJson(path: string): Promise<any | null> {
async function fetchJson(path: string, includeAuth = false): 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" });
const headers: Record<string, string> = { credentials: "include" } as any;
if (includeAuth) {
const token = typeof getToken === "function" ? getToken() : null;
if (token) headers["Authorization"] = `Bearer ${token}`;
}
const res = await fetch(target, headers);
if (!res.ok) return null;
return await res.json();
} catch {
@ -449,12 +454,12 @@ async function fetchJson(path: string): Promise<any | null> {
}
}
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
if (typeof window === "undefined") {
return null;
}
const runtime = await fetchJson("/api/runtime-config");
if (runtime) {
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
if (typeof window === "undefined") {
return null;
}
const runtime = await fetchJson("/api/runtime-config", true);
if (!runtime) return null;
const runtimeRole = normalizeRole(String(runtime?.role || runtime?.user?.active_role || role));
const runtimeRenderMode = String(
runtime?.dashboard_config?.render_mode ?? runtime?.render_mode ?? ""
@ -494,47 +499,6 @@ async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
};
}
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);
const configRenderMode = String((config as any)?.render_mode || "").toLowerCase();
return {
role,
status: payload?.is_active === false ? "INACTIVE" : "ACTIVE",
sidebarItems,
tabs,
widgets,
fields,
source: "dashboard-config",
renderMode: configRenderMode === "preview" ? "preview" : undefined,
};
}
function mergeSidebar(
role: RoleKey,
runtimeSidebar: string[],