From 012a6a607221f06104ee0d17158252b7a08ea855 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Wed, 29 Jul 2026 11:46:15 +0530 Subject: [PATCH] Fix Profile Completion widget stuck at wrong % and empty-field wizard steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ProfileCompletionWidget hardcoded a per-role field list (industry, description, full_name, bio, ...) that never matched what the onboarding wizard actually collects or where it's stored — for COMPANY only 2 of 5 checked fields existed at all, producing a stuck 40% regardless of real completeness. Now derives required field ids from the same fetchOnboardingSchemaForRole the wizard itself uses, and checks them against the same /api/profile endpoint the wizard writes to, so it can't drift again. RoleWizard now shows "no fields configured yet" instead of rendering a blank step card when a step has zero fields — visible failure instead of silent, in case a stale/cached schema ever produces one. Co-Authored-By: Claude Sonnet 5 --- src/components/dashboard/RoleWizard.tsx | 15 +++- .../widgets/ProfileCompletionWidget.tsx | 80 +++++++------------ 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/components/dashboard/RoleWizard.tsx b/src/components/dashboard/RoleWizard.tsx index 4911612..58eea28 100644 --- a/src/components/dashboard/RoleWizard.tsx +++ b/src/components/dashboard/RoleWizard.tsx @@ -171,7 +171,10 @@ export default function RoleWizard(props: Props) {
{/* ── basic ─────────────────────────────────────────────── */} - + +

This step has no fields configured yet.

+
+ 0}>
{(field) => ( @@ -212,7 +215,10 @@ export default function RoleWizard(props: Props) { {/* ── documents ─────────────────────────────────────────── */} - + +

This step has no documents configured yet.

+
+ 0}>
{(field) => ( @@ -260,7 +266,10 @@ export default function RoleWizard(props: Props) { {/* ── portfolio ─────────────────────────────────────────── */} - + +

This step has no fields configured yet.

+
+ 0}>
{(field) => ( diff --git a/src/components/dashboard/widgets/ProfileCompletionWidget.tsx b/src/components/dashboard/widgets/ProfileCompletionWidget.tsx index fa58625..d322d67 100644 --- a/src/components/dashboard/widgets/ProfileCompletionWidget.tsx +++ b/src/components/dashboard/widgets/ProfileCompletionWidget.tsx @@ -2,69 +2,43 @@ import { createResource, Show } from 'solid-js'; import { User } from 'lucide-solid'; import DashboardWidget from './DashboardWidget'; import type { RoleKey } from '../RoleDashboardShared'; -import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES } from '../RoleDashboardShared'; -import { fetchProfile } from '~/lib/api'; - -const API = ''; - -async function apiFetch(path: string, opts?: RequestInit) { - const token = - typeof window !== 'undefined' - ? window.sessionStorage.getItem('nxtgauge_access_token') || '' - : ''; - return fetch(`${API}${path}`, { - ...opts, - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(opts?.headers ?? {}), - }, - }); -} +import { apiFetch } from '~/lib/api'; +import { fetchOnboardingSchemaForRole } from '~/lib/runtime/storage'; type Props = { roleKey: RoleKey; }; -async function fetchProfileData(roleKey: RoleKey) { - if (PROFESSIONAL_ROLE_SET.has(roleKey) || roleKey === 'COMPANY' || roleKey === 'CUSTOMER') { - const prefix = ROLE_PREFIXES[roleKey]; - if (!prefix) return null; - try { - return await fetchProfile(prefix); - } catch { - return null; - } - } - if (roleKey === 'JOB_SEEKER') { - const res = await apiFetch('/api/jobseeker/profile/me'); - if (!res.ok) return null; - return await res.json(); - } - return null; +// Same source of truth the wizard itself uses (fetchOnboardingSchemaForRole) +// and the same storage the wizard writes to (/api/profile?roleKey=...) — so +// this can never drift from what fields actually exist / get saved for a role, +// unlike a hardcoded per-role field list. +async function fetchCompletionInputs(roleKey: RoleKey) { + const [schema, profileRes] = await Promise.all([ + fetchOnboardingSchemaForRole(roleKey), + apiFetch(`/api/profile?roleKey=${roleKey}`).catch(() => null), + ]); + const requiredFieldIds = (schema?.steps ?? []) + .filter((step) => step.type !== 'review') + .flatMap((step) => step.fields) + .filter((field) => field.required) + .map((field) => field.id); + const profileData = profileRes?.profile_data ?? {}; + return { requiredFieldIds, profileData }; } -const PROFILE_FIELDS: Record = { - COMPANY: ['company_name', 'industry', 'description', 'website', 'contact_email'], - CUSTOMER: ['full_name', 'location', 'phone', 'email'], - JOB_SEEKER: ['full_name', 'location', 'summary', 'skills', 'experience_years'], - PROFESSIONAL: ['full_name', 'headline', 'bio', 'location', 'skills'], -}; - export default function ProfileCompletionWidget(props: Props) { - const [profile] = createResource(() => props.roleKey, fetchProfileData); + const [data] = createResource(() => props.roleKey, fetchCompletionInputs); const completion = () => { - const p = profile(); - if (!p) return { filled: 0, total: 5, pct: 0 }; - const fields = PROFILE_FIELDS[props.roleKey] || PROFILE_FIELDS['PROFESSIONAL']; + const d = data(); + if (!d || d.requiredFieldIds.length === 0) return { filled: 0, total: 0, pct: 0 }; let filled = 0; - for (const field of fields) { - const val = (p as any)[field]; + for (const id of d.requiredFieldIds) { + const val = (d.profileData as any)[id]; if (val !== null && val !== undefined && String(val).trim() !== '') filled++; } - const total = fields.length; + const total = d.requiredFieldIds.length; const pct = total > 0 ? Math.round((filled / total) * 100) : 0; return { filled, total, pct }; }; @@ -79,11 +53,11 @@ export default function ProfileCompletionWidget(props: Props) { return ( } > - +