Add onboarding form fields to companies + directors (visual + editable)

PB: 15 fields on ysterbaard_companies (financial_year_end_month,
authorised_shares, structured physical/postal address, company_cell_phone,
public_officer_name, company_secretary_name), 11 fields on
ysterbaard_directors (date_of_birth, structured residential/postal address).

App: types widened in lib/types.ts. EditEntityModal rebuilt with full
address sections (line 1, line 2, town/city, SA province dropdown,
postal code), DOB, company-level fields, financial year end month,
authorised shares, public officer, company secretary.

View pages: structured address blocks now show labeled fields
(line 1, line 2, town/city, province, postal code) on both
/companies/[id] and /directors/[id].
This commit is contained in:
Ysterbaard 2026-08-18 12:25:06 +02:00
parent 4066c1c43c
commit 9bd35898a2
4 changed files with 577 additions and 6 deletions

View file

@ -250,17 +250,98 @@ export default async function CompanyDetailPage({ params }: Props) {
<section data-od-id="company-addresses" className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-surface border border-border rounded-lg p-6">
<h2 className="text-lg font-semibold text-fg mb-3">Physical address</h2>
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed">{text(company.physical_address)}</p>
{company.physical_address ? (
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed">{text(company.physical_address)}</p>
) : null}
<div className="mt-3 grid grid-cols-1 gap-px bg-border border border-border-soft rounded-md overflow-hidden">
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 1</p>
<p className="text-sm text-fg">{text(company.physical_address_line_1)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 2</p>
<p className="text-sm text-fg">{text(company.physical_address_line_2)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Town / city</p>
<p className="text-sm text-fg">{text(company.physical_town_city)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Province</p>
<p className="text-sm text-fg">{text(company.physical_province)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Postal code</p>
<p className="text-sm text-fg font-mono">{text(company.physical_postal_code)}</p>
</div>
</div>
</div>
<div className="bg-surface border border-border rounded-lg p-6">
<h2 className="text-lg font-semibold text-fg mb-3">Postal address</h2>
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed">{text(company.postal_address)}</p>
{company.postal_address ? (
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed">{text(company.postal_address)}</p>
) : null}
<div className="mt-3 grid grid-cols-1 gap-px bg-border border border-border-soft rounded-md overflow-hidden">
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 1</p>
<p className="text-sm text-fg">{text(company.postal_address_line_1)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 2</p>
<p className="text-sm text-fg">{text(company.postal_address_line_2)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Town / city</p>
<p className="text-sm text-fg">{text(company.postal_town_city)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Province</p>
<p className="text-sm text-fg">{text(company.postal_province)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Postal code</p>
<p className="text-sm text-fg font-mono">{text(company.postal_postal_code)}</p>
</div>
</div>
{company.company_email ? (
<div className="mt-3 pt-3 border-t border-border-soft">
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">Email</p>
<a href={`mailto:${company.company_email}`} className="text-sm text-accent hover:underline">{text(company.company_email)}</a>
</div>
) : null}
{company.company_cell_phone ? (
<div className="mt-3 pt-3 border-t border-border-soft">
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">Cell phone</p>
<p className="text-sm text-fg-2">{text(company.company_cell_phone)}</p>
</div>
) : null}
</div>
</section>
{/* Company details (added 2026-08-18) */}
<section data-od-id="company-details" className="bg-surface border border-border rounded-lg p-6 md:p-8">
<h2 className="text-xl font-semibold text-fg mb-4">Company details</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-px bg-border border border-border rounded-md overflow-hidden">
<div className="bg-surface p-4">
<p className="text-xs text-muted font-semibold mb-1">Public officer</p>
<p className="text-sm text-fg">{text(company.public_officer_name)}</p>
</div>
<div className="bg-surface p-4">
<p className="text-xs text-muted font-semibold mb-1">Company secretary</p>
<p className="text-sm text-fg">{text(company.company_secretary_name)}</p>
</div>
<div className="bg-surface p-4">
<p className="text-xs text-muted font-semibold mb-1">Financial year end</p>
<p className="text-sm text-fg">
{company.financial_year_end_month
? new Date(2000, company.financial_year_end_month - 1, 1).toLocaleString("en", { month: "long" })
: "—"}
</p>
</div>
<div className="bg-surface p-4">
<p className="text-xs text-muted font-semibold mb-1">Authorised shares</p>
<p className="text-sm text-fg font-mono">{company.authorised_shares ? company.authorised_shares.toLocaleString() : "—"}</p>
</div>
</div>
</section>

View file

@ -142,6 +142,15 @@ export default async function DirectorDetailPage({ params }: Props) {
) : null}
</div>
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">
Date of birth
</p>
<p className="text-base text-fg">
{formatDate(director.date_of_birth)}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">
Telephone
@ -151,6 +160,19 @@ export default async function DirectorDetailPage({ params }: Props) {
</p>
</div>
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">
Email
</p>
<p className="text-base text-fg">
{director.email ? (
<a href={`mailto:${director.email}`} className="text-accent hover:underline">{text(director.email)}</a>
) : (
<span className="text-muted">—</span>
)}
</p>
</div>
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-1">
First seen
@ -170,6 +192,70 @@ export default async function DirectorDetailPage({ params }: Props) {
</div>
</div>
{/* Addresses (added 2026-08-18) */}
<div className="border-t border-border-soft pt-6 flex flex-col gap-4">
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-2">
Residential address
</p>
{director.residential_address ? (
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed mb-2">{text(director.residential_address)}</p>
) : null}
<div className="grid grid-cols-1 gap-px bg-border border border-border-soft rounded-md overflow-hidden">
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 1</p>
<p className="text-sm text-fg">{text(director.residential_address_line_1)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 2</p>
<p className="text-sm text-fg">{text(director.residential_address_line_2)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Town / city</p>
<p className="text-sm text-fg">{text(director.residential_town_city)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Province</p>
<p className="text-sm text-fg">{text(director.residential_province)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Postal code</p>
<p className="text-sm text-fg font-mono">{text(director.residential_postal_code)}</p>
</div>
</div>
</div>
<div>
<p className="text-xs uppercase tracking-[0.08em] text-muted font-semibold mb-2">
Postal address
</p>
{director.postal_address ? (
<p className="text-sm text-fg-2 whitespace-pre-line break-words leading-relaxed mb-2">{text(director.postal_address)}</p>
) : null}
<div className="grid grid-cols-1 gap-px bg-border border border-border-soft rounded-md overflow-hidden">
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 1</p>
<p className="text-sm text-fg">{text(director.postal_address_line_1)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Address line 2</p>
<p className="text-sm text-fg">{text(director.postal_address_line_2)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Town / city</p>
<p className="text-sm text-fg">{text(director.postal_town_city)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Province</p>
<p className="text-sm text-fg">{text(director.postal_province)}</p>
</div>
<div className="bg-surface p-3">
<p className="text-xs text-muted font-semibold mb-0.5">Postal code</p>
<p className="text-sm text-fg font-mono">{text(director.postal_postal_code)}</p>
</div>
</div>
</div>
</div>
<div className="border-t border-border-soft pt-6 grid grid-cols-3 gap-3">
<div>
<p className="text-3xl font-bold text-fg tabular-nums leading-none">

View file

@ -19,6 +19,19 @@ interface Director {
postal_address?: string | null;
director_type?: string | null;
notes?: string | null;
// ─── Structured address fields (added 2026-08-18) ───
residential_address?: string | null;
residential_address_line_1?: string | null;
residential_address_line_2?: string | null;
residential_town_city?: string | null;
residential_province?: string | null;
residential_postal_code?: string | null;
postal_address_line_1?: string | null;
postal_address_line_2?: string | null;
postal_town_city?: string | null;
postal_province?: string | null;
postal_postal_code?: string | null;
date_of_birth?: string | null;
}
interface Company {
@ -40,6 +53,23 @@ interface Company {
sars_vat_returns_period?: number | null;
sars_paye_return_period?: number | null;
groups?: string[] | null;
// ─── Structured address fields (added 2026-08-18) ───
physical_address_line_1?: string | null;
physical_address_line_2?: string | null;
physical_town_city?: string | null;
physical_province?: string | null;
physical_postal_code?: string | null;
postal_address_line_1?: string | null;
postal_address_line_2?: string | null;
postal_town_city?: string | null;
postal_province?: string | null;
postal_postal_code?: string | null;
// ─── Company-level fields (added 2026-08-18) ───
financial_year_end_month?: number | null;
authorised_shares?: number | null;
company_cell_phone?: string | null;
public_officer_name?: string | null;
company_secretary_name?: string | null;
}
interface GroupOption {
@ -48,6 +78,18 @@ interface GroupOption {
description?: string | null;
}
const SA_PROVINCES = [
"Eastern Cape",
"Free State",
"Gauteng",
"KwaZulu-Natal",
"Limpopo",
"Mpumalanga",
"Northern Cape",
"North West",
"Western Cape",
];
interface Props {
entity: "director" | "company";
/** Original record data for pre-filling the form */
@ -72,6 +114,18 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
const [postalAddress, setPostalAddress] = useState("");
const [directorType, setDirectorType] = useState("");
const [notes, setNotes] = useState("");
// Director structured address fields (added 2026-08-18)
const [dateOfBirth, setDateOfBirth] = useState("");
const [resAddr1, setResAddr1] = useState("");
const [resAddr2, setResAddr2] = useState("");
const [resTown, setResTown] = useState("");
const [resProvince, setResProvince] = useState("");
const [resPostalCode, setResPostalCode] = useState("");
const [postAddr1, setPostAddr1] = useState("");
const [postAddr2, setPostAddr2] = useState("");
const [postTown, setPostTown] = useState("");
const [postProvince, setPostProvince] = useState("");
const [postPostalCode, setPostPostalCode] = useState("");
// Company fields
const [enterpriseNo, setEnterpriseNo] = useState("");
@ -82,6 +136,23 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
const [postalAddressCo, setPostalAddressCo] = useState("");
const [companyEmail, setCompanyEmail] = useState("");
const [notesCo, setNotesCo] = useState("");
// Company structured address fields (added 2026-08-18)
const [physAddr1, setPhysAddr1] = useState("");
const [physAddr2, setPhysAddr2] = useState("");
const [physTown, setPhysTown] = useState("");
const [physProvince, setPhysProvince] = useState("");
const [physPostalCode, setPhysPostalCode] = useState("");
const [postCoAddr1, setPostCoAddr1] = useState("");
const [postCoAddr2, setPostCoAddr2] = useState("");
const [postCoTown, setPostCoTown] = useState("");
const [postCoProvince, setPostCoProvince] = useState("");
const [postCoPostalCode, setPostCoPostalCode] = useState("");
// Company-level fields (added 2026-08-18)
const [financialYearEndMonth, setFinancialYearEndMonth] = useState("");
const [authorisedShares, setAuthorisedShares] = useState("");
const [companyCellPhone, setCompanyCellPhone] = useState("");
const [publicOfficerName, setPublicOfficerName] = useState("");
const [companySecretaryName, setCompanySecretaryName] = useState("");
// SARS fields
const [sarsLoginUsername, setSarsLoginUsername] = useState("");
const [sarsLoginPassword, setSarsLoginPassword] = useState("");
@ -110,6 +181,17 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
setPostalAddress(d.postal_address ?? "");
setDirectorType(d.director_type ?? "");
setNotes(d.notes ?? "");
setDateOfBirth(d.date_of_birth ?? "");
setResAddr1(d.residential_address_line_1 ?? "");
setResAddr2(d.residential_address_line_2 ?? "");
setResTown(d.residential_town_city ?? "");
setResProvince(d.residential_province ?? "");
setResPostalCode(d.residential_postal_code ?? "");
setPostAddr1(d.postal_address_line_1 ?? "");
setPostAddr2(d.postal_address_line_2 ?? "");
setPostTown(d.postal_town_city ?? "");
setPostProvince(d.postal_province ?? "");
setPostPostalCode(d.postal_postal_code ?? "");
} else {
const c = record as Company;
setEnterpriseNo(c.enterprise_no ?? "");
@ -120,6 +202,21 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
setPostalAddressCo(c.postal_address ?? "");
setCompanyEmail(c.company_email ?? "");
setNotesCo(c.notes ?? "");
setPhysAddr1(c.physical_address_line_1 ?? "");
setPhysAddr2(c.physical_address_line_2 ?? "");
setPhysTown(c.physical_town_city ?? "");
setPhysProvince(c.physical_province ?? "");
setPhysPostalCode(c.physical_postal_code ?? "");
setPostCoAddr1(c.postal_address_line_1 ?? "");
setPostCoAddr2(c.postal_address_line_2 ?? "");
setPostCoTown(c.postal_town_city ?? "");
setPostCoProvince(c.postal_province ?? "");
setPostCoPostalCode(c.postal_postal_code ?? "");
setFinancialYearEndMonth(c.financial_year_end_month != null ? String(c.financial_year_end_month) : "");
setAuthorisedShares(c.authorised_shares != null ? String(c.authorised_shares) : "");
setCompanyCellPhone(c.company_cell_phone ?? "");
setPublicOfficerName(c.public_officer_name ?? "");
setCompanySecretaryName(c.company_secretary_name ?? "");
setSarsLoginUsername(c.sars_login_username ?? "");
setSarsLoginPassword(c.sars_login_password ?? "");
setSarsPayeNo(c.sars_paye_no ?? "");
@ -167,6 +264,18 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
if (postalAddress.trim()) fields.postal_address = postalAddress.trim();
if (directorType.trim()) fields.director_type = directorType.trim();
if (notes.trim()) fields.notes = notes.trim();
// Structured address fields (added 2026-08-18)
if (dateOfBirth.trim()) fields.date_of_birth = dateOfBirth.trim();
if (resAddr1.trim()) fields.residential_address_line_1 = resAddr1.trim();
if (resAddr2.trim()) fields.residential_address_line_2 = resAddr2.trim();
if (resTown.trim()) fields.residential_town_city = resTown.trim();
if (resProvince.trim()) fields.residential_province = resProvince.trim();
if (resPostalCode.trim()) fields.residential_postal_code = resPostalCode.trim();
if (postAddr1.trim()) fields.postal_address_line_1 = postAddr1.trim();
if (postAddr2.trim()) fields.postal_address_line_2 = postAddr2.trim();
if (postTown.trim()) fields.postal_town_city = postTown.trim();
if (postProvince.trim()) fields.postal_province = postProvince.trim();
if (postPostalCode.trim()) fields.postal_postal_code = postPostalCode.trim();
} else {
if (!enterpriseNo.trim() || !enterpriseName.trim()) {
setError("Enterprise number and name are required.");
@ -181,12 +290,33 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
if (postalAddressCo.trim()) fields.postal_address = postalAddressCo.trim();
if (companyEmail.trim()) fields.company_email = companyEmail.trim();
if (notesCo.trim()) fields.notes = notesCo.trim();
// Structured address fields (added 2026-08-18)
if (physAddr1.trim()) fields.physical_address_line_1 = physAddr1.trim();
if (physAddr2.trim()) fields.physical_address_line_2 = physAddr2.trim();
if (physTown.trim()) fields.physical_town_city = physTown.trim();
if (physProvince.trim()) fields.physical_province = physProvince.trim();
if (physPostalCode.trim()) fields.physical_postal_code = physPostalCode.trim();
if (postCoAddr1.trim()) fields.postal_address_line_1 = postCoAddr1.trim();
if (postCoAddr2.trim()) fields.postal_address_line_2 = postCoAddr2.trim();
if (postCoTown.trim()) fields.postal_town_city = postCoTown.trim();
if (postCoProvince.trim()) fields.postal_province = postCoProvince.trim();
if (postCoPostalCode.trim()) fields.postal_postal_code = postCoPostalCode.trim();
// Company-level fields (added 2026-08-18)
if (financialYearEndMonth !== "" || (record as Company).financial_year_end_month != null) {
fields.financial_year_end_month = financialYearEndMonth !== "" ? Number(financialYearEndMonth) : 0;
}
if (authorisedShares !== "" || (record as Company).authorised_shares != null) {
fields.authorised_shares = authorisedShares !== "" ? Number(authorisedShares) : 0;
}
if (companyCellPhone.trim()) fields.company_cell_phone = companyCellPhone.trim();
if (publicOfficerName.trim()) fields.public_officer_name = publicOfficerName.trim();
if (companySecretaryName.trim()) fields.company_secretary_name = companySecretaryName.trim();
if (sarsLoginUsername.trim()) fields.sars_login_username = sarsLoginUsername.trim();
if (sarsLoginPassword.trim()) fields.sars_login_password = sarsLoginPassword.trim();
if (sarsPayeNo.trim()) fields.sars_paye_no = sarsPayeNo.trim();
if (sarsVatNo.trim()) fields.sars_vat_no = sarsVatNo.trim();
if (sarsTaxNo.trim()) fields.sars_tax_number = sarsTaxNo.trim();
// Period fields: store the selected numeric value as string (PocketBase number field accepts string->number conversion)
// Period fields
const vatPeriod = sarsVatReturnsPeriod;
const payePeriod = sarsPayeReturnPeriod;
if (vatPeriod !== "" || (record as Company).sars_vat_returns_period != null) {
@ -215,6 +345,9 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
}
};
const monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"];
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
@ -275,6 +408,13 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
maxLength={13} />
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-dob">Date of birth <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-dob" type="date" value={dateOfBirth}
onChange={(e) => setDateOfBirth(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-tel">Telephone <span className="text-xs text-muted font-normal">(optional)</span></label>
@ -291,12 +431,100 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-postal">Postal address <span className="text-xs text-muted font-normal">(optional)</span></label>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-postal">Postal address <span className="text-xs text-muted font-normal">(optional, free-text)</span></label>
<textarea id="edit-postal" value={postalAddress} rows={2}
onChange={(e) => setPostalAddress(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg resize-none focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
{/* Structured residential address (added 2026-08-18) */}
<div className="border-t border-border pt-4 mt-2">
<p className="text-sm font-semibold text-fg mb-3">Residential address <span className="text-xs text-muted font-normal">(structured)</span></p>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-res1">Address line 1 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-res1" type="text" value={resAddr1}
onChange={(e) => setResAddr1(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-res2">Address line 2 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-res2" type="text" value={resAddr2}
onChange={(e) => setResAddr2(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-restown">Town / city <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-restown" type="text" value={resTown}
onChange={(e) => setResTown(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-resprov">Province <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-resprov" value={resProvince}
onChange={(e) => setResProvince(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
{SA_PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-reszip">Postal code <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-reszip" type="text" value={resPostalCode}
onChange={(e) => setResPostalCode(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
</div>
</div>
{/* Structured postal address (added 2026-08-18) */}
<div className="border-t border-border pt-4 mt-2">
<p className="text-sm font-semibold text-fg mb-3">Postal address <span className="text-xs text-muted font-normal">(structured)</span></p>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-post1">Address line 1 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-post1" type="text" value={postAddr1}
onChange={(e) => setPostAddr1(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-post2">Address line 2 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-post2" type="text" value={postAddr2}
onChange={(e) => setPostAddr2(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-posttown">Town / city <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-posttown" type="text" value={postTown}
onChange={(e) => setPostTown(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postprov">Province <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-postprov" value={postProvince}
onChange={(e) => setPostProvince(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
{SA_PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postzip">Postal code <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-postzip" type="text" value={postPostalCode}
onChange={(e) => setPostPostalCode(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-type">Director type <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-type" type="text" value={directorType}
@ -347,19 +575,107 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-phys">Physical address <span className="text-xs text-muted font-normal">(optional)</span></label>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-phys">Physical address <span className="text-xs text-muted font-normal">(optional, free-text)</span></label>
<textarea id="edit-phys" value={physicalAddress} rows={2}
onChange={(e) => setPhysicalAddress(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg resize-none focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-postalco">Postal address <span className="text-xs text-muted font-normal">(optional)</span></label>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-postalco">Postal address <span className="text-xs text-muted font-normal">(optional, free-text)</span></label>
<textarea id="edit-postalco" value={postalAddressCo} rows={2}
onChange={(e) => setPostalAddressCo(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg resize-none focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
{/* Structured physical address (added 2026-08-18) */}
<div className="border-t border-border pt-4 mt-2">
<p className="text-sm font-semibold text-fg mb-3">Physical address <span className="text-xs text-muted font-normal">(structured)</span></p>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-phys1">Address line 1 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-phys1" type="text" value={physAddr1}
onChange={(e) => setPhysAddr1(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-phys2">Address line 2 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-phys2" type="text" value={physAddr2}
onChange={(e) => setPhysAddr2(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-phystown">Town / city <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-phystown" type="text" value={physTown}
onChange={(e) => setPhysTown(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-physprov">Province <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-physprov" value={physProvince}
onChange={(e) => setPhysProvince(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
{SA_PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-physzip">Postal code <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-physzip" type="text" value={physPostalCode}
onChange={(e) => setPhysPostalCode(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
</div>
</div>
{/* Structured postal address (added 2026-08-18) */}
<div className="border-t border-border pt-4 mt-2">
<p className="text-sm font-semibold text-fg mb-3">Postal address <span className="text-xs text-muted font-normal">(structured)</span></p>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postco1">Address line 1 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-postco1" type="text" value={postCoAddr1}
onChange={(e) => setPostCoAddr1(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postco2">Address line 2 <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-postco2" type="text" value={postCoAddr2}
onChange={(e) => setPostCoAddr2(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postcotown">Town / city <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-postcotown" type="text" value={postCoTown}
onChange={(e) => setPostCoTown(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postcoprov">Province <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-postcoprov" value={postCoProvince}
onChange={(e) => setPostCoProvince(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
{SA_PROVINCES.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-postcozip">Postal code <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-postcozip" type="text" value={postCoPostalCode}
onChange={(e) => setPostCoPostalCode(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg font-mono focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-emailco">Company email <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-emailco" type="email" value={companyEmail}
@ -368,6 +684,56 @@ export default function EditEntityModal({ entity, record, open, onClose, onAfter
placeholder="company@example.com" />
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-cellco">Company cell phone <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-cellco" type="tel" value={companyCellPhone}
onChange={(e) => setCompanyCellPhone(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
{/* Company-level fields (added 2026-08-18) */}
<div className="border-t border-border pt-4 mt-2">
<p className="text-sm font-semibold text-fg mb-3">Company details</p>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-fye">Financial year end <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-fye" value={financialYearEndMonth}
onChange={(e) => setFinancialYearEndMonth(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
{monthNames.map((m, i) => (
<option key={i + 1} value={String(i + 1)}>{m}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-shares">Authorised shares <span className="text-xs text-muted font-normal">(optional)</span></label>
<select id="edit-shares" value={authorisedShares}
onChange={(e) => setAuthorisedShares(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent">
<option value="">— select —</option>
<option value="100">100</option>
<option value="1000">1,000</option>
<option value="10000">10,000</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-officer">Public officer <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-officer" type="text" value={publicOfficerName}
onChange={(e) => setPublicOfficerName(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
<div>
<label className="block text-sm font-medium text-fg-2 mb-1.5" htmlFor="edit-secretary">Company secretary <span className="text-xs text-muted font-normal">(optional)</span></label>
<input id="edit-secretary" type="text" value={companySecretaryName}
onChange={(e) => setCompanySecretaryName(e.target.value)}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent" />
</div>
</div>
</div>
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="edit-notesco">Notes <span className="text-xs text-muted font-normal">(optional)</span></label>
<textarea id="edit-notesco" value={notesCo} rows={2}

View file

@ -26,6 +26,22 @@ export interface YsterbaardDirector {
first_seen: string | null; // ISO date
last_checked: string | null; // ISO date
notes: string | null;
// ─── Structured address fields (added 2026-08-18 from onboarding form) ───
/** Flat address — back-compat. Use line_1 + line_2 + town_city + province + postal_code. */
residential_address: string | null;
residential_address_line_1: string | null;
residential_address_line_2: string | null;
residential_town_city: string | null;
residential_province: string | null;
residential_postal_code: string | null;
postal_address: string | null;
postal_address_line_1: string | null;
postal_address_line_2: string | null;
postal_town_city: string | null;
postal_province: string | null;
postal_postal_code: string | null;
/** ISO date string YYYY-MM-DD. */
date_of_birth: string | null;
created: string;
updated: string;
}
@ -77,6 +93,28 @@ export interface YsterbaardCompany {
physical_address_normalized: string | null;
postal_address_normalized: string | null;
company_email: string | null;
// ─── Structured address fields (added 2026-08-18 from onboarding form) ───
physical_address_line_1: string | null;
physical_address_line_2: string | null;
physical_town_city: string | null;
physical_province: string | null;
physical_postal_code: string | null;
postal_address_line_1: string | null;
postal_address_line_2: string | null;
postal_town_city: string | null;
postal_province: string | null;
postal_postal_code: string | null;
// ─── Company-level fields (added 2026-08-18 from onboarding form) ───
/** 1-12, month when the company financial year ends. */
financial_year_end_month: number | null;
/** Total number of authorised shares (e.g. 100 or 1000). */
authorised_shares: number | null;
/** Company cell phone (different from director mobile). */
company_cell_phone: string | null;
/** SARS "Public Officer" — typically the accountant / responsible person. */
public_officer_name: string | null;
/** Company secretary name. */
company_secretary_name: string | null;
sars_tax_number: string | null;
uif_registration_number: string | null;
compensation_fund_registration_number: string | null;