Fix Profile Completion widget stuck at wrong % and empty-field wizard steps
All checks were successful
build-and-release / build (push) Successful in 1m42s
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:
parent
f4b9885974
commit
012a6a6072
2 changed files with 39 additions and 56 deletions
|
|
@ -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) => (
|
||||||
|
|
|
||||||
|
|
@ -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={{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue