diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index 2563df2..7597194 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -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)); diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index d1db04e..84a0cb8 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -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 { try { const isServer = typeof window === "undefined"; @@ -387,6 +429,9 @@ async function loadRoleBundle(role: RoleKey): Promise { 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 { 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 { ) .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 { 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(); - 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()} > - + - + - + - + - + - + - + - + - + - + - + @@ -663,29 +748,29 @@ export default function RuntimeDashboardPage() { - + @@ -693,7 +778,7 @@ export default function RuntimeDashboardPage() {