WORKFLOW-APP/components/AddEntityModal.tsx

391 lines
16 KiB
TypeScript
Raw Normal View History

"use client";
// components/AddEntityModal.tsx
// Shared modal for adding a new client director or company.
// Opens on "Add Director" / "Add Company" button click.
// On submit: POSTs to /api/pb/records/create, invalidates the
// relevant list query, shows success toast, closes modal.
import { useState, useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { pbCreateClient } from "@/lib/api";
import { pbList } from "@/lib/api";
interface Props {
/** "director" | "company" */
entity: "director" | "company";
/** Controlled open state (passed from parent) */
open: boolean;
/** Called when modal requests to close (user dismissed or success) */
onClose: () => void;
}
interface Director {
id: string;
surname: string;
first_names: string;
id_no: string;
}
export default function AddEntityModal({ entity, open, onClose }: Props) {
const qc = useQueryClient();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const overlayRef = useRef<HTMLDivElement>(null);
// Director form state
const [surname, setSurname] = useState("");
const [firstNames, setFirstNames] = useState("");
const [idNo, setIdNo] = useState("");
const [telephone, setTelephone] = useState("");
const [email, setEmail] = useState("");
// Company form state
const [enterpriseNo, setEnterpriseNo] = useState("");
const [enterpriseName, setEnterpriseName] = useState("");
const [enterpriseType, setEnterpriseType] = useState("");
const [registrationDate, setRegistrationDate] = useState("");
const [physicalAddress, setPhysicalAddress] = useState("");
const [postalAddress, setPostalAddress] = useState("");
const [selectedDirectorId, setSelectedDirectorId] = useState("");
// Directors list for company form dropdown
const [directors, setDirectors] = useState<Director[]>([]);
// Reset form whenever modal opens
useEffect(() => {
if (open) {
setSurname(""); setFirstNames(""); setIdNo(""); setTelephone(""); setEmail("");
setEnterpriseNo(""); setEnterpriseName(""); setEnterpriseType("");
setRegistrationDate(""); setPhysicalAddress(""); setPostalAddress("");
setSelectedDirectorId(""); setError(null);
}
}, [open]);
// Load directors list when opening the company form
useEffect(() => {
if (open && entity === "company") {
pbList<Director>({
collection: "ysterbaard_directors",
perPage: 500,
sort: "surname",
fields: "id,surname,first_names,id_no",
}).then((r) => setDirectors(r.items)).catch(() => {});
}
}, [open, entity]);
if (!open) return null;
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === overlayRef.current) onClose();
};
const isDirector = entity === "director";
const title = isDirector ? "Add new client director" : "Add new client company";
const subtitle = isDirector
? "Fill in the details. The director will be saved with is_client=true."
: "Fill in the details. The company will be saved with is_client=true.";
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
if (isDirector) {
if (!surname.trim() || !firstNames.trim()) {
setError("Surname and first names are required.");
setLoading(false);
return;
}
const fields: Record<string, string> = {
surname: surname.trim(),
first_names: firstNames.trim(),
source: "app",
};
if (idNo.trim()) fields.id_no = idNo.trim();
if (telephone.trim()) fields.telephone = telephone.trim();
if (email.trim()) fields.email = email.trim();
await pbCreateClient({ collection: "ysterbaard_directors", fields });
await qc.invalidateQueries({ queryKey: ["pb-list", "ysterbaard_directors"] });
} else {
if (!enterpriseNo.trim() || !enterpriseName.trim()) {
setError("Enterprise number and name are required.");
setLoading(false);
return;
}
const fields: Record<string, string> = {
enterprise_no: enterpriseNo.trim().toUpperCase(),
enterprise_name: enterpriseName.trim().toUpperCase(),
source: "app",
};
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 (postalAddress.trim()) fields.postal_address = postalAddress.trim();
if (selectedDirectorId) fields.director = selectedDirectorId;
await pbCreateClient({ collection: "ysterbaard_companies", fields });
await qc.invalidateQueries({ queryKey: ["pb-list", "ysterbaard_companies"] });
}
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">{title}</h2>
<p className="text-sm text-muted mt-0.5">{subtitle}</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 ? (
<>
{/* Surname */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="surname">
Surname <span className="text-red-400">*</span>
</label>
<input
id="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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="e.g. JONES"
required
/>
</div>
{/* First names */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="firstNames">
First names <span className="text-red-400">*</span>
</label>
<input
id="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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="e.g. PETER JOHANNES"
required
/>
</div>
{/* ID number */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="idNo">
SA ID number <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<input
id="idNo"
type="text"
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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent font-mono"
placeholder="13 digits"
maxLength={13}
/>
</div>
{/* Telephone */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="telephone">
Telephone <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<input
id="telephone"
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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="0821234567"
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="email">
Email <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<input
id="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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="director@example.com"
/>
</div>
</>
) : (
<>
{/* Enterprise number */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="enterpriseNo">
Enterprise number <span className="text-red-400">*</span>
</label>
<input
id="enterpriseNo"
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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="K2022808158"
required
/>
</div>
{/* Enterprise name */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="enterpriseName">
Enterprise name <span className="text-red-400">*</span>
</label>
<input
id="enterpriseName"
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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="ABC TRADING PTY LTD"
required
/>
</div>
{/* Enterprise type */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="enterpriseType">
Enterprise type <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<input
id="enterpriseType"
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 placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent"
placeholder="Private Company"
/>
</div>
{/* Registration date */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="registrationDate">
Registration date <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<input
id="registrationDate"
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>
{/* Physical address */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="physicalAddress">
Physical address <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<textarea
id="physicalAddress"
value={physicalAddress}
onChange={(e) => setPhysicalAddress(e.target.value)}
rows={2}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent resize-none"
placeholder="123 Main Street, Pretoria, 0001"
/>
</div>
{/* Postal address */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="postalAddress">
Postal address <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<textarea
id="postalAddress"
value={postalAddress}
onChange={(e) => setPostalAddress(e.target.value)}
rows={2}
className="w-full rounded-lg border border-border bg-surface-warm px-3 py-2.5 text-sm text-fg placeholder:text-muted focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent resize-none"
placeholder="PO Box 123, Pretoria, 0001"
/>
</div>
{/* Linked director dropdown */}
<div>
<label className="block text-sm font-semibold text-fg mb-1.5" htmlFor="directorSelect">
Linked director <span className="text-xs text-muted font-normal">(optional)</span>
</label>
<select
id="directorSelect"
value={selectedDirectorId}
onChange={(e) => setSelectedDirectorId(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="">— None —</option>
{directors.map((d) => (
<option key={d.id} value={d.id}>
{d.surname}, {d.first_names}
{d.id_no ? ` (${d.id_no})` : ""}
</option>
))}
</select>
</div>
</>
)}
{/* Submit */}
<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 as client"}
</button>
</div>
</form>
</div>
</div>
);
}