WORKFLOW-APP/components/EditEntityModal.tsx

331 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
<input id="edit-idno" type="tel" value={idNo}
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>
);
}