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}>
|
||||
{/* ── 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) => (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
// 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 };
|
||||
}
|
||||
}
|
||||
if (roleKey === 'JOB_SEEKER') {
|
||||
const res = await apiFetch('/api/jobseeker/profile/me');
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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={{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue