"use client"; // components/OverviewSearch.tsx // Global tax-number / CK-number search for the Overview tab. // // Searches ONLY the company registry — every field we search is a tax // or registration number, per SJ's rule (verified 2026-09-11). // Substring, case-insensitive. In-memory; the data ships from the // server with the rest of the dashboard props. import { useEffect, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { text } from "@/lib/format"; import type { PBRecord } from "@/lib/pb"; export interface OverviewSearchItem extends PBRecord { id: string; enterprise_no: string; enterprise_name: string; sars_tax_number: string | null; sars_paye_no: string | null; sars_vat_no: string | null; uif_registration_number: string | null; compensation_fund_registration_number: string | null; } interface Props { items: OverviewSearchItem[]; } interface Hit { id: string; enterprise_no: string; enterprise_name: string; /** Human label, e.g. "SARS PAYE no". */ fieldLabel: string; /** Actual value that matched (preserves original casing/spacing). */ fieldValue: string; /** Path the row should link to. */ href: string; } // Field definitions: (data key, human label) — order is intentional, // most-common first. const SEARCH_FIELDS: Array<{ key: keyof OverviewSearchItem; label: string }> = [ { key: "enterprise_no", label: "CK number" }, { key: "sars_tax_number", label: "SARS Income Tax no" }, { key: "sars_paye_no", label: "SARS PAYE no" }, { key: "sars_vat_no", label: "SARS VAT no" }, { key: "uif_registration_number", label: "UIF no" }, { key: "compensation_fund_registration_number", label: "COID no" }, ]; const MAX_RESULTS = 50; const MIN_QUERY_LEN = 2; function search(items: OverviewSearchItem[], q: string): Hit[] { const needle = q.trim().toLowerCase(); if (needle.length < MIN_QUERY_LEN) return []; const hits: Hit[] = []; for (const item of items) { for (const { key, label } of SEARCH_FIELDS) { const raw = item[key]; if (typeof raw !== "string" || raw === "") continue; if (raw.toLowerCase().includes(needle)) { hits.push({ id: item.id, enterprise_no: item.enterprise_no, enterprise_name: item.enterprise_name, fieldLabel: label, fieldValue: raw, href: `/companies/${item.id}`, }); // One hit per company is enough — don't double-list the same // record under multiple matching fields. If the user wants // every match, the CK number itself usually disambiguates. break; } } } return hits.slice(0, MAX_RESULTS); } export default function OverviewSearch({ items }: Props) { const [query, setQuery] = useState(""); const [debounced, setDebounced] = useState(""); const inputRef = useRef(null); // Debounce 150ms so each keystroke doesn't re-render the list. useEffect(() => { const handle = setTimeout(() => setDebounced(query), 150); return () => clearTimeout(handle); }, [query]); // Cmd/Ctrl+K focuses the search bar from anywhere on the page. useEffect(() => { const onKey = (e: KeyboardEvent) => { const isMod = e.metaKey || e.ctrlKey; if (isMod && e.key.toLowerCase() === "k") { e.preventDefault(); inputRef.current?.focus(); inputRef.current?.select(); } else if (e.key === "Escape" && document.activeElement === inputRef.current) { setQuery(""); inputRef.current?.blur(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const hits = useMemo(() => search(items, debounced), [items, debounced]); const showPanel = debounced.trim().length >= MIN_QUERY_LEN; const totalCompanies = items.length; const clear = () => { setQuery(""); setDebounced(""); inputRef.current?.focus(); }; return (
{/* Header / input row */}

Quick search

{totalCompanies.toLocaleString()} companies · CK no · SARS Income Tax · PAYE · VAT · UIF · COID
setQuery(e.target.value)} placeholder="Type a CK number or SARS tax number…" aria-label="Search companies by CK number or tax number" className="w-full bg-bg border border-border rounded-md pl-9 pr-20 py-2.5 text-sm text-fg placeholder:text-muted focus:outline-none focus:border-accent transition-colors font-mono" />
{query && ( )} Ctrl K
{/* Results panel — only renders when query is long enough */} {showPanel && (
{hits.length === 0 ? `No matches for "${debounced.trim()}"` : `${hits.length}${hits.length === MAX_RESULTS ? "+" : ""} match${hits.length === 1 ? "" : "es"} for "${debounced.trim()}"`}
{hits.length === 0 ? (
Nothing matched. Double-check the number — these are the fields we search:{" "} enterprise_no, sars_tax_number, sars_paye_no, sars_vat_no, uif_registration_number, compensation_fund_registration_number .
) : (
    {hits.map((hit, idx) => (
  • Company

    {text(hit.enterprise_name) || "—"}

    {hit.enterprise_no} · {hit.fieldLabel}:{" "} {hit.fieldValue}

  • ))}
)}
)} {/* Idle hint when no query */} {!showPanel && (
Type at least {MIN_QUERY_LEN} characters. Results open the company detail page in a click.
)}
); }