From f4b9885974653721a8ca869eaaae4f5d0419b0a7 Mon Sep 17 00:00:00 2001 From: Ashwin Kumar Sivakumar Date: Tue, 28 Jul 2026 20:10:41 +0530 Subject: [PATCH] Add schema-driven verification wizard, replacing per-role hardcoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalizes the COMPANY-only 3-step wizard into RoleWizard, a component driven entirely by the role's active onboarding_configs schema (fetched via fetchOnboardingSchemaForRole) rather than hardcoded field/step lists. Which roles get the wizard, and when, is now a runtime toggle (enableWizardFlow) set through the new admin schema editor: - ProfilePage shows the wizard while unverified/sent-back-for-fixes, falls back to the existing free-form tabs once approved, and greys out (both client- and server-side) any field the schema marks lockAfterApproval once approved — previously isLocked() only covered the in-review window and unlocked everything again at APPROVED. - dashboard.tsx hides My Profile/My Portfolio from the sidebar pre-approval only for roles that have the wizard enabled (roles not yet rolled out keep direct access, so they aren't left with no way to complete their profile), and shows a checkmark on the Verification nav item once approved. Rolling out to more roles is now purely an admin-panel config change — no further code needed, since RoleWizard has no role-specific branches. Co-Authored-By: Claude Sonnet 5 --- src/components/DashboardShell.tsx | 5 + src/components/dashboard/ProfilePage.tsx | 79 ++++- src/components/dashboard/RoleWizard.tsx | 351 +++++++++++++++++++++++ src/lib/runtime/storage.ts | 30 ++ src/lib/runtime/types.ts | 16 ++ src/routes/dashboard.tsx | 32 ++- 6 files changed, 496 insertions(+), 17 deletions(-) create mode 100644 src/components/dashboard/RoleWizard.tsx diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx index cffdfbf..683f512 100644 --- a/src/components/DashboardShell.tsx +++ b/src/components/DashboardShell.tsx @@ -87,6 +87,8 @@ interface Props { roleKey: string; userName?: string; isAdmin?: boolean; + /** Shows a checkmark on the Verification nav item once the profile is approved. */ + verificationApproved?: boolean; children: JSX.Element; } @@ -196,6 +198,9 @@ export default function DashboardShell(props: Props) { {titleCase(item)} + + + ); }} diff --git a/src/components/dashboard/ProfilePage.tsx b/src/components/dashboard/ProfilePage.tsx index 1cb50c1..110a388 100644 --- a/src/components/dashboard/ProfilePage.tsx +++ b/src/components/dashboard/ProfilePage.tsx @@ -28,6 +28,8 @@ import { type BasicField, type DocField, } from "~/lib/profile-fields-config"; +import { fetchOnboardingSchemaForRole } from "~/lib/runtime/storage"; +import RoleWizard from "~/components/dashboard/RoleWizard"; // The K8s ingress routes /api/* directly to the Rust gateway service in // production — there is no /api/gateway rewrite layer, so paths below are @@ -566,8 +568,32 @@ export default function ProfilePage(props: Props) { const [wizardSubmitMsg, setWizardSubmitMsg] = createSignal(""); const [wizardSubmitSuccess, setWizardSubmitSuccess] = createSignal(false); + // ── Runtime onboarding config: wizard-vs-tabs + per-field post-approval lock ── + // Admin-configurable via the Onboarding Schema Editor (onboarding_configs + // table) — nothing here is hardcoded per role. + const [wizardEnabled, setWizardEnabled] = createSignal(false); + const [lockedFieldIds, setLockedFieldIds] = createSignal>(new Set()); + const isCompany = () => props.roleKey === "COMPANY"; + const rolePrefix = () => + props.roleKey === "JOB_SEEKER" + ? "jobseeker" + : props.roleKey === "COMPANY" + ? "companies" + : props.roleKey.toLowerCase().replace(/_/g, "-") + "s"; + + // Statuses during which the guided wizard (rather than free-form tabs) + // should be shown: first-time setup, or the admin sent it back for fixes. + const WIZARD_STATUSES = ["NOT_SUBMITTED", "DOCUMENTS_REQUESTED", "REVISION_REQUESTED", "REJECTED"]; + const showWizard = () => wizardEnabled() && WIZARD_STATUSES.includes(verificationStatus()); + + // A field is permanently locked once the profile has ever been approved, + // if the active onboarding schema marked it lockAfterApproval — enforced + // server-side too (apps/users/src/handlers/profile.rs), this just keeps + // the UI honest about it. + const fieldLocked = (key: string) => isLocked() || (verificationStatus() === "APPROVED" && lockedFieldIds().has(key)); + const requiresPortfolio = () => props.roleKey === "JOB_SEEKER" || Boolean(PORTFOLIO_PREFIX[props.roleKey]); const refreshPortfolioSubmission = async (): Promise => { @@ -651,6 +677,18 @@ export default function ProfilePage(props: Props) { // Load saved profile + verification status on mount onMount(async () => { + void fetchOnboardingSchemaForRole(props.roleKey).then((cfg) => { + if (!cfg) return; + setWizardEnabled(Boolean(cfg.enableWizardFlow)); + const locked = new Set(); + for (const step of cfg.steps) { + for (const field of step.fields) { + if (field.lockAfterApproval) locked.add(field.id); + } + } + setLockedFieldIds(locked); + }); + const [profileRes, statusRes] = await Promise.all([ apiFetch(`/api/profile?roleKey=${props.roleKey}`), apiFetch(`/api/me/verification-status?roleKey=${props.roleKey}`), @@ -956,7 +994,7 @@ export default function ProfilePage(props: Props) { return (
- }> + { + setVerificationStatus(status); + props.onVerificationStatusChange?.(status); + }} + /> + } + >