WORKFLOW-APP/components/DirectorStep.tsx
Dave da504c45a0 wip: registration wizard, address splits, document panel, appointments
Uncommitted work in progress at the point of starting the Kanban
project. Captures:
- Registration wizard refactor + multi-step routing
- structured physical/postal address fields on companies and directors
- appointments flow (sign + verify)
- document type manager + upload/panel for companies and directors
- status actions, approve button, action/dropdown buttons
- step2 review page

tsconfig.tsbuildinfo is now ignored (build artefact).
2026-08-17 18:33:56 +02:00

220 lines
11 KiB
TypeScript

// components/DirectorStep.tsx
"use client";
import type { DirectorInput, Section69 } from "@/lib/wizard-types";
const SA_PROVINCES = [
"Eastern Cape", "Free State", "Gauteng", "KwaZulu-Natal",
"Limpopo", "Mpumalanga", "Northern Cape", "North West", "Western Cape",
];
function ProvinceSelect({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return (
<select
className={INPUT_CLS}
value={value}
onChange={(e) => onChange(e.target.value)}
>
<option value="">— Select province —</option>
{SA_PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
);
}
function Section69Questions({ section69, onUpdate }: { section69: Section69; onUpdate: (s: Section69) => void }) {
const qs: { key: keyof Section69; label: string }[] = [
{ key: "q1_probation", label: "Are you a person who has been placed under probation by a court in terms of section 162, or in terms of section 47 of the Close Corporations Act, 1984?" },
{ key: "q2_court_prohibited", label: "Has a court prohibited you to be a director, or declared you to be delinquent in terms of section 162?" },
{ key: "q3_unrehabilitated_insolvent", label: "Are you an unrehabilitated insolvent?" },
{ key: "q4_public_regulation", label: "Are you prohibited in terms of any public regulation to be a director of the company?" },
{ key: "q5_removed_from_office", label: "Were you removed from an office of trust, on grounds of misconduct involving dishonesty?" },
{ key: "q6_convicted_offence", label: "Were you convicted and imprisoned without the option of a fine, or fined more than the prescribed amount, for theft, fraud, forgery, perjury or an offence?" },
];
return (
<div className="space-y-4">
{qs.map((q) => {
const val = section69[q.key];
return (
<div key={q.key} className="flex flex-col sm:flex-row sm:items-start sm:gap-4">
<div className="sm:w-2/3 text-sm text-fg leading-relaxed pt-0.5">{q.label}</div>
<div className="sm:w-1/3 flex-shrink-0">
<div className="flex gap-2 mt-1">
{[true, false].map((opt) => {
const active = val === opt;
return (
<button
key={String(opt)}
type="button"
onClick={() => onUpdate({ ...section69, [q.key]: active ? null : opt })}
className={
"px-4 py-1.5 rounded text-sm font-semibold border transition-colors " +
(active
? opt ? "bg-success text-success-on border-transparent" : "bg-danger text-danger-on border-transparent"
: "bg-surface border-border text-muted hover:text-fg")
}
>
{opt ? "Yes" : "No"}
</button>
);
})}
</div>
</div>
</div>
);
})}
</div>
);
}
const INPUT_CLS = "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";
const LABEL_CLS = "text-xs font-semibold text-muted mb-1 block";
interface Props {
director: DirectorInput;
index: number;
onUpdate: (patch: Partial<DirectorInput>) => void;
}
export default function DirectorStep({ director: d, index, onUpdate }: Props) {
const updateS69 = (s: Section69) => onUpdate({ section69: s });
return (
<div className="border border-border rounded-lg overflow-hidden mb-6 last:mb-0">
<div className="bg-surface-warm border-b border-border px-5 py-3">
<p className="text-sm font-bold text-fg">Director {index + 1}</p>
</div>
<div className="p-5 space-y-5">
{/* Name + ID */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className={LABEL_CLS}>First names</label>
<input className={INPUT_CLS} placeholder="e.g. Stephanus Johannes" value={d.first_names} onChange={(e) => onUpdate({ first_names: e.target.value })} />
</div>
<div className="space-y-1.5">
<label className={LABEL_CLS}>Surname</label>
<input className={INPUT_CLS} placeholder="e.g. van der Merwe" value={d.surname} onChange={(e) => onUpdate({ surname: e.target.value })} />
</div>
<div className="space-y-1.5">
<label className={LABEL_CLS}>SA ID number</label>
<input type="tel" className={INPUT_CLS} placeholder="13-digit SA ID" maxLength={13} value={d.id_no} onChange={(e) => onUpdate({ id_no: e.target.value })} />
</div>
<div className="space-y-1.5">
<label className={LABEL_CLS}>Date of birth</label>
<input type="date" className={INPUT_CLS} value={d.date_of_birth} onChange={(e) => onUpdate({ date_of_birth: e.target.value })} />
</div>
</div>
{/* Residential address */}
<div className="bg-surface border border-border rounded-lg p-4 space-y-3">
<p className="text-xs font-bold text-accent uppercase tracking-wider">Residential address</p>
<div>
<label className={LABEL_CLS}>Address line 1</label>
<input className={INPUT_CLS} placeholder="Street number and name" value={d.residential_address_line_1} onChange={(e) => onUpdate({ residential_address_line_1: e.target.value })} />
</div>
<div>
<label className={LABEL_CLS}>Address line 2 <span className="text-muted font-normal">(optional)</span></label>
<input className={INPUT_CLS} placeholder="Unit, building, suburb" value={d.residential_address_line_2} onChange={(e) => onUpdate({ residential_address_line_2: e.target.value })} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className={LABEL_CLS}>Town / City</label>
<input className={INPUT_CLS} placeholder="e.g. Centurion" value={d.residential_town_city} onChange={(e) => onUpdate({ residential_town_city: e.target.value })} />
</div>
<div>
<label className={LABEL_CLS}>Province</label>
<ProvinceSelect value={d.residential_province} onChange={(v) => onUpdate({ residential_province: v })} />
</div>
<div>
<label className={LABEL_CLS}>Postal code</label>
<input className={INPUT_CLS} placeholder="e.g. 0157" value={d.residential_postal_code} onChange={(e) => onUpdate({ residential_postal_code: e.target.value })} />
</div>
</div>
</div>
{/* Postal address */}
<div className="bg-surface border border-border rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs font-bold text-accent uppercase tracking-wider">Postal address</p>
<button
type="button"
className="text-xs text-accent hover:underline"
onClick={() => onUpdate({
postal_address_line_1: d.residential_address_line_1,
postal_address_line_2: d.residential_address_line_2,
postal_town_city: d.residential_town_city,
postal_province: d.residential_province,
postal_postal_code: d.residential_postal_code,
})}
>
Copy residential
</button>
</div>
<div>
<label className={LABEL_CLS}>Address line 1</label>
<input className={INPUT_CLS} placeholder="PO Box or street" value={d.postal_address_line_1} onChange={(e) => onUpdate({ postal_address_line_1: e.target.value })} />
</div>
<div>
<label className={LABEL_CLS}>Address line 2 <span className="text-muted font-normal">(optional)</span></label>
<input className={INPUT_CLS} placeholder="PO Box number, suburb" value={d.postal_address_line_2} onChange={(e) => onUpdate({ postal_address_line_2: e.target.value })} />
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label className={LABEL_CLS}>Town / City</label>
<input className={INPUT_CLS} placeholder="e.g. Pretoria" value={d.postal_town_city} onChange={(e) => onUpdate({ postal_town_city: e.target.value })} />
</div>
<div>
<label className={LABEL_CLS}>Province</label>
<ProvinceSelect value={d.postal_province} onChange={(v) => onUpdate({ postal_province: v })} />
</div>
<div>
<label className={LABEL_CLS}>Postal code</label>
<input className={INPUT_CLS} placeholder="e.g. 0001" value={d.postal_postal_code} onChange={(e) => onUpdate({ postal_postal_code: e.target.value })} />
</div>
</div>
</div>
{/* Contact */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-1.5">
<label className={LABEL_CLS}>Cell phone</label>
<input type="tel" className={INPUT_CLS} placeholder="e.g. 0821234567" value={d.cell_phone} onChange={(e) => onUpdate({ cell_phone: e.target.value })} />
</div>
<div className="space-y-1.5">
<label className={LABEL_CLS}>Email address</label>
<input type="email" className={INPUT_CLS} placeholder="unique@email.com" value={d.email} onChange={(e) => onUpdate({ email: e.target.value })} />
</div>
</div>
{/* Section 69 */}
<div className="border-t border-border pt-5">
<p className="text-sm font-semibold text-fg mb-4">Section 69 — Companies Act 71 of 2008</p>
<Section69Questions section69={d.section69} onUpdate={updateS69} />
</div>
{/* ID document upload */}
<div className="border-t border-border pt-5">
<label className="text-sm font-semibold text-fg mb-1 block">
ID document <span className="text-muted font-normal">(optional — PDF / JPG / PNG, ≤ 10 MB)</span>
</label>
<p className="text-xs text-muted mb-2">
File will be saved as <span className="font-mono">ID - {d.first_names || "FirstName"} {d.surname || "Surname"}.&lt;ext&gt;</span>.
</p>
<input
type="file"
accept="application/pdf,image/jpeg,image/png"
onChange={(e) => onUpdate({ id_file: e.target.files?.[0] || null } as Partial<DirectorInput>)}
className="block w-full text-sm text-fg file:mr-3 file:py-1.5 file:px-3 file:rounded file:border file:border-border file:bg-surface-warm file:text-fg file:text-xs file:font-semibold file:cursor-pointer hover:file:bg-accent/10"
/>
{(d as DirectorInput & { id_file?: File | null }).id_file && (
<p className="text-xs text-success mt-1">
✓ Selected: {(d as DirectorInput & { id_file?: File | null }).id_file?.name}
</p>
)}
</div>
</div>
</div>
);
}