WORKFLOW-APP/components/AddEntityModal.tsx

390 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/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="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 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>
);
}