feat: add BizPortal tab + name-already-approved flow + registrations improvements

This commit is contained in:
Dave 2026-08-14 18:08:51 +02:00
parent c5ee550795
commit 5cedaf714c
8 changed files with 581 additions and 23 deletions

View file

@ -0,0 +1,325 @@
// app/api/register/[id]/name-already-approved/route.ts
// POST — "Name Already Approved" button in Step 2
//
// Searches DeltaDet for existing CIPC Name Reservation + COR9.1 emails matching
// this registration (by proposed_name), then:
// 1. Patch status = bizportal_in_progress + cipc_reference_no
// 2. Parse COR9.1 PDF to extract decision (approved / rejected)
// 3. Patch registration with cipc_decision + status
// 4. Generate Director Acceptance Letter + Resolution PDFs
// 5. Save all 3 documents to PB
// 6. Email client with all 3 documents attached
//
// No new emails injected. Uses existing emails already in DeltaDet.
import { NextRequest, NextResponse } from "next/server";
import { execSync } from "child_process";
import { writeFileSync, mkdirSync, rmSync } from "fs";
const PB_URL = process.env.PB_URL;
const PB_TOKEN = process.env.PB_TOKEN;
const COLLECTION_ID = "pbc_3075950410";
export const dynamic = "force-dynamic";
export const maxDuration = 180;
const SCRIPTS = "/Volumes/KIOXIA/.hermes/profiles/ysterbaard/scripts";
const PYTHON = "/Volumes/KIOXIA/.hermes/hermes-agent/venv/bin/python";
const PB_AUTH = PB_TOKEN;
async function loadReg(id) {
const r = await fetch(`${PB_URL}/api/collections/${COLLECTION_ID}/records/${id}`, {
headers: { Authorization: PB_AUTH },
cache: "no-store",
});
if (!r.ok) return null;
return await r.json();
}
async function patchReg(id, body) {
const r = await fetch(`${PB_URL}/api/collections/${COLLECTION_ID}/records/${id}`, {
method: "PATCH",
headers: {
Authorization: PB_AUTH,
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
return await r.json();
}
function runCmd(cmd) {
try {
return execSync(cmd, { timeout: 120 * 1000, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
} catch (err) {
return err.stdout?.toString() || err.message || "";
}
}
// Escape a string for embedding in a shell double-quoted string
function shellEscape(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
}
// Find CIPC ref by proposed name — searches recent DeltaDet emails
function findCipcRefByName(proposedName) {
// Build a temp Python script file so we avoid all JS template literal escaping issues
const pythonCode = [
"import subprocess, re",
`proposed_name = "${shellEscape(proposedName)}"`,
"name_norm = proposed_name.strip().upper()",
"",
"result = subprocess.run(",
' ["himalaya", "-a", "deltadet", "--backend", "imap", "envelope", "list", "-p", "1", "-s", "60"],',
" capture_output=True, text=True, timeout=30",
")",
"",
'LOGGED_RE = re.compile(r"Name reservation lodged", re.IGNORECASE)',
'REF_RE = re.compile(r"Reference No\\\\s*:\\\\s*(\\\\d{8,})", re.IGNORECASE)',
'NAME_RE = re.compile(r"First proposed name\\\\s*:\\\\s*([^\\\\n\\\\r.]+?)\\\\s*(?:\\\\.|$)", re.IGNORECASE)',
"",
'for line in result.stdout.split("\\n"):',
' if "│" not in line:',
" continue",
' parts = [c.strip() for c in line.replace("┆", "│").split("│") if c.strip() and not set(c.strip()) <= set("─═┌┐└┘├┤┬┴┼ ")]',
" if len(parts) < 2:",
" continue",
" env_id = parts[0]",
" if not env_id.isdigit():",
" continue",
"",
" body_out = subprocess.run(",
' ["himalaya", "-a", "deltadet", "--backend", "imap", "message", "read", env_id],',
" capture_output=True, text=True, timeout=30",
" )",
" body = body_out.stdout",
" if not LOGGED_RE.search(body):",
" continue",
"",
" ref_match = REF_RE.search(body)",
" name_match = NAME_RE.search(body)",
" if not ref_match or not name_match:",
" continue",
"",
' ref = ref_match.group(1).strip()',
' name = name_match.group(1).strip().upper()',
" if name == name_norm:",
' print(f"REF:{ref} ENV:{env_id}")',
].join("\n");
const scriptPath = `/tmp/find_cipc_ref_${Date.now()}.py`;
writeFileSync(scriptPath, pythonCode);
const out = runCmd(`${PYTHON} "${scriptPath}" 2>&1`);
try { rmSync(scriptPath, { force: true }); } catch (_) {}
const match = out.match(/REF:(\d+)\s+ENV:(\d+)/);
if (match) return { ref: match[1], envId: match[2] };
return null;
}
// Find COR9.1 received email by CIPC reference
function findCor91ByRef(ref) {
const pythonCode = [
"import subprocess, re",
`search_ref = "${shellEscape(ref)}"`,
"",
"result = subprocess.run(",
' ["himalaya", "-a", "deltadet", "--backend", "imap", "envelope", "list", "-p", "1", "-s", "60"],',
" capture_output=True, text=True, timeout=30",
")",
"",
'COR9_RE = re.compile(r"COR9\\\\.1 received", re.IGNORECASE)',
'REF_RE = re.compile(r"Tracking reference\\\\s*:?\\\\s*(\\\\d+)", re.IGNORECASE)',
"",
'for line in result.stdout.split("\\n"):',
' if "│" not in line:',
" continue",
' parts = [c.strip() for c in line.replace("┆", "│").split("│") if c.strip() and not set(c.strip()) <= set("─═┌┐└┘├┤┬┴┼ ")]',
" if len(parts) < 2:",
" continue",
" env_id = parts[0]",
" if not env_id.isdigit():",
" continue",
"",
" body_out = subprocess.run(",
' ["himalaya", "-a", "deltadet", "--backend", "imap", "message", "read", env_id],',
" capture_output=True, text=True, timeout=30",
" )",
" body = body_out.stdout",
" if not COR9_RE.search(body):",
" continue",
"",
" ref_match = REF_RE.search(body)",
" if not ref_match:",
" continue",
"",
' found_ref = ref_match.group(1).strip()',
" if found_ref == search_ref:",
' print(f"ENV:{env_id}")',
].join("\n");
const scriptPath = `/tmp/find_cor91_${Date.now()}.py`;
writeFileSync(scriptPath, pythonCode);
const out = runCmd(`${PYTHON} "${scriptPath}" 2>&1`);
try { rmSync(scriptPath, { force: true }); } catch (_) {}
const match = out.match(/ENV:(\d+)/);
if (match) return { envId: match[1] };
return null;
}
// Download COR9.1 PDF attachment from email
function downloadCor91Pdf(envId, outDir) {
const out = runCmd(`himalaya -a deltadet --backend imap attachment download ${envId} 1 --dir "${outDir}" 2>&1`);
const lsOut = runCmd(`ls -t "${outDir}"/*.pdf 2>/dev/null | head -1`);
const pdfPath = lsOut.trim();
return pdfPath && pdfPath.length > 0 ? pdfPath : null;
}
// Parse COR9.1 PDF to determine decision
function parseCor91Decision(pdfPath) {
const out = runCmd(`${PYTHON} "${SCRIPTS}/parse_cor91.py" "${pdfPath}" 2>&1`);
const text = out.toLowerCase();
if (text.includes("has been reserved")) return "approved";
if (text.includes("regret to inform") && text.includes("no name has been approved")) return "rejected";
return "unknown";
}
// Generate PDFs and return their paths
function generatePdfs(regId, outDir) {
runCmd(`TMPDIR="${outDir}" ${PYTHON} "${SCRIPTS}/reg_letter_generator.py" ${regId} 2>&1`);
runCmd(`TMPDIR="${outDir}" ${PYTHON} "${SCRIPTS}/reg_resolution_generator.py" ${regId} 2>&1`);
const letterPdf = runCmd(`ls "${outDir}"/Director_Acceptance*.pdf 2>/dev/null | head -1`).trim() || null;
const resPdf = runCmd(`ls "${outDir}"/New_Company_Resolution.pdf 2>/dev/null | head -1`).trim() || null;
return { letterPdf, resPdf };
}
// Upload a file to PB via the app's upload endpoint
function uploadToPB(regId, filePath, docType) {
const out = runCmd(
`curl -s -X POST "http://localhost:6001/api/register/${regId}/upload" ` +
`-H "Authorization: Bearer ${PB_TOKEN}" ` +
`-F "files=@${filePath};type=application/pdf" ` +
`-F "type=${docType}" 2>&1`
);
console.log(`Upload ${docType}: ${out.slice(0, 200)}`);
}
async function POST(req, { params }) {
const regId = params.id;
const reg = await loadReg(regId);
if (!reg) {
return NextResponse.json({ error: "Registration not found" }, { status: 404 });
}
const proposedName = (
reg.proposed_name_1 || reg.proposed_name_2 || reg.proposed_name_3 || reg.proposed_name_4 || ""
).trim();
const steps = [];
// Step 1: Find CIPC reference by proposed name
const refInfo = findCipcRefByName(proposedName);
if (!refInfo) {
return NextResponse.json({
ok: false,
error: `Could not find a CIPC Name Reservation email in DeltaDet for "${proposedName}". Make sure the name was submitted on BizPortal first.`,
registration: { id: regId, reference_name: reg.reference_name, status: reg.status }
}, { status: 200 });
}
steps.push(`Found CIPC ref ${refInfo.ref} (email ${refInfo.envId})`);
// Step 2: Find COR9.1 email by reference
const cor91Info = findCor91ByRef(refInfo.ref);
if (!cor91Info) {
return NextResponse.json({
ok: false,
error: `Found CIPC reference ${refInfo.ref} but could not find the COR9.1 approval email. The approval may not have arrived yet.`,
registration: { id: regId, reference_name: reg.reference_name, status: reg.status }
}, { status: 200 });
}
steps.push(`Found COR9.1 email ${cor91Info.envId}`);
// Step 3: Download COR9.1 PDF
const tmpDir = `/tmp/name_already_approved_${regId}_${Date.now()}`;
mkdirSync(tmpDir, { recursive: true });
const pdfPath = downloadCor91Pdf(cor91Info.envId, tmpDir);
if (!pdfPath) {
rmSync(tmpDir, { recursive: true, force: true });
return NextResponse.json({ ok: false, error: "Could not download COR9.1 PDF attachment", steps }, { status: 200 });
}
steps.push("Downloaded COR9.1 PDF");
// Step 4: Parse PDF for decision
const decision = parseCor91Decision(pdfPath);
if (decision === "unknown") {
rmSync(tmpDir, { recursive: true, force: true });
return NextResponse.json({
ok: false,
error: `Could not determine CIPC decision from the COR9.1 PDF. The document may not be a standard approval/rejection form.`,
steps
}, { status: 200 });
}
steps.push(`CIPC decision: ${decision}`);
// Step 5: Patch status to bizportal_in_progress + save ref
await patchReg(regId, { status: "bizportal_in_progress", cipc_reference_no: refInfo.ref });
steps.push(`Set status=bizportal_in_progress, cipc_reference_no=${refInfo.ref}`);
// Step 6: Patch decision
const newStatus = decision === "approved" ? "name_approved" : "bizportal_in_progress";
await patchReg(regId, {
status: newStatus,
cipc_decision: decision,
cipc_decided_at: new Date().toISOString()
});
steps.push(`Patched: status=${newStatus}, decision=${decision}`);
// Step 7: Generate PDFs + upload + send client email (approved only)
if (decision === "approved") {
const { letterPdf, resPdf } = generatePdfs(regId, tmpDir);
// Upload all 3 docs in one multipart call
const allPdfs = [pdfPath, letterPdf, resPdf].filter(Boolean);
if (allPdfs.length > 0) {
const fileArgs = allPdfs.map(p => `-F "files=@${p};type=application/pdf"`).join(" ");
const uploadOut = runCmd(
`curl -s -X POST "http://localhost:6001/api/register/${regId}/upload" ` +
`-H "Authorization: Bearer ${PB_TOKEN}" ` +
`${fileArgs} 2>&1`
);
steps.push(`Uploaded ${allPdfs.length} documents to PB`);
console.log("Upload result:", uploadOut.slice(0, 300));
}
// Step 8: Send client email with all 3 PDFs attached
const emailResult = runCmd(
`${PYTHON} "${SCRIPTS}/send_client_email.py" "${regId}" "${pdfPath}" "${letterPdf || ""}" "${resPdf || ""}" 2>&1`
);
steps.push(`Client email: ${emailResult.includes("Email sent") ? "sent ✅" : "failed — check logs"}`);
console.log("Email result:", emailResult);
}
// Cleanup
rmSync(tmpDir, { recursive: true, force: true });
const regAfter = await loadReg(regId);
return NextResponse.json({
ok: true,
decision,
registration: {
id: regId,
reference_name: reg.reference_name,
status_before: reg.status,
status_after: regAfter?.status,
cipc_reference_no: refInfo.ref,
cipc_decision: decision
},
steps,
message: decision === "approved"
? `Name approved! Status → name_approved. Documents saved to PB.`
: `Name rejected by CIPC. Status → bizportal_in_progress. Client needs to propose new names.`,
}, { status: 200 });
}

View file

@ -27,6 +27,8 @@ const ALLOWED = new Set([
"bizportal_done",
"client_requested_changes",
"revert_new_names",
"step3",
"back_to_name_approved",
]);
export async function POST(
@ -129,6 +131,22 @@ export async function POST(
newStatus = "bizportal_done";
break;
case "step3":
// Move to Step 3 after name has been approved by CIPC
if (currentStatus !== "name_approved") {
return NextResponse.json({ error: `Cannot move to Step 3 from status "${currentStatus}". Must be "name_approved".` }, { status: 409 });
}
newStatus = "step3";
break;
case "back_to_name_approved":
// Roll back to name_approved from step3
if (currentStatus !== "step3") {
return NextResponse.json({ error: `Cannot revert from "${currentStatus}". Must be "step3".` }, { status: 409 });
}
newStatus = "name_approved";
break;
case "client_requested_changes":
// Roll back to step 1 if client wants to change something after BizPortal submission
if (currentStatus !== "bizportal_done" && currentStatus !== "bizportal_in_progress") {

View file

@ -16,14 +16,23 @@ export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const perPage = Math.min(Number(searchParams.get("perPage")) || 500, 1000);
const q = (searchParams.get("q") || "").trim();
const byId = searchParams.get("id") || "";
// Fetch all — no fields filter so every column is available
// NOTE: this collection has no `created` field — sort by id (nanoid = time-sortable)
const data = await fetchCollection<RegItem>("ysterbaard_company_registrations", {
const fetchOpts: Record<string, unknown> = {
page: 1,
perPage,
sort: "-id",
});
};
// If fetching by ID, reduce perPage to 1 and add filter
if (byId) {
fetchOpts.perPage = 1;
fetchOpts.filter = `id="${byId}"`;
}
const data = await fetchCollection<RegItem>("ysterbaard_company_registrations", fetchOpts);
let items = data.items;

View file

@ -10,7 +10,6 @@
// 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,
@ -131,24 +130,7 @@ export default async function DashboardPage() {
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>
}
action={null}
/>
<Dashboard {...data} />

View file

@ -4,7 +4,7 @@
import { fetchCollection } from "@/lib/pb";
import type { PBRecord } from "@/lib/pb";
import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";
import PageHeader from "@/components/PageHeader";
import Link from "next/link";
import Step2Actions from "@/components/Step2Actions";
@ -153,6 +153,7 @@ function StatusBanner({ reg, suggestedNames }: { reg: RegMain; suggestedNames: s
export default async function Step2Page({ params }: { params: { id: string } }) {
const reg = await loadReg(params.id);
if (!reg) notFound();
if (reg.status === "step3") redirect(`/registrations/${params.id}/step3`);
const suggestedNames = [reg.proposed_name_1, reg.proposed_name_2, reg.proposed_name_3, reg.proposed_name_4]
.filter(n => n && n.trim());

View file

@ -0,0 +1,143 @@
// app/registrations/[id]/step3/page.tsx
// Step 3 — Client Approval
// Status: step3 = "Awaiting client approval"
"use client";
import { useState, useEffect } from "react";
import PageHeader from "@/components/PageHeader";
import Link from "next/link";
interface RegItem {
id: string;
reference_name: string;
status: string;
cipc_reserved_name: string;
proposed_name_1: string;
[k: string]: unknown;
}
export default function Step3Page({ params }: { params: { id: string } }) {
const [reg, setReg] = useState<RegItem | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState("");
const [msgKind, setMsgKind] = useState<"success" | "error" | "info">("info");
useEffect(() => {
fetch(`/api/register/all?perPage=1&id=${params.id}`)
.then(r => r.json())
.then(d => {
const item = (d.items || []).find((i: RegItem) => i.id === params.id);
setReg(item || null);
setLoading(false);
})
.catch(() => setLoading(false));
}, [params.id]);
async function handleBackToStep2() {
setBusy(true);
setMsg("");
try {
const res = await fetch(`/api/register/${params.id}/status?action=back_to_name_approved`, { method: "POST" });
const data = await res.json();
if (!res.ok) {
setMsgKind("error");
setMsg(`❌ ${data.error || "HTTP " + res.status}`);
} else {
setMsgKind("success");
setMsg("✅ Moved back to Step 2.");
setTimeout(() => { window.location.href = `/registrations/${params.id}/step2`; }, 1200);
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(false);
}
}
if (loading) {
return (
<div className="container-x py-10 text-center text-muted">
Loading…
</div>
);
}
if (!reg) {
return (
<div className="container-x py-10 text-center text-muted">
Registration not found.
</div>
);
}
const approvedName = reg.cipc_reserved_name || reg.proposed_name_1 || "the approved name";
return (
<>
<PageHeader
eyebrow="Workflow · Step 3 of 4"
title={reg.reference_name || reg.id}
subtitle="Awaiting client approval — client must review and sign the registration documents."
action={
<Link
href="/registrations"
className="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 all registrations
</Link>
}
/>
<div className="container-x py-10 space-y-6">
{/* Status banner */}
<div className="bg-purple-500/10 border border-purple-500/30 rounded-lg p-6">
<div className="flex items-start gap-3">
<span className="flex-shrink-0 text-2xl">⏳</span>
<div className="flex-1">
<p className="text-base font-bold text-purple-700">Awaiting client approval</p>
<p className="text-sm text-purple-700/80 mt-1">
Company name <span className="font-bold">{approvedName}</span> has been reserved.
The client must now review and approve the registration documents before we can file with CIPC.
</p>
</div>
</div>
</div>
{/* Action buttons */}
<div className="bg-surface border border-border rounded-lg p-5">
<p className="text-xs uppercase tracking-[0.08em] text-muted font-bold mb-3">Step 3 actions</p>
<div className="flex flex-wrap gap-3">
<button
onClick={handleBackToStep2}
disabled={busy}
className="px-4 py-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 text-yellow-700 text-sm font-semibold hover:bg-yellow-500/20 disabled:opacity-50 transition-colors"
>
{busy ? "…" : "← Back to Step 2"}
</button>
</div>
{msg && (
<div className={`mt-3 px-4 py-3 rounded-md text-sm border ${
msgKind === "success" ? "bg-success/10 border-success/30 text-success" :
msgKind === "error" ? "bg-danger/10 border-danger/30 text-danger" :
"bg-surface-warm border-border text-fg"
}`}>
{msg}
</div>
)}
</div>
{/* Placeholder for Step 3 content — SJ will define */}
<div className="bg-surface border border-border rounded-lg p-8 text-center">
<p className="text-muted text-sm">
Step 3 workflow (document review, client sign-off, filing) — to be built here.
</p>
</div>
</div>
</>
);
}

View file

@ -21,6 +21,7 @@ const STATUS_LABELS: Record<string, string> = {
bizportal_in_progress: "Bizportal submission in progress",
bizportal_done: "Bizportal received application",
name_approved: "Name approved by Bizportal",
step3: "Awaiting client approval",
name_reserved: "Name reserved (workflow done)",
client_requested_changes: "Step 2 — Client requested changes",
resignation_sent: "Step 3 — Resignation Sent",
@ -40,6 +41,7 @@ const STATUS_STYLE: Record<string, string> = {
bizportal_in_progress: "bg-blue-500/10 text-blue-700 border border-blue-500/30",
bizportal_done: "bg-yellow-500/10 text-yellow-700 border border-yellow-500/30",
name_approved: "bg-success/10 text-success border border-success/30",
step3: "bg-purple-500/10 text-purple-700 border border-purple-500/30",
name_reserved: "bg-purple-500/10 text-purple-700 border border-purple-500/30",
client_requested_changes: "bg-yellow-500/10 text-yellow-700 border border-yellow-500/30",
resignation_sent: "bg-purple-100 text-purple-700 border border-purple-300",
@ -58,7 +60,8 @@ const STEP_NUMBER: Record<string, number> = {
bizportal_in_progress: 2,
bizportal_done: 2,
name_approved: 2,
name_reserved: 2,
step3: 3,
name_reserved: 3,
client_requested_changes: 2,
resignation_sent: 3,
appointment_accepted: 4,
@ -448,6 +451,27 @@ export default function RegistrationsPage() {
← Back
</button>
)}
{/* name_approved → Proceed to Step 3 */}
{status === "name_approved" && (
<button
onClick={async () => {
setReopeningId(r.id);
try {
const res = await fetch(`/api/register/${r.id}/status?action=step3`, { method: "POST" });
const data = await res.json();
if (!res.ok) { alert(`❌ ${data.error || res.status}`); return; }
setItems(prev => prev.map(i => i.id === r.id ? { ...i, status: "step3" } : i));
} finally {
setReopeningId(null);
}
}}
disabled={reopeningId === r.id}
title="Move to Step 3"
className="px-2.5 py-1 bg-purple-500/10 border border-purple-500/30 rounded text-xs font-semibold text-purple-700 hover:bg-purple-500/20 transition-colors whitespace-nowrap disabled:opacity-50"
>
{reopeningId === r.id ? "…" : "➡️ Step 3"}
</button>
)}
{/* View Step 2 page — available for all Step 2 statuses */}
{(status === "registered" || status === "bizportal_in_progress" || status === "bizportal_done" || status === "client_requested_changes") && (
<Link
@ -457,6 +481,15 @@ export default function RegistrationsPage() {
Step 2 →
</Link>
)}
{/* View Step 3 page */}
{status === "step3" && (
<Link
href={`/registrations/${r.id}/step3`}
className="px-2.5 py-1 bg-purple-500/10 border border-purple-500/30 rounded text-xs font-semibold text-purple-700 no-underline hover:bg-purple-500/20 transition-colors whitespace-nowrap"
>
Step 3 →
</Link>
)}
{/* Reopen client wizard — works from any non-cancelled state */}
{status !== "cancelled" && (
<button

View file

@ -145,6 +145,38 @@ export default function Step2Actions({ registrationId, status, proposedNames }:
}
}
async function runNameAlreadyApproved() {
setBusy("name_already_approved");
setMsg("");
setMsgKind("info");
try {
const res = await fetch(`/api/register/${registrationId}/name-already-approved`, {
method: "POST",
headers: { "Content-Type": "application/json" },
});
const data = await res.json();
if (!res.ok || !data.ok) {
setMsgKind("error");
setMsg(`❌ ${data.error || "Failed to process."}`);
} else if (data.decision === "approved") {
setMsgKind("success");
setMsg(`✅ Name approved! Documents saved to PB. Reloading…`);
setTimeout(() => window.location.reload(), 3000);
} else if (data.decision === "rejected") {
setMsgKind("warn");
setMsg(`⚠ Name rejected by CIPC. Client needs to propose new names.`);
} else {
setMsgKind("info");
setMsg(data.message || "Done.");
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(null);
}
}
const isRegistered = status === "registered";
const isBizportalInProgress = status === "bizportal_in_progress";
const isBizportalDone = status === "bizportal_done";
@ -170,6 +202,14 @@ export default function Step2Actions({ registrationId, status, proposedNames }:
>
{busy === "apply_name_only" ? "⏳ Running…" : "🪪 Name Reservation Only"}
</button>
<button
onClick={runNameAlreadyApproved}
disabled={busy === "name_already_approved"}
className="px-4 py-2 rounded-md border border-green-500/30 bg-green-500/10 text-green-700 text-sm font-bold hover:bg-green-500/20 disabled:opacity-50 transition-colors"
title="Name was already approved on BizPortal by the company secretary — search DeltaDet for existing CIPC emails and complete the workflow"
>
{busy === "name_already_approved" ? "⏳ Searching emails…" : "✅ Name Already Approved"}
</button>
<button
onClick={() => runAction("back_to_step1")}
disabled={busy === "back_to_step1"}
@ -244,6 +284,13 @@ export default function Step2Actions({ registrationId, status, proposedNames }:
<span className="px-3 py-2 rounded-md border border-success/30 bg-success/10 text-success text-sm font-bold">
✓ Name approved by Bizportal
</span>
<button
onClick={() => runAction("step3")}
disabled={busy === "step3"}
className="px-4 py-2 rounded-md border border-purple-500/30 bg-purple-500/10 text-purple-700 text-sm font-bold hover:bg-purple-500/20 disabled:opacity-50 transition-colors"
>
{busy === "step3" ? "⏳ Moving…" : "➡️ Proceed to Step 3"}
</button>
<button
onClick={() => setShowRevertConfirm(true)}
disabled={busy === "revert_new_names"}