"use client"; // components/EditEntityModal.tsx // Pre-filled edit form for existing director or company records. // Opens from the Edit button on a table row or from the detail page. // PUT/PATCH via /api/pb/records/:id, invalidates list + detail cache. import { useState, useEffect } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { pbPatch, pbList } from "@/lib/api"; import { dateInputValue } from "@/lib/format"; interface Director { id: string; surname?: string | null; first_names?: string | null; id_no?: string | null; /** SARS income-tax reference number (number; added 2026-09-16). */ tax_reference_no?: number | null; telephone?: string | null; email?: string | null; 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 { id: string; enterprise_no?: string | null; enterprise_name?: string | null; enterprise_type?: string | null; registration_date?: string | null; physical_address?: string | null; postal_address?: string | null; company_email?: string | null; director?: string | null; notes?: string | null; sars_login_username?: string | null; sars_login_password?: string | null; sars_paye_no?: string | null; sars_vat_no?: string | null; sars_tax_number?: string | null; sars_vat_returns_period?: number | null; sars_paye_return_period?: number | null; // ─── SARS login behaviour (added 2026-09-11) ─── otp?: boolean | 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 { id: string; name: string; 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 */ record: Director | Company; open: boolean; onClose: () => void; /** Called after a successful save */ onAfterSave?: () => void; } export default function EditEntityModal({ entity, record, open, onClose, onAfterSave }: Props) { const qc = useQueryClient(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Director fields const [surname, setSurname] = useState(""); const [firstNames, setFirstNames] = useState(""); const [idNo, setIdNo] = useState(""); // SARS tax reference — number, optional. Stored as a string in the input // so we can render empty cleanly; coerced to a number on save (added 2026-09-16). const [taxReferenceNo, setTaxReferenceNo] = useState(""); const [telephone, setTelephone] = useState(""); const [email, setEmail] = useState(""); const [postalAddress, setPostalAddress] = useState(""); const [residentialAddress, setResidentialAddress] = useState(""); // flat legacy — added 2026-08-20 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(""); const [enterpriseName, setEnterpriseName] = useState(""); const [enterpriseType, setEnterpriseType] = useState(""); const [registrationDate, setRegistrationDate] = useState(""); const [physicalAddress, setPhysicalAddress] = useState(""); 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(""); const [sarsPayeNo, setSarsPayeNo] = useState(""); const [sarsVatNo, setSarsVatNo] = useState(""); const [sarsTaxNo, setSarsTaxNo] = useState(""); const [sarsVatReturnsPeriod, setSarsVatReturnsPeriod] = useState(""); const [sarsPayeReturnPeriod, setSarsPayeReturnPeriod] = useState(""); // OTP-required flag — when ON, the SARS login script expects an // SMS one-time PIN after the password and waits for it (added 2026-09-11). const [otpRequired, setOtpRequired] = useState(false); // Group fields const [selectedGroups, setSelectedGroups] = useState([]); const [groupOptions, setGroupOptions] = useState([]); // Populate form when modal opens or record changes useEffect(() => { if (!open) return; setError(null); if (entity === "director") { const d = record as Director; setSurname(d.surname ?? ""); setFirstNames(d.first_names ?? ""); setIdNo(d.id_no ?? ""); setTaxReferenceNo(d.tax_reference_no != null ? String(d.tax_reference_no) : ""); setTelephone(d.telephone ?? ""); setEmail(d.email ?? ""); setPostalAddress(d.postal_address ?? ""); setResidentialAddress(d.residential_address ?? ""); setDirectorType(d.director_type ?? ""); setNotes(d.notes ?? ""); setDateOfBirth(dateInputValue(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 ?? ""); setEnterpriseName(c.enterprise_name ?? ""); setEnterpriseType(c.enterprise_type ?? ""); setRegistrationDate(dateInputValue(c.registration_date)); setPhysicalAddress(c.physical_address ?? ""); 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 ?? ""); setSarsVatNo(c.sars_vat_no ?? ""); setSarsTaxNo(c.sars_tax_number ?? ""); setSarsVatReturnsPeriod(c.sars_vat_returns_period != null ? String(c.sars_vat_returns_period) : ""); setSarsPayeReturnPeriod(c.sars_paye_return_period != null ? String(c.sars_paye_return_period) : ""); setOtpRequired(Boolean(c.otp)); setSelectedGroups(c.groups ?? []); } }, [open, entity, record]); // Fetch group options when modal opens (company entity only) useEffect(() => { if (!open || entity !== "company") return; pbList({ collection: "groups", perPage: 200, sort: "name" }) .then((res) => setGroupOptions(res.items)) .catch(() => {}); }, [open, entity]); if (!open) return null; const isDirector = entity === "director"; const title = isDirector ? `Edit director — ${(record as Director).surname}, ${(record as Director).first_names}` : `Edit company — ${(record as Company).enterprise_name}`; const collection = isDirector ? "ysterbaard_directors" : "ysterbaard_companies"; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); setLoading(true); try { const fields: Record = {}; // Always send every field — empty string clears the value in PB. // The previous "if (value.trim())" guard LOST the user's intent to // clear a field: empty fields were silently skipped, so a delete in // the UI never landed in PB. We now send the literal value the user // left in the field, including "" to clear, and only validate // required fields up-front. if (isDirector) { if (!surname.trim() || !firstNames.trim()) { setError("Surname and first names are required."); setLoading(false); return; } fields.surname = surname.trim(); fields.first_names = firstNames.trim(); fields.id_no = idNo.trim(); // tax_reference_no: PB `number` field. Empty string means clear it // (send 0 — PB coerces to null on the way out for `required:false` // number fields, matching how we already handle financial_year_end // month and authorised_shares below). Added 2026-09-16. fields.tax_reference_no = taxReferenceNo.trim() !== "" ? Number(taxReferenceNo) : 0; fields.telephone = telephone.trim(); fields.email = email.trim(); fields.postal_address = postalAddress.trim(); fields.residential_address = residentialAddress.trim(); fields.director_type = directorType.trim(); fields.notes = notes.trim(); // Structured address fields fields.date_of_birth = dateOfBirth.trim(); fields.residential_address_line_1 = resAddr1.trim(); fields.residential_address_line_2 = resAddr2.trim(); fields.residential_town_city = resTown.trim(); fields.residential_province = resProvince.trim(); fields.residential_postal_code = resPostalCode.trim(); fields.postal_address_line_1 = postAddr1.trim(); fields.postal_address_line_2 = postAddr2.trim(); fields.postal_town_city = postTown.trim(); fields.postal_province = postProvince.trim(); fields.postal_postal_code = postPostalCode.trim(); } else { if (!enterpriseNo.trim() || !enterpriseName.trim()) { setError("Enterprise number and name are required."); setLoading(false); return; } fields.enterprise_no = enterpriseNo.trim().toUpperCase(); fields.enterprise_name = enterpriseName.trim().toUpperCase(); fields.enterprise_type = enterpriseType.trim(); fields.registration_date = registrationDate.trim(); fields.physical_address = physicalAddress.trim(); fields.postal_address = postalAddressCo.trim(); fields.company_email = companyEmail.trim(); fields.notes = notesCo.trim(); // Structured address fields fields.physical_address_line_1 = physAddr1.trim(); fields.physical_address_line_2 = physAddr2.trim(); fields.physical_town_city = physTown.trim(); fields.physical_province = physProvince.trim(); fields.physical_postal_code = physPostalCode.trim(); fields.postal_address_line_1 = postCoAddr1.trim(); fields.postal_address_line_2 = postCoAddr2.trim(); fields.postal_town_city = postCoTown.trim(); fields.postal_province = postCoProvince.trim(); fields.postal_postal_code = postCoPostalCode.trim(); // Company-level fields fields.financial_year_end_month = financialYearEndMonth !== "" ? Number(financialYearEndMonth) : 0; fields.authorised_shares = authorisedShares !== "" ? Number(authorisedShares) : 0; fields.company_cell_phone = companyCellPhone.trim(); fields.public_officer_name = publicOfficerName.trim(); fields.company_secretary_name = companySecretaryName.trim(); fields.sars_login_username = sarsLoginUsername.trim(); fields.sars_login_password = sarsLoginPassword.trim(); fields.sars_paye_no = sarsPayeNo.trim(); fields.sars_vat_no = sarsVatNo.trim(); fields.sars_tax_number = sarsTaxNo.trim(); fields.sars_vat_returns_period = sarsVatReturnsPeriod !== "" ? Number(sarsVatReturnsPeriod) : 0; fields.sars_paye_return_period = sarsPayeReturnPeriod !== "" ? Number(sarsPayeReturnPeriod) : 0; fields.otp = otpRequired; // Groups — always send current selection (empty array is valid to clear) fields.groups = selectedGroups; } // Pre-check for ID collisions on directors before PATCH (added 2026-08-20). // PB's UNIQUE index on id_no_masked rejects the PATCH with a 502 if // the typed id_no masks to a value already on another director. // Detect this client-side and offer to open the existing record. if (isDirector && fields.id_no && (fields.id_no as string).length >= 12) { const typedId = fields.id_no as string; const masked = typedId.slice(0, 6) + "XXXX" + typedId.slice(10, 12) + "X"; try { const existing = await pbList<{ id: string; surname: string; first_names: string; id_no_masked: string }>({ collection: "ysterbaard_directors", perPage: 5, filter: `id_no_masked="${masked}" || id_no="${typedId}"`, }); const conflict = existing.items.find((d) => d.id !== record.id); if (conflict) { setLoading(false); setError( `A director with this ID already exists: ${conflict.surname}, ${conflict.first_names}. ` + `Open their record instead?` ); return; } } catch { // If pre-check fails, let the PATCH proceed and surface the 502. } } await pbPatch({ collection, id: record.id, fields }); // Invalidate both list and detail caches // Invalidate every key the detail page or list pages might be holding. // The list pages use "companies" / "directors" keys (not "pb-list"), // and the detail page uses "director" / "company" keys. await qc.invalidateQueries({ queryKey: ["pb-list", collection] }); await qc.invalidateQueries({ queryKey: ["companies"] }); await qc.invalidateQueries({ queryKey: ["directors"] }); await qc.invalidateQueries({ queryKey: ["director", record.id] }); await qc.invalidateQueries({ queryKey: ["company", record.id] }); onAfterSave?.(); onClose(); } catch (err) { setError(err instanceof Error ? err.message : "Something went wrong. Please try again."); } finally { setLoading(false); } }; const monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; return (
{/* Header */}

Edit {isDirector ? "director" : "company"}

{title}

{/* Form */}
{error && (
{error}
)} {isDirector ? ( <>
setSurname(e.target.value.toUpperCase())} 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" required />
setFirstNames(e.target.value.toUpperCase())} 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" required />
setIdNo(e.target.value.replace(/\D/g, "").slice(0, 13))} 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" maxLength={13} />
setTaxReferenceNo(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" placeholder="e.g. 9004615283" />
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" />
setTelephone(e.target.value.replace(/\D/g, "").slice(0, 11))} inputMode="numeric" 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" />
setEmail(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" />