Workflow-App/Workflow/app/directors/page.tsx
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
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.
2026-10-18 15:21:45 +02:00

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