WORKFLOW-APP/app/page.tsx

157 lines
4.9 KiB
TypeScript

// app/page.tsx
// Dashboard / landing. Server-rendered summary tiles + sections
// driven by direct PocketBase calls via lib/pb.ts.
//
// The page itself stays thin — it loads the data and passes it to
// components/Dashboard.tsx, which owns the visual layout. This
// keeps PB error handling in one place and lets the dashboard
// component stay testable in isolation.
//
// All PB queries run in parallel (Promise.all) to keep first paint
// under a single round-trip.
import Link from "next/link";
import { fetchCollection } from "@/lib/pb";
import type {
YsterbaardCompany,
YsterbaardDirector,
} from "@/lib/types";
import { deriveCompanyStatus } from "@/lib/format";
import Dashboard from "@/components/Dashboard";
import PageHeader from "@/components/PageHeader";
export const dynamic = "force-dynamic";
async function loadDashboardData() {
// Parallel PB calls. Each one is bounded (perPage=500 or 5).
const [
directorsCount,
companiesCount,
companiesFull,
directorsFull,
recentCompanies,
recentDirectors,
] = await Promise.all([
fetchCollection<YsterbaardDirector>("ysterbaard_directors", {
page: 1,
perPage: 1,
fields: "id",
}),
fetchCollection<YsterbaardCompany>("ysterbaard_companies", {
page: 1,
perPage: 1,
fields: "id",
}),
// Full sweep for status rollups + client counts.
fetchCollection<YsterbaardCompany>("ysterbaard_companies", {
page: 1,
perPage: 500,
fields:
"id,enterprise_no,enterprise_name,enterprise_status,is_active,is_client",
}),
// Directors sweep for client count + active-role count.
fetchCollection<YsterbaardDirector>("ysterbaard_directors", {
page: 1,
perPage: 500,
fields: "id,total_companies,is_client",
}),
fetchCollection<YsterbaardCompany>("ysterbaard_companies", {
page: 1,
perPage: 5,
sort: "-last_checked",
fields:
"id,enterprise_no,enterprise_name,enterprise_status,is_active,is_client,last_checked",
}),
fetchCollection<YsterbaardDirector>("ysterbaard_directors", {
page: 1,
perPage: 5,
sort: "-last_checked",
fields:
"id,id_no,first_names,surname,total_companies,is_client,last_checked",
}),
]);
// Company rollups
const rollup = companiesFull.items.reduce(
(acc, c) => {
const bucket = deriveCompanyStatus(c.enterprise_status, c.is_active);
if (bucket === "active") acc.active += 1;
else if (bucket === "inactive") acc.inactive += 1;
else if (bucket === "in_process") acc.in_process += 1;
else if (bucket === "deregistered") acc.deregistered += 1;
if (c.is_client) acc.clients += 1;
return acc;
},
{ active: 0, inactive: 0, in_process: 0, deregistered: 0, clients: 0 }
);
// Director rollups
const directorRollup = directorsFull.items.reduce(
(acc, d) => {
if ((d.total_companies ?? 0) > 0) acc.activeRoles += 1;
if (d.is_client) acc.clients += 1;
return acc;
},
{ activeRoles: 0, clients: 0 }
);
return {
kpis: {
directors: {
total: directorsCount.totalItems,
activeRoles: directorRollup.activeRoles,
clients: directorRollup.clients,
},
companies: {
total: companiesCount.totalItems,
active: rollup.active,
inProcess: rollup.in_process,
inactive: rollup.inactive,
deregistered: rollup.deregistered,
clients: rollup.clients,
},
},
statusDistribution: [
{ key: "active", label: "In business", count: rollup.active },
{ key: "in_process", label: "In process", count: rollup.in_process },
{ key: "inactive", label: "Inactive", count: rollup.inactive },
{ key: "deregistered", label: "Deregistered", count: rollup.deregistered },
] as const,
recentCompanies: recentCompanies.items,
recentDirectors: recentDirectors.items,
};
}
export default async function DashboardPage() {
const data = await loadDashboardData();
return (
<>
<PageHeader
eyebrow="Overview"
title="Registry dashboard"
subtitle="Live counts from PocketBase. Click into a section for the full searchable list."
action={
<div className="flex gap-2">
<Link
href="/directors"
data-od-id="cta-directors"
className="inline-flex items-center px-5 py-3 rounded-md bg-accent text-accent-on font-semibold text-sm no-underline hover:bg-accent-hover transition-colors"
>
Browse directors
</Link>
<Link
href="/companies"
data-od-id="cta-companies"
className="inline-flex items-center px-5 py-3 rounded-md border border-border bg-surface text-fg font-semibold text-sm no-underline hover:bg-surface-warm transition-colors"
>
Browse companies
</Link>
</div>
}
/>
<Dashboard {...data} />
</>
);
}