2026-07-30 14:38:05 +00:00
"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 , useRef } from "react" ;
import { useQueryClient } from "@tanstack/react-query" ;
import { pbPatch } from "@/lib/api" ;
interface Director {
id : string ;
surname? : string | null ;
first_names? : string | null ;
id_no? : string | null ;
telephone? : string | null ;
email? : string | null ;
postal_address? : string | null ;
director_type? : string | null ;
notes? : 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 ;
director? : string | null ;
notes? : string | null ;
}
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 < string | null > ( null ) ;
const overlayRef = useRef < HTMLDivElement > ( null ) ;
// Director fields
const [ surname , setSurname ] = useState ( "" ) ;
const [ firstNames , setFirstNames ] = useState ( "" ) ;
const [ idNo , setIdNo ] = useState ( "" ) ;
const [ telephone , setTelephone ] = useState ( "" ) ;
const [ email , setEmail ] = useState ( "" ) ;
const [ postalAddress , setPostalAddress ] = useState ( "" ) ;
const [ directorType , setDirectorType ] = useState ( "" ) ;
const [ notes , setNotes ] = 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 [ notesCo , setNotesCo ] = 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 ? ? "" ) ;
setTelephone ( d . telephone ? ? "" ) ;
setEmail ( d . email ? ? "" ) ;
setPostalAddress ( d . postal_address ? ? "" ) ;
setDirectorType ( d . director_type ? ? "" ) ;
setNotes ( d . notes ? ? "" ) ;
} else {
const c = record as Company ;
setEnterpriseNo ( c . enterprise_no ? ? "" ) ;
setEnterpriseName ( c . enterprise_name ? ? "" ) ;
setEnterpriseType ( c . enterprise_type ? ? "" ) ;
setRegistrationDate ( c . registration_date ? ? "" ) ;
setPhysicalAddress ( c . physical_address ? ? "" ) ;
setPostalAddressCo ( c . postal_address ? ? "" ) ;
setNotesCo ( c . notes ? ? "" ) ;
}
} , [ open , entity , record ] ) ;
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 handleOverlayClick = ( e : React.MouseEvent ) = > {
if ( e . target === overlayRef . current ) onClose ( ) ;
} ;
const handleSubmit = async ( e : React.FormEvent ) = > {
e . preventDefault ( ) ;
setError ( null ) ;
setLoading ( true ) ;
try {
const fields : Record < string , string > = { } ;
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 ( ) ;
if ( idNo . trim ( ) ) fields . id_no = idNo . trim ( ) ;
if ( telephone . trim ( ) ) fields . telephone = telephone . trim ( ) ;
if ( email . trim ( ) ) fields . email = email . trim ( ) ;
if ( postalAddress . trim ( ) ) fields . postal_address = postalAddress . trim ( ) ;
if ( directorType . trim ( ) ) fields . director_type = directorType . trim ( ) ;
if ( notes . trim ( ) ) fields . notes = notes . 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 ( ) ;
if ( enterpriseType . trim ( ) ) fields . enterprise_type = enterpriseType . trim ( ) ;
if ( registrationDate . trim ( ) ) fields . registration_date = registrationDate . trim ( ) ;
if ( physicalAddress . trim ( ) ) fields . physical_address = physicalAddress . trim ( ) ;
if ( postalAddressCo . trim ( ) ) fields . postal_address = postalAddressCo . trim ( ) ;
if ( notesCo . trim ( ) ) fields . notes = notesCo . trim ( ) ;
}
await pbPatch ( { collection , id : record.id , fields } ) ;
// Invalidate both list and detail caches
await qc . invalidateQueries ( { queryKey : [ "pb-list" , collection ] } ) ;
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 ) ;
}
} ;
return (
< div
ref = { overlayRef }
onClick = { handleOverlayClick }
className = "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
role = "dialog"
aria - modal = "true"
aria - label = { title }
>
< div className = "bg-surface border border-border rounded-xl shadow-2xl w-full max-w-lg flex flex-col max-h-[90vh]" >
{ /* Header */ }
< div className = "px-6 pt-6 pb-4 border-b border-border flex-shrink-0" >
< div className = "flex items-start justify-between" >
< div >
< h2 className = "text-lg font-bold text-fg" > Edit { isDirector ? "director" : "company" } < / h2 >
< p className = "text-sm text-muted mt-0.5" > { title } < / p >
< / div >
< button
onClick = { onClose }
className = "ml-4 text-muted hover:text-fg text-xl leading-none p-1 rounded transition-colors"
aria - label = "Close"
>
×
< / button >
< / div >
< / div >
{ /* Form */ }
< form onSubmit = { handleSubmit } className = "flex-1 overflow-y-auto px-6 py-5 space-y-4" >
{ error && (
< div className = "bg-red-500/10 border border-red-500/30 rounded-lg px-4 py-3 text-sm text-red-400" >
{ error }
< / div >
) }
{ isDirector ? (
< >
< div className = "grid grid-cols-2 gap-4" >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-surname" > Surname < span className = "text-red-400" > * < / span > < / label >
< input id = "edit-surname" type = "text" value = { surname }
onChange = { ( e ) = > 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 / >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-firstnames" > First names < span className = "text-red-400" > * < / span > < / label >
< input id = "edit-firstnames" type = "text" value = { firstNames }
onChange = { ( e ) = > 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 / >
< / div >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-idno" > SA ID number < span className = "text-xs text-muted font-normal" > ( optional ) < / span > < / label >
2026-07-30 15:53:41 +00:00
< input id = "edit-idno" type = "tel" value = { idNo }
2026-07-30 14:38:05 +00:00
onChange = { ( e ) = > 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 } / >
< / 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 >
< input id = "edit-tel" type = "tel" value = { telephone }
onChange = { ( e ) = > setTelephone ( 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-semibold text-fg mb-1.5" htmlFor = "edit-email" > Email < span className = "text-xs text-muted font-normal" > ( optional ) < / span > < / label >
< input id = "edit-email" type = "email" value = { email }
onChange = { ( e ) = > 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" / >
< / div >
< / 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 >
< 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 >
< 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 }
onChange = { ( e ) = > setDirectorType ( 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"
placeholder = "e.g. Individual, Corporate" / >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-notes" > Notes < span className = "text-xs text-muted font-normal" > ( optional ) < / span > < / label >
< textarea id = "edit-notes" value = { notes } rows = { 2 }
onChange = { ( e ) = > setNotes ( 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 className = "grid grid-cols-2 gap-4" >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-eno" > Enterprise No . < span className = "text-red-400" > * < / span > < / label >
< input id = "edit-eno" type = "text" value = { enterpriseNo }
onChange = { ( e ) = > setEnterpriseNo ( e . target . value . toUpperCase ( ) ) }
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"
required / >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-etype" > Enterprise type < span className = "text-xs text-muted font-normal" > ( optional ) < / span > < / label >
< input id = "edit-etype" type = "text" value = { enterpriseType }
onChange = { ( e ) = > setEnterpriseType ( 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"
placeholder = "Private Company" / >
< / div >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-ename" > Enterprise name < span className = "text-red-400" > * < / span > < / label >
< input id = "edit-ename" type = "text" value = { enterpriseName }
onChange = { ( e ) = > setEnterpriseName ( 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 / >
< / div >
< div >
< label className = "block text-sm font-semibold text-fg mb-1.5" htmlFor = "edit-rdate" > Registration date < span className = "text-xs text-muted font-normal" > ( optional ) < / span > < / label >
< input id = "edit-rdate" type = "date" value = { registrationDate }
onChange = { ( e ) = > setRegistrationDate ( 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-semibold text-fg mb-1.5" htmlFor = "edit-phys" > Physical address < span className = "text-xs text-muted font-normal" > ( optional ) < / 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 >
< 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 >
< 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 }
onChange = { ( e ) = > setNotesCo ( 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 className = "pt-2 flex gap-3" >
< button type = "button" onClick = { onClose }
className = "flex-1 px-4 py-2.5 rounded-lg border border-border bg-surface-warm text-sm font-semibold text-fg hover:bg-surface transition-colors" >
Cancel
< / button >
< button type = "submit" disabled = { loading }
className = "flex-1 px-4 py-2.5 rounded-lg bg-accent text-accent-on text-sm font-bold hover:bg-accent-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed" >
{ loading ? "Saving..." : "Save changes" }
< / button >
< / div >
< / form >
< / div >
< / div >
) ;
}