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.
173 lines
6.2 KiB
TypeScript
173 lines
6.2 KiB
TypeScript
// app/onboarding/page.tsx
|
|
// Internal inbox of all onboarding submissions. Status filter + simple
|
|
// table. Server Component for first paint, TanStack Query lives in the
|
|
// explorer for client-side refresh.
|
|
|
|
import Link from "next/link";
|
|
import PageHeader from "@/components/PageHeader";
|
|
import StatusBadge from "@/components/StatusBadge";
|
|
import { fetchCollection } from "@/lib/pb";
|
|
import type { OnboardingSubmission } from "@/lib/types";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const STATUS_FILTERS = [
|
|
{ value: "all", label: "All" },
|
|
{ value: "submitted", label: "New" },
|
|
{ value: "processing", label: "Processing" },
|
|
{ value: "processed", label: "Processed" },
|
|
{ value: "rejected", label: "Archived" },
|
|
] as const;
|
|
|
|
type StatusFilter = (typeof STATUS_FILTERS)[number]["value"];
|
|
|
|
function statusToVariant(status: OnboardingSubmission["status"]): "success" | "warn" | "danger" | "muted" | "active" | "inactive" | "in_process" | "deregistered" {
|
|
switch (status) {
|
|
case "submitted": return "active";
|
|
case "processing": return "in_process";
|
|
case "processed": return "success";
|
|
case "rejected": return "danger";
|
|
case "draft": return "muted";
|
|
default: return "muted";
|
|
}
|
|
}
|
|
|
|
function formatDate(iso: string | null): string {
|
|
if (!iso) return "—";
|
|
return new Date(iso).toLocaleString("en-ZA", {
|
|
year: "numeric", month: "short", day: "2-digit",
|
|
hour: "2-digit", minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
export default async function OnboardingInboxPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: { status?: string };
|
|
}) {
|
|
const filter = (STATUS_FILTERS.some((f) => f.value === searchParams.status)
|
|
? searchParams.status
|
|
: "all") as StatusFilter;
|
|
|
|
const pbFilter = filter === "all" ? undefined : `status = "${filter}"`;
|
|
|
|
const result = await fetchCollection<OnboardingSubmission>(
|
|
"ysterbaard_onboarding_submissions",
|
|
{
|
|
page: 1,
|
|
perPage: 50,
|
|
sort: "-submitted_at",
|
|
filter: pbFilter,
|
|
},
|
|
);
|
|
|
|
return (
|
|
<div data-od-id="onboarding-inbox">
|
|
<PageHeader
|
|
eyebrow="Workflow"
|
|
title="Onboard"
|
|
subtitle={
|
|
<>
|
|
<span>Submissions from new clients.</span>
|
|
<span>{result.totalItems} total</span>
|
|
<span>{filter === "all" ? "all statuses" : filter}</span>
|
|
</>
|
|
}
|
|
action={
|
|
<div className="flex items-center gap-3">
|
|
<Link
|
|
href="/onboarding/new"
|
|
className="inline-flex items-center px-4 py-2.5 rounded-md bg-accent text-accent-on text-sm font-bold hover:bg-accent-hover transition-colors"
|
|
>
|
|
+ New resolution link
|
|
</Link>
|
|
<Link
|
|
href="/"
|
|
className="inline-flex items-center px-4 py-2.5 rounded-md border border-border bg-surface text-fg text-sm font-semibold no-underline hover:bg-surface-warm transition-colors"
|
|
>
|
|
← Back to overview
|
|
</Link>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
<div className="container-x py-10">
|
|
<div className="flex flex-wrap items-center gap-2 mb-6">
|
|
{STATUS_FILTERS.map((f) => {
|
|
const isActive = f.value === filter;
|
|
const href = f.value === "all" ? "/onboarding" : `/onboarding?status=${f.value}`;
|
|
return (
|
|
<Link
|
|
key={f.value}
|
|
href={href}
|
|
className={
|
|
"px-3 py-1.5 rounded-pill text-sm font-semibold no-underline transition-colors " +
|
|
(isActive
|
|
? "bg-surface-warm text-fg border border-border"
|
|
: "text-muted hover:bg-surface-warm border border-transparent")
|
|
}
|
|
>
|
|
{f.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{result.items.length === 0 ? (
|
|
<div className="bg-surface-warm border border-border rounded-lg p-12 text-center">
|
|
<p className="text-base text-muted">No submissions match this filter.</p>
|
|
<p className="text-sm text-muted mt-2">
|
|
Mint a new onboarding link to send to a client.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="bg-surface border border-border rounded-lg overflow-hidden">
|
|
<div className="hidden md:grid grid-cols-[1fr_1fr_140px_180px_120px] gap-4 px-5 py-3 bg-surface-warm border-b border-border text-xs font-semibold text-muted uppercase tracking-wider">
|
|
<div>Reference</div>
|
|
<div>Client</div>
|
|
<div>Status</div>
|
|
<div>Submitted</div>
|
|
<div className="text-right">Action</div>
|
|
</div>
|
|
<ul>
|
|
{result.items.map((sub) => (
|
|
<li
|
|
key={sub.id}
|
|
className="border-b border-border-soft last:border-b-0"
|
|
>
|
|
<Link
|
|
href={`/onboarding/${sub.id}`}
|
|
className="grid grid-cols-1 md:grid-cols-[1fr_1fr_140px_180px_120px] gap-2 md:gap-4 px-5 py-4 hover:bg-surface-warm transition-colors no-underline"
|
|
>
|
|
<div>
|
|
<p className="font-mono text-sm font-semibold text-fg">{sub.reference_no}</p>
|
|
<p className="text-xs text-muted md:hidden mt-0.5">{sub.client_email}</p>
|
|
</div>
|
|
<div className="hidden md:block">
|
|
<p className="text-sm font-semibold text-fg">{sub.client_name || "—"}</p>
|
|
<p className="text-xs text-muted">{sub.client_email}</p>
|
|
</div>
|
|
<div>
|
|
<StatusBadge variant={statusToVariant(sub.status)}>
|
|
{sub.status}
|
|
</StatusBadge>
|
|
</div>
|
|
<div className="text-sm text-muted md:block hidden">
|
|
{formatDate(sub.submitted_at)}
|
|
</div>
|
|
<div className="md:hidden text-xs text-muted">
|
|
{formatDate(sub.submitted_at)}
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="text-sm font-semibold text-fg">View →</span>
|
|
</div>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|