Wire external runtime preview mode and backend data in dashboard preview

This commit is contained in:
Tracewebstudio Dev 2026-04-15 00:15:11 +02:00
parent b2856f49c1
commit c2cbafc159
2 changed files with 200 additions and 39 deletions

View file

@ -1376,10 +1376,18 @@ export default function DashboardDesignPreview(props: {
const apiDelete = (path: string) =>
fetch(`${GW}${path}`, { method: 'DELETE', credentials: 'include' }).catch(() => null);
// Credits balance
// Credits / wallet
const [creditsResource] = createResource(
() => (hasLive() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/wallet/balance`),
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/wallet/me`),
);
const [walletLedgerResource] = createResource(
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/wallet/me/ledger?page=1&limit=50`),
);
const [paymentHistoryResource] = createResource(
() => (hasLive() ? 'yes' : null),
() => apiFetch('/api/payments/history?page=1&limit=50'),
);
// Marketplace requirements (professionals)
const [marketplaceResource] = createResource(
@ -1402,7 +1410,9 @@ export default function DashboardDesignPreview(props: {
const r = normalizeRoleKey(props.roleKey ?? '');
return hasLive() && (r === 'JOB_SEEKER' || r === 'COMPANY') ? r : null;
},
() => apiFetch('/api/jobs?limit=50'),
(role) => role === 'JOB_SEEKER'
? apiFetch('/api/jobseeker/jobs?page=1&limit=50')
: apiFetch('/api/companies/jobs?page=1&limit=50'),
);
// User profile (all roles)
const [profileResource] = createResource(
@ -1603,7 +1613,70 @@ export default function DashboardDesignPreview(props: {
// Sync resources → local signals
createEffect(() => {
const d = creditsResource();
if (d != null && typeof d.balance === 'number') setLeadCredits(d.balance);
if (!d) return;
const nextBalance = Number(
d?.balance
?? d?.tracecoins_balance
?? d?.data?.balance
?? d?.wallet?.balance
?? 0
);
if (Number.isFinite(nextBalance) && nextBalance >= 0) setLeadCredits(nextBalance);
});
createEffect(() => {
const paymentsPayload = paymentHistoryResource();
const ledgerPayload = walletLedgerResource();
const payments: any[] = Array.isArray(paymentsPayload?.payments)
? paymentsPayload.payments
: Array.isArray(paymentsPayload?.data)
? paymentsPayload.data
: [];
if (payments.length > 0) {
const rows: Array<[string, string, string, string, string, string]> = payments.map((item: any) => {
const statusRaw = String(item?.status || 'PENDING').toUpperCase();
const status = statusRaw.includes('SUCCESS')
? 'Completed'
: statusRaw.includes('FAIL')
? 'Failed'
: 'Pending';
const amount = Number(item?.amount_inr ?? item?.amount ?? 0);
const credits = Number(item?.tracecoins_credited ?? item?.credits ?? 0);
const when = item?.created_at
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: '--';
return [
String(item?.id || item?.invoice_no || `#INV-${Date.now()}`),
String(item?.package_name || item?.package || 'Tracecoin Purchase'),
String(Math.round(credits || 0).toLocaleString('en-IN')),
`${Math.round(amount || 0).toLocaleString('en-IN')}`,
status,
when,
];
});
if (rows.length) setTxRows(rows);
return;
}
const ledger: any[] = Array.isArray(ledgerPayload?.data)
? ledgerPayload.data
: Array.isArray(ledgerPayload)
? ledgerPayload
: [];
if (!ledger.length) return;
const rows: Array<[string, string, string, string, string, string]> = ledger.map((item: any) => {
const amount = Number(item?.amount ?? 0);
const when = item?.created_at
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: '--';
return [
String(item?.id || item?.reference_id || `#TX-${Date.now()}`),
String(item?.reason || item?.type || 'Ledger Entry'),
`${amount >= 0 ? '+' : ''}${Math.round(amount).toLocaleString('en-IN')}`,
'₹0',
amount >= 0 ? 'Completed' : 'Pending',
when,
];
});
if (rows.length) setTxRows(rows);
});
createEffect(() => {
const d = marketplaceResource();
@ -1621,11 +1694,11 @@ export default function DashboardDesignPreview(props: {
: 'TBD',
urgency: item.urgency === 'HIGH' ? 'High' : item.urgency === 'MEDIUM' ? 'Medium' : 'Low',
budget: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
? `${Math.round(item.budget_min).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min)).toLocaleString('en-IN')}`
: '₹0',
budgetValue: Number(item.budget_max ?? item.budget_min ?? 0) / 100,
budgetValue: Number(item.budget_max ?? item.budget_min ?? 0),
priceRange: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
? `${Math.round(item.budget_min).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min)).toLocaleString('en-IN')}`
: '₹0',
cost: 25,
status: 'open' as const,
@ -1671,10 +1744,10 @@ export default function DashboardDesignPreview(props: {
summary: String(item.description ?? ''),
category: String(item.category ?? item.profession_key ?? ''),
amount: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')}`
? `${Math.round(item.budget_min).toLocaleString('en-IN')}`
: '₹0',
budget: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
? `${Math.round(item.budget_min).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min)).toLocaleString('en-IN')}`
: '₹0',
location: String(item.location ?? 'India'),
submission: item.created_at
@ -1696,10 +1769,12 @@ export default function DashboardDesignPreview(props: {
company: String(item.company_name ?? item.company ?? 'Company'),
location: String(item.location ?? 'India'),
salary: item.salary_min != null
? `${Math.round(item.salary_min / 100).toLocaleString('en-IN')}+`
? item.salary_max != null
? `${Math.round(item.salary_min).toLocaleString('en-IN')} - ₹${Math.round(item.salary_max).toLocaleString('en-IN')}`
: `${Math.round(item.salary_min).toLocaleString('en-IN')}`
: 'Negotiable',
exp: String(item.experience_required ?? item.experience ?? '0-2 yrs'),
type: String(item.employment_type ?? item.type ?? 'Full-Time'),
exp: String(item.experience_required ?? item.experience_years ?? item.experience ?? '0-2 yrs'),
type: String(item.employment_type ?? item.job_type ?? item.type ?? 'Full-Time'),
tags: Array.isArray(item.tags) ? item.tags : [],
match: '',
posted: item.created_at
@ -1804,6 +1879,7 @@ export default function DashboardDesignPreview(props: {
setTimeout(() => setPortfolioApprovalState('IN_REVIEW'), 250);
};
createEffect(() => {
if (hasLive()) return;
const roleKey = normalizeRoleKey(props.roleKey || '');
const spec = portfolioSpecForRole(roleKey);
setPortfolioSpecialties(spec.specialties.slice(0, 6));

View file

@ -73,6 +73,7 @@ type RuntimeBundle = {
fields: string[];
verificationStatus?: string;
source: "dashboard-config";
renderMode?: "preview";
};
const API_GATEWAY = "/api/gateway";
@ -337,6 +338,32 @@ function normalizeRole(value: string): RoleKey {
return (ROLE_OPTIONS.find((r) => r === up) || "JOB_SEEKER") as RoleKey;
}
function normalizeSidebarKey(value: string): string {
const key = String(value || "").trim().toLowerCase();
if (!key) return "my dashboard";
if (key === "my dashboard" || key === "dashboard") return "my dashboard";
if (key === "my profile" || key === "profile") return "my profile";
if (key === "my portfolio" || key === "portfolio") return "my portfolio";
if (key === "lead" || key === "leads") return "leads";
if (key === "my response" || key === "responses" || key === "response") return "my responses";
if (key === "credit" || key === "credits") return "credits";
if (key.includes("explore")) return "explore nxtgauge";
if (key === "verification" || key === "verify") return "verification";
if (key === "help centre" || key === "support" || key === "help") return "help center";
if (key === "setting" || key === "settings") return "settings";
if (key === "switch service" || key === "switch role" || key === "switch roles" || key === "switch services") return "switch services";
if (key === "logout" || key === "log out" || key === "sign out") return "logout";
if (key === "job" || key === "jobs") return "jobs";
if (key === "application" || key === "applications" || key === "job applications") return "applications";
if (key === "shortlisted candidate" || key === "shortlisted candidates") return "shortlisted candidates";
if (key === "requirement" || key === "requirements" || key === "my requirement" || key === "my requirements") return "my requirements";
if (key === "received response" || key === "received responses") return "received responses";
if (key === "shortlisted response" || key === "shortlisted responses") return "shortlisted responses";
if (key === "my application" || key === "my applications") return "my applications";
if (key === "saved job" || key === "saved jobs") return "saved jobs";
return key;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((item) => String(item || "").trim()).filter(Boolean);
@ -350,6 +377,8 @@ function getInitialRoleFromStorage(): RoleKey {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const preferred = normalizeRole(String(parsed?.selectedProfessionalRole || ""));
if (ROLE_OPTIONS.includes(preferred) && preferred !== "JOB_SEEKER") return preferred;
const found = normalizeRole(
String(
parsed?.roleKey ||
@ -368,6 +397,19 @@ function getInitialRoleFromStorage(): RoleKey {
return "JOB_SEEKER";
}
function resolveRoleForDashboard(rawRole: string | null | undefined): RoleKey {
const raw = String(rawRole || "").trim();
if (raw) {
return normalizeRole(raw);
}
const normalized = normalizeRole(raw);
const preferred = getInitialRoleFromStorage();
if (preferred !== "JOB_SEEKER") {
return preferred;
}
return normalized;
}
async function fetchJson(path: string): Promise<any | null> {
try {
const isServer = typeof window === "undefined";
@ -387,6 +429,9 @@ async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
const runtime = await fetchJson("/api/runtime-config");
if (runtime) {
const runtimeRole = normalizeRole(String(runtime?.role || runtime?.user?.active_role || role));
const runtimeRenderMode = String(
runtime?.dashboard_config?.render_mode ?? runtime?.render_mode ?? ""
).toLowerCase();
const runtimeSidebar = asStringArray(
runtime?.dashboard_config?.sidebar_items ??
runtime?.dashboard_config?.sidebarItems ??
@ -418,6 +463,7 @@ async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
runtime?.verification_status || runtime?.user?.verification_status || ""
).toUpperCase() || undefined,
source: "dashboard-config",
renderMode: runtimeRenderMode === "preview" ? "preview" : undefined,
};
}
@ -449,6 +495,7 @@ async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
)
.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",
@ -457,6 +504,7 @@ async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
widgets,
fields,
source: "dashboard-config",
renderMode: configRenderMode === "preview" ? "preview" : undefined,
};
}
@ -472,13 +520,46 @@ function mergeSidebar(
"Logout",
];
const fromRuntime = runtimeSidebar.filter(Boolean);
const source = fromRuntime.length > 0 ? fromRuntime : base;
const map = new Map<string, string>();
for (const item of source) {
for (const item of [...fromRuntime, ...base]) {
const key = item.trim().toLowerCase();
if (!map.has(key)) map.set(key, item);
}
let merged = Array.from(map.values());
const persona: "PROFESSIONAL" | "COMPANY" | "JOB_SEEKER" | "CUSTOMER" =
role === "COMPANY"
? "COMPANY"
: role === "CUSTOMER"
? "CUSTOMER"
: role === "JOB_SEEKER"
? "JOB_SEEKER"
: "PROFESSIONAL";
if (persona === "JOB_SEEKER") {
merged = merged.filter((item) => normalizeSidebarKey(item) !== "credits");
}
const hasExplore = merged.some((item) => normalizeSidebarKey(item) === "explore nxtgauge");
if (!hasExplore) {
const verificationIdx = merged.findIndex(
(item) => normalizeSidebarKey(item) === "verification"
);
if (verificationIdx >= 0) merged.splice(verificationIdx, 0, "Explore Nxtgauge");
else merged.push("Explore Nxtgauge");
}
const hasPortfolio = merged.some((item) => normalizeSidebarKey(item) === "my portfolio");
if (persona === "PROFESSIONAL" || persona === "JOB_SEEKER") {
if (!hasPortfolio) {
const profileIdx = merged.findIndex((item) => normalizeSidebarKey(item) === "my profile");
if (profileIdx >= 0) merged.splice(profileIdx + 1, 0, "My Portfolio");
else merged.push("My Portfolio");
}
} else if (hasPortfolio) {
merged = merged.filter((item) => normalizeSidebarKey(item) !== "my portfolio");
}
const status = String(verificationStatus || "").toUpperCase();
const approved = status === "APPROVED";
if (!approved && status) {
@ -516,7 +597,7 @@ export default function RuntimeDashboardPage() {
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));
if (u.active_role) setRole(resolveRoleForDashboard(u.active_role));
}
});
@ -525,11 +606,12 @@ export default function RuntimeDashboardPage() {
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));
if (u.active_role) setRole(resolveRoleForDashboard(u.active_role));
}
});
const [bundle] = createResource(() => role(), loadRoleBundle);
const activeSidebarKey = createMemo(() => normalizeSidebarKey(activeSidebar()));
const sidebarItems = createMemo(() =>
mergeSidebar(role(), bundle()?.sidebarItems || [], bundle()?.verificationStatus)
@ -567,8 +649,11 @@ export default function RuntimeDashboardPage() {
return { userName: userName(), userId: userId(), rolePrefix: prefix };
});
const forcePreviewFromConfig = createMemo(() => bundle()?.renderMode === "preview");
const isRealPage = createMemo(() => {
const key = activeSidebar().toLowerCase();
if (forcePreviewFromConfig()) return false;
const key = activeSidebarKey();
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;
@ -596,66 +681,66 @@ export default function RuntimeDashboardPage() {
userName={userName()}
>
<Switch>
<Match when={activeSidebar().toLowerCase() === "my dashboard"}>
<Match when={activeSidebarKey() === "my dashboard"}>
<MyDashboardPage roleKey={role()} userName={userName()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "my profile"}>
<Match when={activeSidebarKey() === "my profile"}>
<ProfilePage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "my portfolio"}>
<Match when={activeSidebarKey() === "my portfolio"}>
<PortfolioPage
roleKey={role()}
runtimeTabs={bundle()?.tabs || []}
runtimeFields={bundle()?.fields || []}
/>
</Match>
<Match when={activeSidebar().toLowerCase() === "verification"}>
<Match when={activeSidebarKey() === "verification"}>
<VerificationStatusPage roleKey={role()} onNavigate={setActiveSidebar} />
</Match>
<Match when={activeSidebar().toLowerCase() === "settings"}>
<Match when={activeSidebarKey() === "settings"}>
<SettingsPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "credits"}>
<Match when={activeSidebarKey() === "credits"}>
<CreditsPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "explore nxtgauge"}>
<Match when={activeSidebarKey() === "explore nxtgauge"}>
<ExploreServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "help center"}>
<Match when={activeSidebarKey() === "help center"}>
<HelpCenterDashboardPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === "switch services"}>
<Match when={activeSidebarKey() === "switch services"}>
<SwitchServicesPage />
</Match>
<Match when={activeSidebar().toLowerCase() === "logout"}>
<Match when={activeSidebarKey() === "logout"}>
<LogoutPage />
</Match>
<Match when={role() === "COMPANY" && activeSidebar().toLowerCase() === "jobs"}>
<Match when={role() === "COMPANY" && activeSidebarKey() === "jobs"}>
<CompanyJobsPage />
</Match>
<Match
when={role() === "COMPANY" && activeSidebar().toLowerCase() === "applications"}
when={role() === "COMPANY" && activeSidebarKey() === "applications"}
>
<CompanyApplicationsPage />
</Match>
<Match
when={
role() === "COMPANY" &&
activeSidebar().toLowerCase() === "shortlisted candidates"
activeSidebarKey() === "shortlisted candidates"
}
>
<CompanyShortlistedCandidatesPage />
</Match>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "my requirements"
role() === "CUSTOMER" && activeSidebarKey() === "my requirements"
}
>
<CustomerRequirementsPage />
</Match>
<Match
when={
role() === "CUSTOMER" && activeSidebar().toLowerCase() === "received responses"
role() === "CUSTOMER" && activeSidebarKey() === "received responses"
}
>
<CustomerResponsesPage mode="received" />
@ -663,29 +748,29 @@ export default function RuntimeDashboardPage() {
<Match
when={
role() === "CUSTOMER" &&
activeSidebar().toLowerCase() === "shortlisted responses"
activeSidebarKey() === "shortlisted responses"
}
>
<CustomerResponsesPage mode="shortlisted" />
</Match>
<Match
when={
role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "my applications"
role() === "JOB_SEEKER" && activeSidebarKey() === "my applications"
}
>
<JobSeekerApplicationsPage />
</Match>
<Match when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "jobs"}>
<Match when={role() === "JOB_SEEKER" && activeSidebarKey() === "jobs"}>
<JobSeekerJobsPage />
</Match>
<Match
when={role() === "JOB_SEEKER" && activeSidebar().toLowerCase() === "saved jobs"}
when={role() === "JOB_SEEKER" && activeSidebarKey() === "saved jobs"}
>
<JobSeekerSavedJobsPage />
</Match>
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) && activeSidebar().toLowerCase() === "leads"
PROFESSIONAL_ROLE_SET.has(role()) && activeSidebarKey() === "leads"
}
>
<ProfessionalLeadsPage roleKey={role()} />
@ -693,7 +778,7 @@ export default function RuntimeDashboardPage() {
<Match
when={
PROFESSIONAL_ROLE_SET.has(role()) &&
activeSidebar().toLowerCase() === "my responses"
activeSidebarKey() === "my responses"
}
>
<ProfessionalResponsesPage roleKey={role()} />