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.
105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
// components/ProcessActions.tsx
|
|
// Client component that drives the Process / Archive / Reopen buttons
|
|
// for a single onboarding submission. Optimistically disables itself
|
|
// while the API call is in flight and revalidates the page on success.
|
|
|
|
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface Props {
|
|
id: string;
|
|
status: "draft" | "submitted" | "processing" | "processed" | "rejected";
|
|
directorCount: number;
|
|
hasDuplicates: boolean;
|
|
}
|
|
|
|
export default function ProcessActions({
|
|
id,
|
|
status,
|
|
directorCount,
|
|
hasDuplicates,
|
|
}: Props) {
|
|
const router = useRouter();
|
|
const [busy, setBusy] = useState<"process" | "archive" | "reopen" | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function call(action: "process" | "archive" | "reopen") {
|
|
setBusy(action);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(`/api/onboarding/${id}/${action}`, { method: "POST" });
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
throw new Error(body.error ?? `Request failed (${res.status})`);
|
|
}
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Action failed");
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
}
|
|
|
|
const isDraft = status === "draft";
|
|
const isSubmitted = status === "submitted";
|
|
const isProcessed = status === "processed";
|
|
const isArchived = status === "rejected";
|
|
const canProcess = isSubmitted && directorCount > 0;
|
|
|
|
return (
|
|
<div className="bg-surface border border-border rounded-lg p-5 space-y-3" data-od-id="process-actions">
|
|
<h2 className="text-sm font-semibold text-fg uppercase tracking-wider">Actions</h2>
|
|
|
|
{hasDuplicates && canProcess ? (
|
|
<p className="text-xs text-warn font-medium bg-[color-mix(in_oklch,var(--warn)_10%,white)] border border-[color:var(--warn)]/30 rounded-md p-2">
|
|
Duplicate IDs detected — review before processing.
|
|
</p>
|
|
) : null}
|
|
|
|
{error ? (
|
|
<p className="text-xs text-danger font-medium bg-[color-mix(in_oklch,var(--danger)_8%,white)] border border-[color:var(--danger)]/30 rounded-md p-2">
|
|
{error}
|
|
</p>
|
|
) : null}
|
|
|
|
{(isSubmitted || isDraft) ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => call("process")}
|
|
disabled={!canProcess || busy !== null}
|
|
className="w-full px-4 py-2.5 rounded-md bg-accent text-accent-on text-sm font-bold hover:bg-accent-hover disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{busy === "process" ? "Processing…" : `Create ${directorCount} director record${directorCount === 1 ? "" : "s"}`}
|
|
</button>
|
|
) : null}
|
|
|
|
{isProcessed ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => call("reopen")}
|
|
disabled={busy !== null}
|
|
className="w-full px-4 py-2.5 rounded-md border border-border text-sm font-semibold text-fg hover:bg-surface-warm disabled:opacity-50 transition-colors"
|
|
>
|
|
{busy === "reopen" ? "Reopening…" : "Reopen submission"}
|
|
</button>
|
|
) : null}
|
|
|
|
{(isSubmitted || isProcessed || isDraft) && !isArchived ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => call("archive")}
|
|
disabled={busy !== null}
|
|
className="w-full px-4 py-2.5 rounded-md border border-border text-sm font-semibold text-muted hover:text-danger hover:border-danger disabled:opacity-50 transition-colors"
|
|
>
|
|
{busy === "archive" ? "Archiving…" : "Archive"}
|
|
</button>
|
|
) : null}
|
|
|
|
{isArchived ? (
|
|
<p className="text-xs text-muted italic">This submission has been archived.</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|