WORKFLOW-APP/components/RegistrationWizard.tsx

764 lines
33 KiB
TypeScript

// components/RegistrationWizard.tsx
// Client Component. New-company registration wizard.
"use client";
import { useState } from "react";
import type { DirectorInput, ShareholderInput, CompanyInput, WizardState } from "@/lib/wizard-types";
import DirectorStep from "@/components/DirectorStep";
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
const STEPS = [
{ id: "directors", label: "Directors" },
{ id: "company", label: "Company" },
{ id: "shareholders",label: "Shareholders" },
{ id: "review", label: "Review" },
] as const;
type StepId = (typeof STEPS)[number]["id"];
function uid(): string {
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: Math.random().toString(36).slice(2) + Date.now().toString(36);
}
function blankDirector(): DirectorInput {
return {
ui_key: uid(),
first_names: "", surname: "", id_no: "", date_of_birth: "",
residential_address: "", postal_address: "", cell_phone: "", email: "",
section69: {
q1_probation: null, q2_court_prohibited: null,
q3_unrehabilitated_insolvent: null, q4_public_regulation: null,
q5_removed_from_office: null, q6_convicted_offence: null,
},
};
}
function blankShareholder(): ShareholderInput {
return { ui_key: uid(), is_director: false, director_index: null, name: "", id_or_entity_no: "", share_percent: 0 };
}
function emptyCompany(): CompanyInput {
return {
proposed_name_1: "", proposed_name_2: "", proposed_name_3: "",
trading_address: "", postal_address: "",
financial_year_end: 2, // February default
authorised_shares: "1000",
company_cell: "", company_email: "",
public_officer: "", company_secretary: "",
};
}
// ── Company Step ──────────────────────────────────────────────────────────────
function CompanyStep({
company,
directors,
onUpdate,
}: {
company: CompanyInput;
directors: DirectorInput[];
onUpdate: (p: Partial<CompanyInput>) => void;
}) {
const set = (p: Partial<CompanyInput>) => onUpdate(p);
const filledDirectors = directors.filter((d) => d.first_names.trim() && d.surname.trim());
return (
<div className="space-y-6">
{/* Proposed names */}
<div className="bg-surface-warm border border-border rounded-lg p-5 space-y-3">
<p className="text-sm font-semibold text-fg">Proposed company names</p>
{([
{ key: "proposed_name_1" as const, placeholder: "First choice" },
{ key: "proposed_name_2" as const, placeholder: "Second choice" },
{ key: "proposed_name_3" as const, placeholder: "Third choice" },
]).map(({ key, placeholder }) => (
<div key={key}>
<label className="text-xs font-semibold text-muted mb-1 block">{placeholder}</label>
<input
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="Company name"
value={company[key]}
onChange={(e) => set({ [key]: e.target.value })}
/>
</div>
))}
</div>
{/* Addresses */}
<div className="bg-surface-warm border border-border rounded-lg p-5 space-y-3">
<p className="text-sm font-semibold text-fg">Addresses</p>
{/* Trading address */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-semibold text-muted">Trading / physical address</label>
{filledDirectors.length > 0 && (
<select
className="text-xs py-1 bg-surface border border-border rounded px-2 text-accent focus:outline-none focus:border-accent"
value=""
onChange={(e) => {
const idx = Number(e.target.value);
if (!isNaN(idx)) set({ trading_address: directors[idx]?.residential_address || "" });
}}
>
<option value="">Use director address ↓</option>
{directors.map((d, i) => (
<option key={d.ui_key} value={i}>
{d.first_names} {d.surname} — residential
</option>
))}
</select>
)}
</div>
<textarea
rows={2}
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors resize-none"
placeholder="Where the company will operate"
value={company.trading_address}
onChange={(e) => set({ trading_address: e.target.value })}
/>
</div>
{/* Postal address */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-semibold text-muted">Postal address</label>
{filledDirectors.length > 0 && (
<select
className="text-xs py-1 bg-surface border border-border rounded px-2 text-accent focus:outline-none focus:border-accent"
value=""
onChange={(e) => {
const idx = Number(e.target.value);
if (!isNaN(idx)) set({ postal_address: directors[idx]?.postal_address || directors[idx]?.residential_address || "" });
}}
>
<option value="">Use director address ↓</option>
{directors.map((d, i) => (
<option key={d.ui_key} value={i}>
{d.first_names} {d.surname} — postal
</option>
))}
</select>
)}
</div>
<textarea
rows={2}
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors resize-none"
placeholder="Postal address for correspondence"
value={company.postal_address}
onChange={(e) => set({ postal_address: e.target.value })}
/>
</div>
</div>
{/* Financial year end + share capital */}
<div className="bg-surface-warm border border-border rounded-lg p-5">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-semibold text-muted mb-1 block">Financial year end</label>
<select
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg focus:outline-none focus:border-accent transition-colors"
value={company.financial_year_end}
onChange={(e) => set({ financial_year_end: Number(e.target.value) })}
>
{MONTHS.map((m, i) => <option key={m} value={i + 1}>{m}</option>)}
</select>
</div>
<div>
<label className="text-xs font-semibold text-muted mb-1 block">Authorised share capital</label>
<select
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg focus:outline-none focus:border-accent transition-colors"
value={company.authorised_shares}
onChange={(e) => set({ authorised_shares: e.target.value as "100" | "1000" })}
>
<option value="100">100 Shares</option>
<option value="1000">1000 Shares</option>
</select>
</div>
</div>
</div>
{/* Contact + officers */}
<div className="bg-surface-warm border border-border rounded-lg p-5 space-y-4">
<p className="text-sm font-semibold text-fg">Company contact &amp; officers</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Company cell */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-semibold text-muted">Company cell</label>
{filledDirectors.length > 0 && (
<select
className="text-xs py-1 bg-surface border border-border rounded px-2 text-accent focus:outline-none focus:border-accent"
value=""
onChange={(e) => {
const idx = Number(e.target.value);
if (!isNaN(idx)) set({ company_cell: directors[idx]?.cell_phone || "" });
}}
>
<option value="">Use director cell ↓</option>
{directors.map((d, i) => (
<option key={d.ui_key} value={i}>{d.first_names} {d.surname}</option>
))}
</select>
)}
</div>
<input
type="tel"
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="e.g. 0871234567"
value={company.company_cell}
onChange={(e) => set({ company_cell: e.target.value })}
/>
</div>
{/* Company email */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs font-semibold text-muted">Company email</label>
{filledDirectors.length > 0 && (
<select
className="text-xs py-1 bg-surface border border-border rounded px-2 text-accent focus:outline-none focus:border-accent"
value=""
onChange={(e) => {
const idx = Number(e.target.value);
if (!isNaN(idx)) set({ company_email: directors[idx]?.email || "" });
}}
>
<option value="">Use director email ↓</option>
{directors.map((d, i) => (
<option key={d.ui_key} value={i}>{d.first_names} {d.surname}</option>
))}
</select>
)}
</div>
<input
type="email"
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="company@domain.co.za"
value={company.company_email}
onChange={(e) => set({ company_email: e.target.value })}
/>
</div>
</div>
{/* Public officer + secretary (not compulsory) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-semibold text-muted mb-1 block">
Public officer{" "}
<span className="text-muted font-normal">(optional)</span>
</label>
<input
type="text"
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="Full name"
value={company.public_officer}
onChange={(e) => set({ public_officer: e.target.value })}
/>
</div>
<div>
<label className="text-xs font-semibold text-muted mb-1 block">
Company secretary{" "}
<span className="text-muted font-normal">(optional)</span>
</label>
<input
type="text"
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="Full name"
value={company.company_secretary}
onChange={(e) => set({ company_secretary: e.target.value })}
/>
</div>
</div>
</div>
</div>
);
}
// ── Shareholder Step ─────────────────────────────────────────────────────────
function ShareholderStep({ state, onUpdate }: { state: WizardState; onUpdate: (s: WizardState) => void }) {
const updateSH = (i: number, patch: Partial<ShareholderInput>) => {
const shareholders = [...state.shareholders];
shareholders[i] = { ...shareholders[i], ...patch };
onUpdate({ ...state, shareholders });
};
const totalPct = state.shareholders.reduce((s, sh) => s + (sh.share_percent || 0), 0);
return (
<div className="space-y-6">
<div className="bg-surface-warm border border-border rounded-lg p-5">
<p className="text-sm font-semibold text-fg mb-3">How many shareholders?</p>
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
onClick={() => {
let shareholders = [...state.shareholders];
if (n > shareholders.length) {
while (shareholders.length < n) shareholders.push(blankShareholder());
} else {
shareholders = shareholders.slice(0, n);
}
onUpdate({ ...state, shareholders });
}}
className={
"w-10 h-10 rounded border font-semibold text-sm transition-colors " +
(state.shareholders.length === n ? "bg-accent text-accent-on border-transparent" : "bg-surface border-border text-muted hover:text-fg")
}
>
{n}
</button>
))}
</div>
<p className="text-sm mt-2">
Total: <strong className={totalPct === 100 ? "text-success" : "text-danger"}>{totalPct}%</strong>
{totalPct !== 100 && <span className="ml-1 text-danger">(must total 100%)</span>}
</p>
</div>
{state.shareholders.map((sh, i) => (
<div key={sh.ui_key} className="border border-border rounded-lg overflow-hidden">
<div className="bg-surface-warm border-b border-border px-5 py-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<p className="text-sm font-bold text-fg">Shareholder {i + 1}</p>
{state.directors.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted">Same as director:</span>
<select
className="text-xs py-1 bg-surface border border-border rounded px-2 text-fg focus:outline-none focus:border-accent"
value={sh.is_director ? String(sh.director_index ?? "") : ""}
onChange={(e) => {
const idx = Number(e.target.value);
if (!isNaN(idx) && state.directors[idx]) {
updateSH(i, {
is_director: true,
director_index: idx,
name: (state.directors[idx].first_names + " " + state.directors[idx].surname).trim(),
id_or_entity_no: state.directors[idx].id_no,
});
} else {
updateSH(i, { is_director: false, director_index: null, name: "", id_or_entity_no: "" });
}
}}
>
<option value="">— Not a director —</option>
{state.directors.map((d, j) => (
<option key={d.ui_key} value={j}>
{j + 1}. {d.first_names} {d.surname}
</option>
))}
</select>
</div>
)}
</div>
</div>
<div className="p-5 space-y-4">
{!sh.is_director && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="text-xs font-semibold text-muted mb-1 block">Full name / Entity name</label>
<input
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="Shareholder name or entity"
value={sh.name}
onChange={(e) => updateSH(i, { name: e.target.value })}
/>
</div>
<div>
<label className="text-xs font-semibold text-muted mb-1 block">ID or Company number</label>
<input
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
placeholder="ID or CK/CC/PTY number"
value={sh.id_or_entity_no}
onChange={(e) => updateSH(i, { id_or_entity_no: e.target.value })}
/>
</div>
</div>
)}
<div>
<label className="text-xs font-semibold text-muted mb-1 block">% Shareholding</label>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
max={100}
step={1}
className="w-24 bg-surface border border-border rounded px-3 py-2 text-sm text-fg focus:outline-none focus:border-accent transition-colors"
placeholder="0"
value={sh.share_percent || ""}
onChange={(e) => updateSH(i, { share_percent: Number(e.target.value) })}
/>
<span className="text-sm text-muted">%</span>
</div>
</div>
</div>
</div>
))}
</div>
);
}
// ── Review Step ───────────────────────────────────────────────────────────────
function ReviewStep({ state }: { state: WizardState }) {
return (
<div className="space-y-6">
<div className="bg-surface-warm border border-border rounded-lg p-5">
<p className="text-xs uppercase tracking-wider text-accent font-bold mb-3">Directors</p>
{state.directors.map((d, i) => (
<div key={d.ui_key} className="py-3 border-b border-border last:border-0">
<p className="text-sm font-semibold text-fg">{d.first_names} {d.surname}</p>
<p className="text-xs text-muted">ID: {d.id_no || "—"} &middot; {d.cell_phone}</p>
</div>
))}
</div>
<div className="bg-surface-warm border border-border rounded-lg p-5">
<p className="text-xs uppercase tracking-wider text-accent font-bold mb-3">Company</p>
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
<p className="text-muted">Name 1</p><p className="text-fg font-medium">{state.company.proposed_name_1 || "—"}</p>
<p className="text-muted">Name 2</p><p className="text-fg font-medium">{state.company.proposed_name_2 || "—"}</p>
<p className="text-muted">Name 3</p><p className="text-fg font-medium">{state.company.proposed_name_3 || "—"}</p>
<p className="text-muted">Trading</p><p className="text-fg font-medium">{state.company.trading_address || "—"}</p>
<p className="text-muted">Postal</p><p className="text-fg font-medium">{state.company.postal_address || "—"}</p>
<p className="text-muted">Year end</p><p className="text-fg font-medium">{MONTHS[state.company.financial_year_end - 1]}</p>
<p className="text-muted">Shares</p><p className="text-fg font-medium">{state.company.authorised_shares}</p>
<p className="text-muted">Public officer</p><p className="text-fg font-medium">{state.company.public_officer || "—"}</p>
<p className="text-muted">Secretary</p><p className="text-fg font-medium">{state.company.company_secretary || "—"}</p>
</div>
</div>
<div className="bg-surface-warm border border-border rounded-lg p-5">
<p className="text-xs uppercase tracking-wider text-accent font-bold mb-3">Shareholders</p>
{state.shareholders.map((sh, i) => (
<div key={sh.ui_key} className="py-2 border-b border-border last:border-0 flex justify-between items-start">
<div>
<p className="text-sm font-semibold text-fg">{sh.name || "—"}</p>
<p className="text-xs text-muted">{sh.id_or_entity_no || "—"}</p>
</div>
<span className="text-sm font-bold text-accent">{sh.share_percent}%</span>
</div>
))}
<p className="text-xs text-muted mt-2">Total: {state.shareholders.reduce((s, sh) => s + sh.share_percent, 0)}%</p>
</div>
</div>
);
}
// ── Validation ───────────────────────────────────────────────────────────────
function canProceed(step: StepId, state: WizardState): boolean {
if (step === "directors") {
return state.directors.every(
(d) => d.first_names.trim() && d.surname.trim() && d.id_no.trim().length >= 5 &&
d.cell_phone.trim() && d.email.includes("@") &&
Object.values(d.section69).every((v) => v !== null)
);
}
if (step === "shareholders") {
const total = state.shareholders.reduce((s, sh) => s + sh.share_percent, 0);
return total === 100 && state.shareholders.every((sh) => sh.share_percent > 0);
}
return true;
}
// ── Main component ────────────────────────────────────────────────────────────
interface PrefillData {
directors: {
first_names: string;
surname: string;
id_no: string;
date_of_birth: string;
residential_address: string;
postal_address: string;
cell_phone: string;
email: string;
section69_answers: string;
}[];
shareholders: {
name: string;
id_or_entity_no: string;
share_percent: string;
is_director: string;
}[];
}
export default function RegistrationWizard({
registrationId,
prefill,
}: {
registrationId: string;
prefill?: PrefillData;
}) {
const [step, setStep] = useState<StepId>("directors");
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const isUpdate = !!prefill;
// Initialize state — either blank or from prefill data
const [state, setState] = useState<WizardState>(() => {
if (prefill && prefill.directors.length > 0) {
return {
director_count: prefill.directors.length,
directors: prefill.directors.map((d) => {
let section69 = {
q1_probation: null as boolean | null,
q2_court_prohibited: null as boolean | null,
q3_unrehabilitated_insolvent: null as boolean | null,
q4_public_regulation: null as boolean | null,
q5_removed_from_office: null as boolean | null,
q6_convicted_offence: null as boolean | null,
};
try {
const raw = typeof d.section69_answers === "string" ? JSON.parse(d.section69_answers) : d.section69_answers;
if (raw) {
section69 = {
q1_probation: raw.q1_probation ?? null,
q2_court_prohibited: raw.q2_court_prohibited ?? null,
q3_unrehabilitated_insolvent: raw.q3_unrehabilitated_insolvent ?? null,
q4_public_regulation: raw.q4_public_regulation ?? null,
q5_removed_from_office: raw.q5_removed_from_office ?? null,
q6_convicted_offence: raw.q6_convicted_offence ?? null,
};
}
} catch { /* use defaults */ }
return {
ui_key: uid(),
first_names: d.first_names || "",
surname: d.surname || "",
id_no: d.id_no || "",
date_of_birth: d.date_of_birth || "",
residential_address: d.residential_address || "",
postal_address: d.postal_address || "",
cell_phone: d.cell_phone || "",
email: d.email || "",
section69,
};
}),
company: emptyCompany(),
shareholders: prefill.shareholders.length > 0
? prefill.shareholders.map((s) => ({
ui_key: uid(),
is_director: s.is_director === "true",
director_index: null,
name: s.name || "",
id_or_entity_no: s.id_or_entity_no || "",
share_percent: parseFloat(s.share_percent) || 0,
}))
: [blankShareholder()],
};
}
return {
director_count: 1,
directors: [blankDirector()],
company: emptyCompany(),
shareholders: [blankShareholder()],
};
});
const stepIndex = STEPS.findIndex((s) => s.id === step);
const handleSubmit = async () => {
setSubmitting(true);
setSubmitError(null);
try {
const res = await fetch("/api/register/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ registrationId, ...state, isUpdate }),
});
if (!res.ok) throw new Error(await res.text());
const data = await res.json();
if (isUpdate) {
// Show update success and redirect back
alert("Registration updated! The data has been saved.");
window.location.href = `${window.location.origin}/`;
} else {
setSubmitted(true);
}
} catch (err: unknown) {
setSubmitError(err instanceof Error ? err.message : "Submission failed");
} finally {
setSubmitting(false);
}
};
if (submitted) {
return (
<div className="container-x py-16 text-center max-w-lg mx-auto">
<div className="w-16 h-16 bg-success/10 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-fg font-semibold text-xl mb-2">Registration submitted</h2>
<p className="text-muted text-sm">Your information has been received. Your registration agent will be in touch shortly.</p>
</div>
);
}
return (
<div className="container-x py-8 max-w-3xl mx-auto">
{/* Step indicator */}
<div className="flex items-center gap-0 mb-8 overflow-x-auto">
{STEPS.map((s, i) => {
const isActive = s.id === step;
const isPast = i < stepIndex;
return (
<div key={s.id} className="flex items-center flex-shrink-0">
<button
type="button"
onClick={() => { if (i <= stepIndex) setStep(s.id); }}
disabled={!isActive && !isPast}
className={
"flex items-center gap-2 px-3 py-2 text-sm font-semibold border-b-2 transition-colors " +
(isActive ? "border-accent text-accent" : isPast ? "border-success text-success cursor-pointer" : "border-transparent text-muted cursor-not-allowed")
}
>
<span className={
"w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold " +
(isActive ? "bg-accent text-accent-on" : isPast ? "bg-success text-success-on" : "bg-surface border border-border text-muted")
}>
{isPast ? "\u2713" : i + 1}
</span>
{s.label}
</button>
{i < STEPS.length - 1 && <div className="w-4 h-px bg-border mx-1" />}
</div>
);
})}
</div>
{/* Step content */}
<div className="mb-8">
{step === "directors" && (
<div>
<div className="mb-4">
<p className="text-xs uppercase tracking-wider text-accent font-bold">Step 1</p>
<h2 className="text-fg font-semibold text-xl">Director details</h2>
</div>
<div className="bg-surface-warm border border-border rounded-lg p-5 mb-6">
<p className="text-sm font-semibold text-fg mb-3">How many directors?</p>
<div className="flex gap-2">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
onClick={() => {
let directors = [...state.directors];
if (n > directors.length) {
while (directors.length < n) directors.push(blankDirector());
} else {
directors = directors.slice(0, n);
}
setState({ ...state, director_count: n, directors });
}}
className={
"w-10 h-10 rounded border font-semibold text-sm transition-colors " +
(state.director_count === n ? "bg-accent text-accent-on border-transparent" : "bg-surface border-border text-muted hover:text-fg")
}
>
{n}
</button>
))}
</div>
<p className="text-xs text-muted mt-2">Each director must complete all fields and answer Section 69 questions.</p>
</div>
{state.directors.map((director, i) => (
<DirectorStep
key={director.ui_key}
director={director}
index={i}
onUpdate={(patch) => {
const directors = [...state.directors];
directors[i] = { ...directors[i], ...patch };
setState({ ...state, directors });
}}
/>
))}
</div>
)}
{step === "company" && (
<div>
<div className="mb-4">
<p className="text-xs uppercase tracking-wider text-accent font-bold">Step 2</p>
<h2 className="text-fg font-semibold text-xl">Company information</h2>
</div>
<CompanyStep
company={state.company}
directors={state.directors}
onUpdate={(p) => setState({ ...state, company: { ...state.company, ...p } })}
/>
</div>
)}
{step === "shareholders" && (
<div>
<div className="mb-4">
<p className="text-xs uppercase tracking-wider text-accent font-bold">Step 3</p>
<h2 className="text-fg font-semibold text-xl">Shareholders</h2>
</div>
<ShareholderStep state={state} onUpdate={setState} />
</div>
)}
{step === "review" && (
<div>
<div className="mb-4">
<p className="text-xs uppercase tracking-wider text-accent font-bold">Step 4</p>
<h2 className="text-fg font-semibold text-xl">Review &amp; submit</h2>
</div>
<ReviewStep state={state} />
</div>
)}
</div>
{submitError && (
<div className="mb-4 p-4 bg-danger/10 border border-danger/30 rounded text-danger text-sm">{submitError}</div>
)}
<div className="flex items-center justify-between pt-4 border-t border-border">
<button
type="button"
onClick={() => { if (stepIndex > 0) setStep(STEPS[stepIndex - 1].id); }}
disabled={stepIndex === 0}
className="px-5 py-2 text-sm font-semibold text-muted hover:text-fg disabled:opacity-30 transition-colors"
>
&larr; Back
</button>
{step !== "review" ? (
<button
type="button"
onClick={() => { if (stepIndex < STEPS.length - 1) setStep(STEPS[stepIndex + 1].id); }}
disabled={!canProceed(step, state)}
className="px-6 py-2 bg-accent text-accent-on rounded font-semibold text-sm disabled:opacity-40 hover:opacity-90 transition-opacity"
>
Continue &rarr;
</button>
) : (
<button
type="button"
onClick={handleSubmit}
disabled={submitting}
className="px-6 py-2 bg-success text-success-on rounded font-semibold text-sm disabled:opacity-40 hover:opacity-90 transition-opacity"
>
{submitting ? "Submitting..." : "Submit registration"}
</button>
)}
</div>
</div>
);
}