"use client"; // components/DirectorResignationTab.tsx // Send a director's Letter of Resignation. Mirrors DirectorAppointmentTab UX: // pick director + company → preview recipients → send. // On submit, POST /api/resignations which creates the PB record + emails the // director with a sign link. import { useState } from "react"; import SearchableSelect, { type SearchableSelectRecord } from "./SearchableSelect"; // PB row shapes for the picker. Cast happens at the JSX site; we keep these // type aliases so the rest of the file reads naturally. type DirectorRow = SearchableSelectRecord & { first_names?: string; surname?: string; email?: string; telephone?: string; }; type CompanyRow = SearchableSelectRecord & { enterprise_name?: string; company_email?: string; }; type RecipientStatus = "valid" | "missing" | "invalid"; interface Recipient { key: string; label: string; email: string; role: string; status: RecipientStatus; } interface PreviewReport { recipients: Recipient[]; problems: number; } function StatusDot({ status }: { status: RecipientStatus }) { if (status === "valid") return ✓; if (status === "missing") return !; return x; } export default function DirectorResignationTab() { const [showModal, setShowModal] = useState(false); const [sending, setSending] = useState(false); const [previewing, setPreviewing] = useState(false); const [sendError, setSendError] = useState(null); const [sent, setSent] = useState<{ id: string; signUrl: string } | null>(null); const [selectedDirector, setSelectedDirector] = useState(null); const [selectedCompany, setSelectedCompany] = useState(null); const [report, setReport] = useState(null); const openModal = () => { setShowModal(true); setSendError(null); setSent(null); setSelectedDirector(null); setSelectedCompany(null); setReport(null); // Director + company lists are fetched on-demand by the SearchableSelect // when the user opens the dropdown — no upfront load. }; const handlePreview = async () => { if (!selectedDirector || !selectedCompany) { setSendError("Please select a company and a director first."); return; } setPreviewing(true); setSendError(null); setReport(null); try { const director = selectedDirector; const company = selectedCompany; // Re-fetch to get the freshest email/company_email. const dirDetails = await fetch(`/api/pb/records/${director.id}?collection=ysterbaard_directors&fields=id,first_names,surname,email`).then((r) => r.ok ? r.json() : null).catch(() => null); const coDetails = await fetch(`/api/pb/records/${company.id}?collection=ysterbaard_companies&fields=id,enterprise_name,company_email`).then((r) => r.ok ? r.json() : null).catch(() => null); const emailRx = /^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$/; const classify = (e: string | undefined | null): RecipientStatus => { const v = (e ?? "").trim(); if (!v) return "missing"; return emailRx.test(v) ? "valid" : "invalid"; }; const dirLabel = [director.first_names, director.surname].filter(Boolean).join(" ") || director.id; const recipients: Recipient[] = [ { key: "director", label: `Director: ${dirLabel}`, email: dirDetails?.email ?? director.email ?? "", role: "to", status: classify(dirDetails?.email ?? director.email), }, { key: "company_email", label: `Company: ${coDetails?.enterprise_name ?? company.enterprise_name ?? company.id}`, email: coDetails?.company_email ?? "", role: "to", status: classify(coDetails?.company_email), }, ]; const problems = recipients.filter((r) => r.status !== "valid").length; setReport({ recipients, problems }); } catch (err: unknown) { setSendError(err instanceof Error ? err.message : "Validation failed."); } finally { setPreviewing(false); } }; const handleSend = async (proceedAnyway: boolean) => { if (!selectedDirector || !selectedCompany) return; setSending(true); setSendError(null); try { const res = await fetch("/api/resignations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ director_id: selectedDirector.id, company_id: selectedCompany.id, proceed_anyway: proceedAnyway, }), }); // The route answers 502 (not 200) when the mail itself failed, and // 500 on a hard error. Previously it answered 200 with // { ok: false } and this client ignored `ok` — so the UI showed // "Letter sent successfully" while nothing had left the machine. const data = await res.json().catch(() => ({} as Record)); if (!res.ok) { throw new Error( (data?.error as string) || (data?.warning as string) || `Letter failed (HTTP ${res.status})` ); } if (data?.ok === false) { throw new Error( (data?.warning as string) || "The letter was saved but the email could not be sent." ); } const signToken = data?.record?.sign_token; const baseUrl = typeof window !== "undefined" ? window.location.origin : ""; setSent({ id: data?.record?.id || "", signUrl: signToken ? `${baseUrl}/resign/${signToken}` : "", }); setReport(null); } catch (err: unknown) { setSendError(err instanceof Error ? err.message : "Failed to send letter"); } finally { setSending(false); } }; const closeModal = () => { setShowModal(false); setSent(null); setReport(null); setSendError(null); }; return (

Director resignations

{showModal && (
{ if (e.target === e.currentTarget) closeModal(); }} >

Send Letter of Resignation

{sent ? "Letter sent — share the sign link with the director." : report ? "Review the recipient list before sending." : "Select a company and director to send the resignation letter for signature."}

{sent ? (

Letter sent successfully.

{sent.signUrl && ( <>

Share this link with the director to sign:

)}
) : ( <>
{ setSelectedCompany(r as CompanyRow | null); setReport(null); }} renderLabel={(r) => (r.enterprise_name as string) || (r.id as string)} renderSub={(r) => (r.enterprise_no as string) || (r.company_email as string) || ""} required /> { setSelectedDirector(r as DirectorRow | null); setReport(null); }} renderLabel={(r) => [r.first_names as string, r.surname as string].filter(Boolean).join(" ") || (r.id as string)} renderSub={(r) => (r.email as string) || (r.telephone as string) || (r.id_no as string) || ""} required />
{sendError &&

{sendError}

} {report && (

Recipients

{report.recipients.map((r) => (
{r.label} {r.email || "(no email)"}
))} {report.problems > 0 && (

⚠ {report.problems} recipient(s) missing or invalid email.

)}
)} )}
{sent ? ( ) : report ? ( <> ) : ( <> )}
)}
); }