// components/ResolutionExplorer.tsx // Client Component. 4-section menu for company registration, // director resignation, director appointment, and resolutions. // Only Company Registration is active; others are placeholders. "use client"; import { useState, useRef, useEffect, useCallback } from "react"; import type { ComponentProps } from "react"; import type { PBList, YsterbaardResolution, ResolutionStatus, } from "@/lib/types"; import StatusBadge from "@/components/StatusBadge"; import { useRouter } from "next/navigation"; type Section = "company_registration" | "director_resignation" | "director_appointment" | "resolution_appointment"; const SECTIONS: { id: Section; label: string }[] = [ { id: "company_registration", label: "1 — Company registration" }, { id: "director_resignation", label: "2 — Director resignation" }, { id: "director_appointment", label: "3 — Director appointment acceptance" }, { id: "resolution_appointment", label: "4 — Resolution appointment and resignation" }, ]; type BadgeVariant = ComponentProps["variant"]; function statusVariant(s: ResolutionStatus): BadgeVariant { switch (s) { case "draft": return "muted"; case "pending": return "warn"; case "completed": return "success"; case "archived": return "muted"; } } function formatDate(iso: string | null): string { if (!iso) return "—"; return new Date(iso).toLocaleDateString("en-ZA", { day: "2-digit", month: "short", year: "numeric", }); } interface Props { resolutions: PBList; } interface RegSearchItem { id: string; reference_name: string; status: string; proposed_name_1: string; } export default function ResolutionExplorer({ resolutions }: Props) { const router = useRouter(); const [activeSection, setActiveSection] = useState
("company_registration"); const [showNewDialog, setShowNewDialog] = useState(false); const [newRegName, setNewRegName] = useState(""); const [newRegLoading, setNewRegLoading] = useState(false); const [generatedLink, setGeneratedLink] = useState(null); const [dialogError, setDialogError] = useState(null); const [linkCopied, setLinkCopied] = useState(false); // Search state const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [searchFocused, setSearchFocused] = useState(false); const searchRef = useRef(null); const searchTimeout = useRef | null>(null); const filteredResolutions = resolutions.items .filter((r) => r.type === activeSection) .sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime()); // Close search dropdown when clicking outside useEffect(() => { const handler = (e: MouseEvent) => { if (searchRef.current && !searchRef.current.contains(e.target as Node)) { setSearchFocused(false); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, []); // Search registrations as user types const doSearch = useCallback(async (q: string) => { if (q.trim().length === 0) { setSearchResults([]); setSearchLoading(false); return; } setSearchLoading(true); try { const res = await fetch(`/api/register/search?q=${encodeURIComponent(q.trim())}&fields=id,reference_name,status,proposed_name_1`); if (!res.ok) throw new Error(); const data = await res.json(); setSearchResults(data.items || []); } catch { setSearchResults([]); } finally { setSearchLoading(false); } }, []); const handleSearchChange = (e: React.ChangeEvent) => { const q = e.target.value; setSearchQuery(q); if (searchTimeout.current) clearTimeout(searchTimeout.current); if (!q.trim()) { setSearchResults([]); setSearchLoading(false); return; } setSearchLoading(true); searchTimeout.current = setTimeout(() => doSearch(q), 300); }; const handleSelectExisting = (reg: RegSearchItem) => { setSearchFocused(false); setShowNewDialog(false); router.push(`/register/${reg.id}?prefill=1`); }; const handleCreateRegistration = async () => { if (!newRegName.trim()) return; setNewRegLoading(true); setDialogError(null); try { const res = await fetch("/api/register/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reference_name: newRegName.trim() }), }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); setGeneratedLink(`${window.location.origin}/register/${data.id}`); } catch (err: unknown) { setDialogError(err instanceof Error ? err.message : "Failed to create link"); } finally { setNewRegLoading(false); } }; const openNewDialog = () => { setNewRegName(""); setGeneratedLink(null); setDialogError(null); setSearchQuery(""); setSearchResults([]); setShowNewDialog(true); }; return (
{/* 4 Section Buttons */}
{SECTIONS.map((sec) => { const isActive = activeSection === sec.id; const isDisabled = sec.id !== "company_registration"; return ( ); })}
{/* ── Company Registration ── */} {activeSection === "company_registration" && (
{/* Header + New button */}

{filteredResolutions.length === 0 ? "No registrations yet" : `${filteredResolutions.length} registration${filteredResolutions.length === 1 ? "" : "s"}`}

{/* Registration search bar */}
setSearchFocused(true)} /> {searchLoading && (
)}
View all →
{/* Search results dropdown */} {searchFocused && searchResults.length > 0 && (
{searchResults.map((reg) => ( ))}
)} {searchFocused && !searchLoading && searchQuery.trim() && searchResults.length === 0 && (
No registrations found for “{searchQuery}”
)}
{/* Resolutions list (draft/pending resolutions for company_registration) */} {filteredResolutions.length > 0 && (
{filteredResolutions.map((r) => ( ))}
Title Client ID / Ref Status Date
{r.title ?? "—"} {r.client_name ?? "—"} {r.client_id_no ?? "—"} {r.status} {formatDate(r.resolution_date ?? r.created)}
)}
)} {/* ── Other sections (placeholder) ── */} {activeSection !== "company_registration" && (
This section is under development.
)} {/* New Registration Dialog */} {showNewDialog && (

New registration

Give this registration a reference name so you can find it later.

{!generatedLink ? ( <>
setNewRegName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleCreateRegistration(); }} autoFocus />
{dialogError &&

{dialogError}

} ) : (

Share this link with your client:

{typeof navigator !== "undefined" && navigator.share ? ( ) : null}

Send this link to your client. They will fill in the director, company and shareholder details. You will be notified on Telegram when they submit.

)}
{!generatedLink ? ( <> ) : ( )}
)}
); }