Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
// app/directors/page.tsx
|
|
// Server Component. Loads the first page of directors server-side
|
|
// for instant first paint, then hands off to DirectorExplorer for
|
|
// client-side pagination + search + client filtering.
|
|
|
|
import { fetchCollection } from "@/lib/pb";
|
|
import { quoteFilter } from "@/lib/format";
|
|
import type { PBList, YsterbaardDirector } from "@/lib/types";
|
|
import PageHeader from "@/components/PageHeader";
|
|
import DirectorExplorer from "./DirectorExplorer";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
interface SearchParams {
|
|
q?: string;
|
|
page?: string;
|
|
sort?: string;
|
|
/** "all" | "active" | "inactive" */
|
|
cf?: string;
|
|
}
|
|
|
|
interface Props {
|
|
searchParams: SearchParams;
|
|
}
|
|
|
|
type ClientFilter = "all" | "active" | "inactive";
|
|
|
|
function normalizeClientFilter(raw: string | undefined): ClientFilter {
|
|
if (raw === "active" || raw === "inactive") return raw;
|
|
return "active";
|
|
}
|
|
|
|
function buildSearchFilter(q: string | undefined): string | undefined {
|
|
if (!q || q.trim().length === 0) return undefined;
|
|
const term = q.trim();
|
|
return `(surname ~ '${quoteFilter(term)}' || first_names ~ '${quoteFilter(term)}' || id_no ~ '${quoteFilter(term)}' || telephone ~ '${quoteFilter(term)}')`;
|
|
}
|
|
|
|
async function loadFirstPage(
|
|
q: string | undefined,
|
|
sort: string,
|
|
cf: ClientFilter
|
|
): Promise<PBList<YsterbaardDirector>> {
|
|
const search = buildSearchFilter(q);
|
|
const client =
|
|
cf === "active"
|
|
? "is_client = true && active_companies > 0"
|
|
: cf === "inactive"
|
|
? "is_client = true && active_companies = 0"
|
|
: undefined;
|
|
const filter =
|
|
search && client ? `(${search}) && (${client})` : search ?? client;
|
|
|
|
return fetchCollection<YsterbaardDirector>("ysterbaard_directors", {
|
|
page: 1,
|
|
perPage: 25,
|
|
sort,
|
|
filter,
|
|
fields:
|
|
"id,id_no,surname,first_names,telephone,total_companies,active_companies,inactive_companies,disqualification_status,last_checked,is_client"
|
|
});
|
|
}
|
|
|
|
export default async function DirectorsPage({ searchParams }: Props) {
|
|
const q = searchParams.q;
|
|
const page = Number(searchParams.page ?? "1") || 1;
|
|
const sort = searchParams.sort ?? "-last_checked";
|
|
const cf = normalizeClientFilter(searchParams.cf);
|
|
|
|
const initial = await loadFirstPage(q, sort, cf);
|
|
|
|
return (
|
|
<>
|
|
<PageHeader
|
|
eyebrow="Directors"
|
|
title="People on the registry"
|
|
subtitle="Search by surname, first names, ID number, or telephone. Click a row to open the director's linked companies. Use the Client pills to scope to your active or inactive clients."
|
|
/>
|
|
<section className="container-x py-10">
|
|
<DirectorExplorer
|
|
initial={initial}
|
|
initialQuery={q ?? ""}
|
|
initialSort={sort}
|
|
initialPage={page}
|
|
initialClientFilter={cf}
|
|
/>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|