fix: verification page transitions cleanly to Pending Review after wizard submit
All checks were successful
build-and-release / build (push) Successful in 1m37s
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:
parent
00489b7414
commit
9358ab541f
4 changed files with 80 additions and 39 deletions
|
|
@ -581,7 +581,9 @@ export default function ProfilePage(props: Props) {
|
||||||
? "jobseeker"
|
? "jobseeker"
|
||||||
: props.roleKey === "COMPANY"
|
: props.roleKey === "COMPANY"
|
||||||
? "companies"
|
? "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)
|
// 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.
|
// 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'
|
? 'jobseeker'
|
||||||
: props.roleKey === 'COMPANY'
|
: props.roleKey === 'COMPANY'
|
||||||
? 'companies'
|
? 'companies'
|
||||||
: props.roleKey.toLowerCase().replace(/_/g, '-') + 's';
|
: props.roleKey === 'CATERING_SERVICES'
|
||||||
|
? 'catering-services'
|
||||||
|
: props.roleKey.toLowerCase().replace(/_/g, '-') + 's';
|
||||||
try {
|
try {
|
||||||
const result = await uploadDocument(rolePrefix, file, doc.key);
|
const result = await uploadDocument(rolePrefix, file, doc.key);
|
||||||
const url = result?.url ?? result?.file_url ?? result?.path ?? String(result);
|
const url = result?.url ?? result?.file_url ?? result?.path ?? String(result);
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,11 @@ export default function RoleWizard(props: Props) {
|
||||||
return step.fields.filter(fieldRequired).every((f) => String(portfolioForm()[f.id] ?? "").trim().length > 0);
|
return step.fields.filter(fieldRequired).every((f) => String(portfolioForm()[f.id] ?? "").trim().length > 0);
|
||||||
}
|
}
|
||||||
if (step.type === "review") return true;
|
if (step.type === "review") return true;
|
||||||
// basic
|
// basic — file fields are tracked in docUrls, not form
|
||||||
return step.fields.filter(fieldRequired).every((f) => String(form()[f.id] ?? "").trim().length > 0);
|
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()));
|
const canAdvance = createMemo(() => stepIsComplete(currentStep()));
|
||||||
|
|
@ -108,7 +111,7 @@ export default function RoleWizard(props: Props) {
|
||||||
});
|
});
|
||||||
if (status === 200 || status === 201) {
|
if (status === 200 || status === 201) {
|
||||||
setSubmitSuccess(true);
|
setSubmitSuccess(true);
|
||||||
props.onSubmitted("PENDING");
|
setTimeout(() => props.onSubmitted("PENDING"), 2500);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setSubmitMsg(err?.message || "Submission failed. Please try again.");
|
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" }}>
|
<div style={{ display: "grid", "grid-template-columns": "1fr 1fr", gap: "10px" }}>
|
||||||
<For each={currentStep()?.fields ?? []}>
|
<For each={currentStep()?.fields ?? []}>
|
||||||
{(field) => (
|
{(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}>
|
<label style={LABEL}>
|
||||||
{field.label}
|
{field.label}
|
||||||
<Show when={field.required}><span style={{ color: "#EF4444" }}> *</span></Show>
|
<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>
|
<For each={field.options ?? []}>{(opt) => <option value={opt.value}>{opt.label}</option>}</For>
|
||||||
</select>
|
</select>
|
||||||
</Match>
|
</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}>
|
<Match when={true}>
|
||||||
<input
|
<input
|
||||||
type={field.type ?? "text"}
|
type={field.type ?? "text"}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,11 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
const docFields = createMemo(() => getDocFields(props.roleKey));
|
const docFields = createMemo(() => getDocFields(props.roleKey));
|
||||||
const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
|
const canResubmit = () => ['DOCUMENTS_REQUESTED', 'REVISION_REQUESTED', 'REJECTED'].includes(status());
|
||||||
const isApproved = () => ['APPROVED', 'COMPLETED'].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 hasPortfolio = () => {
|
||||||
const role = String(props.roleKey || '').toUpperCase();
|
const role = String(props.roleKey || '').toUpperCase();
|
||||||
return role === 'JOB_SEEKER' || role !== 'COMPANY' && role !== 'CUSTOMER';
|
return role === 'JOB_SEEKER' || role !== 'COMPANY' && role !== 'CUSTOMER';
|
||||||
|
|
@ -380,37 +384,33 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', 'align-items': 'center', gap: '0' }}>
|
<div style={{ display: 'flex', 'align-items': 'center', gap: '0' }}>
|
||||||
<For each={FLOW_STEPS}>
|
<For each={FLOW_STEPS}>
|
||||||
{(step, idx) => {
|
{(step, idx) => (
|
||||||
const done = currentStep() > idx();
|
<>
|
||||||
const active = currentStep() === idx() + 1;
|
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
|
||||||
return (
|
<div style={{
|
||||||
<>
|
width: '28px',
|
||||||
<div style={{ display: 'flex', 'flex-direction': 'column', 'align-items': 'center', 'flex-shrink': '0' }}>
|
height: '28px',
|
||||||
<div style={{
|
'border-radius': '999px',
|
||||||
width: '28px',
|
display: 'flex',
|
||||||
height: '28px',
|
'align-items': 'center',
|
||||||
'border-radius': '999px',
|
'justify-content': 'center',
|
||||||
display: 'flex',
|
'font-size': '11px',
|
||||||
'align-items': 'center',
|
'font-weight': '800',
|
||||||
'justify-content': 'center',
|
background: currentStep() > idx() ? '#FF5E13' : currentStep() === idx() + 1 ? '#FFF3EE' : '#F3F4F6',
|
||||||
'font-size': '11px',
|
color: currentStep() > idx() ? '#fff' : currentStep() === idx() + 1 ? '#FF5E13' : '#9CA3AF',
|
||||||
'font-weight': '800',
|
border: currentStep() === idx() + 1 ? '2px solid #FF5E13' : '2px solid transparent',
|
||||||
background: done ? '#FF5E13' : active ? '#FFF3EE' : '#F3F4F6',
|
}}>
|
||||||
color: done ? '#fff' : active ? '#FF5E13' : '#9CA3AF',
|
{currentStep() > idx() ? '✓' : idx() + 1}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<Show when={idx() < FLOW_STEPS.length - 1}>
|
<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' }}>
|
||||||
<div style={{ flex: '1', height: '2px', background: done ? '#FF5E13' : '#E5E7EB', 'margin-bottom': '18px' }} />
|
{step.label}
|
||||||
</Show>
|
</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>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -489,7 +489,10 @@ export default function VerificationStatusPage(props: Props) {
|
||||||
<ProfilePage
|
<ProfilePage
|
||||||
roleKey={props.roleKey}
|
roleKey={props.roleKey}
|
||||||
runtimeFields={props.runtimeFields || []}
|
runtimeFields={props.runtimeFields || []}
|
||||||
onVerificationStatusChange={props.onVerificationStatusChange}
|
onVerificationStatusChange={(s) => {
|
||||||
|
setStatus(s);
|
||||||
|
props.onVerificationStatusChange?.(s);
|
||||||
|
}}
|
||||||
onNavigate={props.onNavigate}
|
onNavigate={props.onNavigate}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,9 @@ export async function submitCompanyProfileWithDocuments(
|
||||||
fd.append('profile', JSON.stringify(profileData));
|
fd.append('profile', JSON.stringify(profileData));
|
||||||
for (const { documentType, file } of documents) {
|
for (const { documentType, file } of documents) {
|
||||||
// Append a stable filename so the backend can derive document_type from the stem
|
// 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`, {
|
const res = await fetch(`/api/companies/profile/submit-with-documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue