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}>
{/* ── 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" }}>
<For each={currentStep()?.fields ?? []}>
{(field) => (
@ -212,7 +215,10 @@ export default function RoleWizard(props: Props) {
</Show>
{/* ── 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" }}>
<For each={currentStep()?.fields ?? []}>
{(field) => (
@ -260,7 +266,10 @@ export default function RoleWizard(props: Props) {
</Show>
{/* ── 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" }}>
<For each={currentStep()?.fields ?? []}>
{(field) => (

View file

@ -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<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) {
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 (
<DashboardWidget
title="Profile Completion"
loading={profile.loading}
error={profile.error ? 'Failed to load' : undefined}
loading={data.loading}
error={data.error ? 'Failed to load' : undefined}
icon={<User size={16} />}
>
<Show when={!profile.loading && !profile.error}>
<Show when={!data.loading && !data.error}>
<div style={{ 'margin-bottom': '8px' }}>
<div
style={{