fix: verification page transitions cleanly to Pending Review after wizard submit
All checks were successful
build-and-release / build (push) Successful in 1m37s

- VerificationStatusPage: showInlineEditors now hides for PENDING and
  UNDER_REVIEW (only shows when user action is needed: NOT_SUBMITTED,
  DOCUMENTS_REQUESTED, REVISION_REQUESTED, REJECTED). Previously the
  old ProfilePage form with validation errors appeared after submission.

- VerificationStatusPage: wrap onVerificationStatusChange passed to
  ProfilePage so the local status signal also updates when the wizard
  calls onSubmitted. Previously the status card kept showing NOT_SUBMITTED
  until a page reload.

- VerificationStatusPage progress tracker: inline signal reads directly
  in JSX instead of capturing them in local variables. In Solid.js, local
  const done = signal() inside a For callback is computed once and goes
  stale — the step circles stayed grey even after status changed to PENDING.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tracewebstudio Dev 2026-07-30 15:36:29 +02:00
parent 00489b7414
commit 9358ab541f
4 changed files with 80 additions and 39 deletions

View file

@ -581,7 +581,9 @@ export default function ProfilePage(props: Props) {
? "jobseeker"
: props.roleKey === "COMPANY"
? "companies"
: props.roleKey.toLowerCase().replace(/_/g, "-") + "s";
: props.roleKey === "CATERING_SERVICES"
? "catering-services"
: 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.
@ -1277,7 +1279,9 @@ export default function ProfilePage(props: Props) {
? 'jobseeker'
: props.roleKey === 'COMPANY'
? 'companies'
: props.roleKey.toLowerCase().replace(/_/g, '-') + 's';
: props.roleKey === 'CATERING_SERVICES'
? 'catering-services'
: props.roleKey.toLowerCase().replace(/_/g, '-') + 's';
try {
const result = await uploadDocument(rolePrefix, file, doc.key);
const url = result?.url ?? result?.file_url ?? result?.path ?? String(result);

View file

@ -62,8 +62,11 @@ export default function RoleWizard(props: Props) {
return step.fields.filter(fieldRequired).every((f) => String(portfolioForm()[f.id] ?? "").trim().length > 0);
}
if (step.type === "review") return true;
// basic
return step.fields.filter(fieldRequired).every((f) => String(form()[f.id] ?? "").trim().length > 0);
// basic — file fields are tracked in docUrls, not form
return step.fields.filter(fieldRequired).every((f) => {
if (f.type === "file") return Boolean(docUrls()[f.id]);
return String(form()[f.id] ?? "").trim().length > 0;
});
};
const canAdvance = createMemo(() => stepIsComplete(currentStep()));
@ -108,7 +111,7 @@ export default function RoleWizard(props: Props) {
});
if (status === 200 || status === 201) {
setSubmitSuccess(true);
props.onSubmitted("PENDING");
setTimeout(() => props.onSubmitted("PENDING"), 2500);
}
} catch (err: any) {
setSubmitMsg(err?.message || "Submission failed. Please try again.");
@ -178,7 +181,7 @@ export default function RoleWizard(props: Props) {
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "10px" }}>
<For each={currentStep()?.fields ?? []}>
{(field) => (
<div style={{ "grid-column": field.type === "textarea" ? "span 2" : "span 1" }}>
<div style={{ "grid-column": (field.type === "textarea" || field.type === "file") ? "span 2" : "span 1" }}>
<label style={LABEL}>
{field.label}
<Show when={field.required}><span style={{ color: "#EF4444" }}> *</span></Show>
@ -198,6 +201,35 @@ export default function RoleWizard(props: Props) {
<For each={field.options ?? []}>{(opt) => <option value={opt.value}>{opt.label}</option>}</For>
</select>
</Match>
<Match when={field.type === "file"}>
<div style={{ display: "flex", "align-items": "center", gap: "10px", "flex-wrap": "wrap" }}>
<input
type="file"
id={`rw-file-basic-${field.id}`}
style={{ display: "none" }}
accept={(field as any).accept}
multiple={(field as any).multiple}
disabled={docUploading()[field.id]}
onChange={(e) => {
const file = e.currentTarget.files?.[0];
if (file) void handleFileSelect(field, file);
e.currentTarget.value = "";
}}
/>
<label for={`rw-file-basic-${field.id}`} style={{ ...BTN_GHOST, display: "inline-flex", "align-items": "center", "line-height": "1", cursor: docUploading()[field.id] ? "not-allowed" : "pointer", opacity: docUploading()[field.id] ? 0.6 : 1 }}>
{docUploading()[field.id] ? "Uploading…" : "Choose File"}
</label>
<Show when={docUrls()[field.id]}>
<span style={{ "font-size": "12px", "font-weight": "600", color: "#10B981", background: "#ECFDF5", padding: "4px 10px", "border-radius": "6px" }}> Uploaded</span>
</Show>
<Show when={!docUrls()[field.id] && !docUploading()[field.id]}>
<span style={{ "font-size": "12px", color: "#9CA3AF" }}>No file chosen</span>
</Show>
<Show when={docErrors()[field.id]}>
<p style={{ margin: "4px 0 0", "font-size": "11px", color: "#EF4444", width: "100%" }}>{docErrors()[field.id]}</p>
</Show>
</div>
</Match>
<Match when={true}>
<input
type={field.type ?? "text"}

View file

@ -212,7 +212,11 @@ export default function VerificationStatusPage(props: Props) {
const docFields = createMemo(() => getDocFields(props.roleKey));
const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
const isApproved = () => ['APPROVED', 'COMPLETED'].includes(status());
const showInlineEditors = () => !isApproved();
// Hide the inline form editors once submitted — PENDING/UNDER_REVIEW don't
// need editing, only statuses that require user action (NOT_SUBMITTED,
// DOCUMENTS_REQUESTED, REVISION_REQUESTED, REJECTED) do.
const showInlineEditors = () =>
['NOT_SUBMITTED', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
const hasPortfolio = () => {
const role = String(props.roleKey || '').toUpperCase();
return role === 'JOB_SEEKER' || role !== 'COMPANY' && role !== 'CUSTOMER';
@ -380,37 +384,33 @@ export default function VerificationStatusPage(props: Props) {
</p>
<div style={{ display: 'flex', 'align-items': 'center', gap: '0' }}>
<For each={FLOW_STEPS}>
{(step, idx) => {
const done = currentStep() > idx();
const active = currentStep() === idx() + 1;
return (
<>
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
<div style={{
width: '28px',
height: '28px',
'border-radius': '999px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'font-size': '11px',
'font-weight': '800',
background: done ? '#FF5E13' : active ? '#FFF3EE' : '#F3F4F6',
color: done ? '#fff' : active ? '#FF5E13' : '#9CA3AF',
border: active ? '2px solid #FF5E13' : '2px solid transparent',
}}>
{done ? '✓' : idx() + 1}
</div>
<p style={{ margin: '4px 0 0', 'font-size': '10px', 'font-weight': '600', color: done || active ? '#374151' : '#9CA3AF', 'white-space': 'nowrap', 'text-align': 'center' }}>
{step.label}
</p>
{(step, idx) => (
<>
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
<div style={{
width: '28px',
height: '28px',
'border-radius': '999px',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
'font-size': '11px',
'font-weight': '800',
background: currentStep() > idx() ? '#FF5E13' : currentStep() === idx() + 1 ? '#FFF3EE' : '#F3F4F6',
color: currentStep() > idx() ? '#fff' : currentStep() === idx() + 1 ? '#FF5E13' : '#9CA3AF',
border: currentStep() === idx() + 1 ? '2px solid #FF5E13' : '2px solid transparent',
}}>
{currentStep() > idx() ? '✓' : idx() + 1}
</div>
<Show when={idx() < FLOW_STEPS.length - 1}>
<div style={{ flex: '1', height: '2px', background: done ? '#FF5E13' : '#E5E7EB', 'margin-bottom': '18px' }} />
</Show>
</>
);
}}
<p style={{ margin: '4px 0 0', 'font-size': '10px', 'font-weight': '600', color: currentStep() > idx() || currentStep() === idx() + 1 ? '#374151' : '#9CA3AF', 'white-space': 'nowrap', 'text-align': 'center' }}>
{step.label}
</p>
</div>
<Show when={idx() < FLOW_STEPS.length - 1}>
<div style={{ flex: '1', height: '2px', background: currentStep() > idx() ? '#FF5E13' : '#E5E7EB', 'margin-bottom': '18px' }} />
</Show>
</>
)}
</For>
</div>
</div>
@ -489,7 +489,10 @@ export default function VerificationStatusPage(props: Props) {
<ProfilePage
roleKey={props.roleKey}
runtimeFields={props.runtimeFields || []}
onVerificationStatusChange={props.onVerificationStatusChange}
onVerificationStatusChange={(s) => {
setStatus(s);
props.onVerificationStatusChange?.(s);
}}
onNavigate={props.onNavigate}
/>
</Show>

View file

@ -276,7 +276,9 @@ export async function submitCompanyProfileWithDocuments(
fd.append('profile', JSON.stringify(profileData));
for (const { documentType, file } of documents) {
// Append a stable filename so the backend can derive document_type from the stem
fd.append('documents', file, `${documentType}_${file.name}`);
// Use | as separator so the backend can split on the first | to recover documentType,
// regardless of underscores in either the doc type key or the original filename.
fd.append('documents', file, `${documentType}|${file.name}`);
}
const res = await fetch(`/api/companies/profile/submit-with-documents`, {
method: 'POST',