Fix Profile Completion widget stuck at wrong % and empty-field wizard steps
All checks were successful
build-and-release / build (push) Successful in 1m42s

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 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-29 11:46:15 +05:30
parent f4b9885974
commit 012a6a6072
2 changed files with 39 additions and 56 deletions

View file

@ -171,7 +171,10 @@ export default function RoleWizard(props: Props) {
<div style={CARD}> <div style={CARD}>
{/* ── basic ─────────────────────────────────────────────── */} {/* ── basic ─────────────────────────────────────────────── */}
<Show when={currentStep()?.type === "basic" || !currentStep()?.type}> <Show when={(currentStep()?.type === "basic" || !currentStep()?.type) && (currentStep()?.fields?.length ?? 0) === 0}>
<p style={{ "font-size": "13px", color: "#9CA3AF" }}>This step has no fields configured yet.</p>
</Show>
<Show when={(currentStep()?.type === "basic" || !currentStep()?.type) && (currentStep()?.fields?.length ?? 0) > 0}>
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "10px" }}> <div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "10px" }}>
<For each={currentStep()?.fields ?? []}> <For each={currentStep()?.fields ?? []}>
{(field) => ( {(field) => (
@ -212,7 +215,10 @@ export default function RoleWizard(props: Props) {
</Show> </Show>
{/* ── documents ─────────────────────────────────────────── */} {/* ── documents ─────────────────────────────────────────── */}
<Show when={currentStep()?.type === "documents"}> <Show when={currentStep()?.type === "documents" && (currentStep()?.fields?.length ?? 0) === 0}>
<p style={{ "font-size": "13px", color: "#9CA3AF" }}>This step has no documents configured yet.</p>
</Show>
<Show when={currentStep()?.type === "documents" && (currentStep()?.fields?.length ?? 0) > 0}>
<div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}> <div style={{ display: "flex", "flex-direction": "column", gap: "16px" }}>
<For each={currentStep()?.fields ?? []}> <For each={currentStep()?.fields ?? []}>
{(field) => ( {(field) => (
@ -260,7 +266,10 @@ export default function RoleWizard(props: Props) {
</Show> </Show>
{/* ── portfolio ─────────────────────────────────────────── */} {/* ── portfolio ─────────────────────────────────────────── */}
<Show when={currentStep()?.type === "portfolio"}> <Show when={currentStep()?.type === "portfolio" && (currentStep()?.fields?.length ?? 0) === 0}>
<p style={{ "font-size": "13px", color: "#9CA3AF" }}>This step has no fields configured yet.</p>
</Show>
<Show when={currentStep()?.type === "portfolio" && (currentStep()?.fields?.length ?? 0) > 0}>
<div style={{ display: "grid", gap: "10px" }}> <div style={{ display: "grid", gap: "10px" }}>
<For each={currentStep()?.fields ?? []}> <For each={currentStep()?.fields ?? []}>
{(field) => ( {(field) => (

View file

@ -2,69 +2,43 @@ import { createResource, Show } from 'solid-js';
import { User } from 'lucide-solid'; import { User } from 'lucide-solid';
import DashboardWidget from './DashboardWidget'; import DashboardWidget from './DashboardWidget';
import type { RoleKey } from '../RoleDashboardShared'; import type { RoleKey } from '../RoleDashboardShared';
import { PROFESSIONAL_ROLE_SET, ROLE_PREFIXES } from '../RoleDashboardShared'; import { apiFetch } from '~/lib/api';
import { fetchProfile } from '~/lib/api'; import { fetchOnboardingSchemaForRole } from '~/lib/runtime/storage';
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 ?? {}),
},
});
}
type Props = { type Props = {
roleKey: RoleKey; roleKey: RoleKey;
}; };
async function fetchProfileData(roleKey: RoleKey) { // Same source of truth the wizard itself uses (fetchOnboardingSchemaForRole)
if (PROFESSIONAL_ROLE_SET.has(roleKey) || roleKey === 'COMPANY' || roleKey === 'CUSTOMER') { // and the same storage the wizard writes to (/api/profile?roleKey=...) — so
const prefix = ROLE_PREFIXES[roleKey]; // this can never drift from what fields actually exist / get saved for a role,
if (!prefix) return null; // unlike a hardcoded per-role field list.
try { async function fetchCompletionInputs(roleKey: RoleKey) {
return await fetchProfile(prefix); const [schema, profileRes] = await Promise.all([
} catch { fetchOnboardingSchemaForRole(roleKey),
return null; apiFetch(`/api/profile?roleKey=${roleKey}`).catch(() => null),
} ]);
} const requiredFieldIds = (schema?.steps ?? [])
if (roleKey === 'JOB_SEEKER') { .filter((step) => step.type !== 'review')
const res = await apiFetch('/api/jobseeker/profile/me'); .flatMap((step) => step.fields)
if (!res.ok) return null; .filter((field) => field.required)
return await res.json(); .map((field) => field.id);
} const profileData = profileRes?.profile_data ?? {};
return null; return { requiredFieldIds, profileData };
} }
const PROFILE_FIELDS: Record<string, string[]> = {
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) { export default function ProfileCompletionWidget(props: Props) {
const [profile] = createResource(() => props.roleKey, fetchProfileData); const [data] = createResource(() => props.roleKey, fetchCompletionInputs);
const completion = () => { const completion = () => {
const p = profile(); const d = data();
if (!p) return { filled: 0, total: 5, pct: 0 }; if (!d || d.requiredFieldIds.length === 0) return { filled: 0, total: 0, pct: 0 };
const fields = PROFILE_FIELDS[props.roleKey] || PROFILE_FIELDS['PROFESSIONAL'];
let filled = 0; let filled = 0;
for (const field of fields) { for (const id of d.requiredFieldIds) {
const val = (p as any)[field]; const val = (d.profileData as any)[id];
if (val !== null && val !== undefined && String(val).trim() !== '') filled++; 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; const pct = total > 0 ? Math.round((filled / total) * 100) : 0;
return { filled, total, pct }; return { filled, total, pct };
}; };
@ -79,11 +53,11 @@ export default function ProfileCompletionWidget(props: Props) {
return ( return (
<DashboardWidget <DashboardWidget
title="Profile Completion" title="Profile Completion"
loading={profile.loading} loading={data.loading}
error={profile.error ? 'Failed to load' : undefined} error={data.error ? 'Failed to load' : undefined}
icon={<User size={16} />} icon={<User size={16} />}
> >
<Show when={!profile.loading && !profile.error}> <Show when={!data.loading && !data.error}>
<div style={{ 'margin-bottom': '8px' }}> <div style={{ 'margin-bottom': '8px' }}>
<div <div
style={{ style={{