Workflow-App/Workflow/components/Step2Actions.tsx

514 lines
23 KiB
TypeScript
Raw Permalink Normal View History

// components/Step2Actions.tsx
// Action bar for the Step 2 page — Back to Step 1, Apply (BizPortal), Cancel.
"use client";
import { useState } from "react";
import Link from "next/link";
interface Props {
registrationId: string;
status: string;
proposedNames: string[];
}
export default function Step2Actions({ registrationId, status, proposedNames }: Props) {
const [busy, setBusy] = useState<string | null>(null);
const [msg, setMsg] = useState<string>("");
const [msgKind, setMsgKind] = useState<"info" | "success" | "error" | "warn">("info");
const [showConfirm, setShowConfirm] = useState(false);
const [showNameOnlyConfirm, setShowNameOnlyConfirm] = useState(false);
const [bizportalResult, setBizportalResult] = useState<any>(null);
const [checkMailResult, setCheckMailResult] = useState<any>(null);
const [showRevertConfirm, setShowRevertConfirm] = useState(false);
async function runAction(action: string, confirmText?: string) {
if (confirmText && !confirm(confirmText)) return;
setBusy(action);
setMsg("");
setMsgKind("info");
try {
const res = await fetch(`/api/register/${registrationId}/status?action=${action}`, { method: "POST" });
const data = await res.json();
if (!res.ok) {
setMsgKind("error");
setMsg(`❌ ${data.error || `HTTP ${res.status}`}`);
return;
}
if (action === "back_to_step1" || action === "bizportal_done") {
window.location.reload();
} else if (action === "cancel" || action === "client_requested_changes") {
window.location.href = "/registrations";
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(null);
}
}
async function runBizportalApply(nameOnly: boolean = false) {
setShowConfirm(false);
setShowNameOnlyConfirm(false);
setBusy(nameOnly ? "apply_name_only" : "apply");
setMsg("");
setMsgKind("info");
setBizportalResult(null);
try {
const res = await fetch(`/api/register/${registrationId}/apply-bizportal`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nameOnly }),
});
const data = await res.json();
setBizportalResult(data);
if (data.ok && data.status === "bizportal_done") {
setMsgKind("success");
setMsg(`✅ ${data.message || "BizPortal name reservation confirmed. Now wait for the approval letter."}`);
setTimeout(() => window.location.reload(), 2500);
} else if (data.ok) {
setMsgKind("warn");
setMsg(`⚠ ${data.message || "Submitted but status not auto-confirmed. Check BizPortal."}`);
} else {
setMsgKind("error");
setMsg(`❌ ${data.error || data.message || "Submission failed."}`);
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(null);
}
}
async function handleApplyClick() {
setShowConfirm(true);
}
async function handleNameOnlyClick() {
setShowNameOnlyConfirm(true);
}
async function runRevertNewNames() {
setShowRevertConfirm(false);
setBusy("revert_new_names");
setMsg("");
setMsgKind("info");
try {
const res = await fetch(`/api/register/${registrationId}/status?action=revert_new_names`, { method: "POST" });
const data = await res.json();
if (!res.ok) {
setMsgKind("error");
setMsg(`❌ ${data.error || `HTTP ${res.status}`}`);
} else {
setMsgKind("success");
setMsg(`✅ CIPC fields cleared. Registration reverted to Step 1 — client can submit 4 new proposed names.`);
setTimeout(() => window.location.href = "/registrations", 2500);
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(null);
}
}
async function runCheckMail() {
setMsg("");
setMsgKind("info");
setCheckMailResult(null);
try {
const res = await fetch(`/api/register/${registrationId}/check-cipc-mail`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const data = await res.json();
setCheckMailResult(data);
if (!res.ok) {
setMsgKind("error");
setMsg(`❌ ${data.error || `HTTP ${res.status}`}`);
} else if (data.firstScanner?.found || data.secondScanner?.found) {
setMsgKind("success");
setMsg(`✅ ${data.summary}`);
setTimeout(() => window.location.reload(), 3000);
} else {
setMsgKind("info");
setMsg(`ℹ️ ${data.summary}`);
}
} catch (err: unknown) {
setMsgKind("error");
setMsg(`❌ ${err instanceof Error ? err.message : "Error"}`);
} finally {
setBusy(null);
}
}
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";
return (
<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 2 actions</p>
<div className="flex flex-wrap items-center gap-3">
{isRegistered && (
<>
<button
onClick={handleApplyClick}
disabled={busy === "apply"}
className="px-4 py-2 rounded-md border border-accent/30 bg-accent/10 text-accent text-sm font-bold hover:bg-accent/20 disabled:opacity-50 transition-colors"
>
{busy === "apply" ? "⏳ Running BizPortal…" : "🌐 Apply Name Reservation on BizPortal"}
</button>
<button
onClick={handleNameOnlyClick}
disabled={busy === "apply_name_only"}
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"
title="Apply only for a CIPC name reservation — workflow ends once CIPC approves. No follow-up Steps 3/4."
>
{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"}
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_step1" ? "…" : "← Back to Step 1"}
</button>
<button
disabled
title="Coming soon"
className="px-4 py-2 rounded-md border border-border bg-surface-warm text-muted text-sm font-semibold cursor-not-allowed opacity-60"
>
Step 3: Resignation →
</button>
</>
)}
{isBizportalInProgress && (
<>
<span className="px-3 py-2 rounded-md border border-blue-500/30 bg-blue-500/10 text-blue-700 text-sm font-semibold">
⏳ Bizportal submission in progress
</span>
<button
onClick={runCheckMail}
disabled={busy === "check_mail"}
className="px-4 py-2 rounded-md border border-amber-500/30 bg-amber-500/10 text-amber-700 text-sm font-bold hover:bg-amber-500/20 disabled:opacity-50 transition-colors"
title="Check deltadet mailbox for new CIPC emails now"
>
{busy === "check_mail" ? "⏳ Checking mail…" : "📧 Check Mail"}
</button>
<button
onClick={() => setShowRevertConfirm(true)}
disabled={busy === "revert_new_names"}
className="px-4 py-2 rounded-md border border-red-500/30 bg-red-500/10 text-red-700 text-sm font-bold hover:bg-red-500/20 disabled:opacity-50 transition-colors"
title="CIPC rejected the name — revert to Step 1 so client can submit 4 new proposed names"
>
{busy === "revert_new_names" ? "⏳ Reverting…" : "🔄 Revert to Step 1 (New Names)"}
</button>
</>
)}
{isBizportalDone && (
<>
<span className="px-3 py-2 rounded-md border border-success/30 bg-success/10 text-success text-sm font-bold">
✓ BizPortal Done — awaiting approval letter
</span>
<button
onClick={runCheckMail}
disabled={busy === "check_mail"}
className="px-4 py-2 rounded-md border border-amber-500/30 bg-amber-500/10 text-amber-700 text-sm font-bold hover:bg-amber-500/20 disabled:opacity-50 transition-colors"
title="Check deltadet mailbox for CIPC decision email now"
>
{busy === "check_mail" ? "⏳ Checking mail…" : "📧 Check Mail"}
</button>
<button
onClick={() => runAction("client_requested_changes", "Client wants to change something — move back to step 1?")}
disabled={busy === "client_requested_changes"}
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"
>
← Client Requested Changes
</button>
<button
onClick={() => setShowRevertConfirm(true)}
disabled={busy === "revert_new_names"}
className="px-4 py-2 rounded-md border border-red-500/30 bg-red-500/10 text-red-700 text-sm font-bold hover:bg-red-500/20 disabled:opacity-50 transition-colors"
title="CIPC rejected the name — revert to Step 1 so client can submit 4 new proposed names"
>
{busy === "revert_new_names" ? "⏳ Reverting…" : "🔄 Revert to Step 1 (New Names)"}
</button>
</>
)}
{status === "name_approved" && (
<>
<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"}
className="px-4 py-2 rounded-md border border-red-500/30 bg-red-500/10 text-red-700 text-sm font-bold hover:bg-red-500/20 disabled:opacity-50 transition-colors"
title="Name was approved but client wants different names — revert to Step 1 for new proposals"
>
{busy === "revert_new_names" ? "⏳ Reverting…" : "🔄 Revert to Step 1 (New Names)"}
</button>
</>
)}
{status === "name_reserved" && (
<span className="px-3 py-2 rounded-md border border-purple-500/30 bg-purple-500/10 text-purple-700 text-sm font-bold">
✓ Name reserved — workflow complete
</span>
)}
{status === "cancelled" && (
<span className="px-4 py-2 rounded-md border border-danger/30 bg-danger/10 text-danger text-sm font-semibold">
⚠ Cancelled
</span>
)}
<Link
href="/registrations"
className="px-4 py-2 rounded-md border border-border bg-surface text-fg text-sm font-semibold no-underline hover:bg-surface-warm transition-colors"
>
← Back to list
</Link>
</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 === "warn" ? "bg-yellow-500/10 border-yellow-500/30 text-yellow-700" :
msgKind === "error" ? "bg-danger/10 border-danger/30 text-danger" :
"bg-surface-warm border-border text-fg"
}`}>
{msg}
</div>
)}
{bizportalResult && (
<div className="mt-3 px-4 py-3 rounded-md text-sm bg-surface-warm border border-border">
<p className="font-semibold mb-2">BizPortal flow details</p>
{bizportalResult.balance_before !== null && bizportalResult.balance_before !== undefined && (
<p className="text-xs text-muted">Balance before: <span className="font-mono text-fg">R {bizportalResult.balance_before.toFixed(2)}</span></p>
)}
{bizportalResult.balance_after !== null && bizportalResult.balance_after !== undefined && (
<p className="text-xs text-muted">Balance after: <span className="font-mono text-fg">R {bizportalResult.balance_after.toFixed(2)}</span></p>
)}
{bizportalResult.deduction !== null && bizportalResult.deduction !== undefined && (
<p className="text-xs text-muted">Deduction: <span className="font-mono text-fg">R {bizportalResult.deduction.toFixed(2)}</span></p>
)}
{bizportalResult.log && (
<details className="mt-2">
<summary className="text-xs text-muted cursor-pointer">Show log</summary>
<pre className="text-[10px] text-muted mt-2 whitespace-pre-wrap font-mono">
{bizportalResult.log.join("\n")}
</pre>
</details>
)}
</div>
)}
{checkMailResult && (
<div className="mt-3 px-4 py-3 rounded-md text-sm bg-surface-warm border border-border">
<p className="font-semibold mb-2">📧 Check Mail result</p>
<p className="text-xs text-muted mb-1">
Reference: <span className="font-mono text-fg">{checkMailResult.registration?.reference_name}</span>
{" | "}
Status: <span className="font-mono text-fg">{checkMailResult.registration?.status_after || checkMailResult.registration?.status_before}</span>
</p>
{checkMailResult.registration?.cipc_reference_no && (
<p className="text-xs text-muted">
CIPC Ref: <span className="font-mono text-fg">{checkMailResult.registration.cipc_reference_no}</span>
</p>
)}
{checkMailResult.registration?.cipc_decision && (
<p className="text-xs text-muted">
Decision: <span className="font-mono text-fg">{checkMailResult.registration.cipc_decision}</span>
</p>
)}
{checkMailResult.firstScanner?.found && (
<p className="text-xs text-amber-700 mt-1">✓ First email found and processed</p>
)}
{checkMailResult.secondScanner?.found && (
<p className="text-xs text-amber-700 mt-1">✓ Second email found — client notified</p>
)}
{!checkMailResult.firstScanner?.found && !checkMailResult.secondScanner?.found && (
<p className="text-xs text-muted mt-1">No new emails found for this registration.</p>
)}
</div>
)}
{/* Confirmation popup */}
{showConfirm && (
<div
className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4"
onClick={() => setShowConfirm(false)}
>
<div
className="bg-surface border border-border rounded-lg p-6 max-w-md w-full shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<p className="text-xs uppercase tracking-[0.08em] text-accent font-bold mb-2">Apply Name Reservation</p>
<h3 className="text-lg font-bold text-fg mb-3">Apply on BizPortal?</h3>
<p className="text-sm text-muted mb-4">
I will open BizPortal, login if needed, fill the 4 name boxes in the order supplied, and click Continue twice.
The second Continue <strong>costs R50</strong> from your BizPortal balance.
</p>
<p className="text-sm text-danger font-semibold mb-5">
⚠ Before this runs I'll record your BizPortal balance, after the run I'll check it again. If R50 was deducted, I'll auto-mark <strong>BizPortal Done</strong> (green) so you know to wait for the approval letter.
</p>
<p className="text-sm text-fg mb-1 font-semibold">Names to enter:</p>
<ol className="text-sm text-fg list-decimal list-inside mb-5 space-y-1">
{proposedNames.length === 0 && <li className="text-muted italic">No names provided</li>}
{proposedNames.map((n, i) => (
<li key={i} className="font-mono">{n}</li>
))}
</ol>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowConfirm(false)}
className="px-4 py-2 rounded-md border border-border bg-surface text-fg text-sm font-semibold hover:bg-surface-warm transition-colors"
>
Cancel
</button>
<button
onClick={() => runBizportalApply(false)}
disabled={busy === "apply"}
className="px-4 py-2 rounded-md border border-accent bg-accent text-accent-on text-sm font-bold hover:opacity-90 transition-opacity disabled:opacity-50"
>
{busy === "apply" ? "Running…" : "OK"}
</button>
</div>
</div>
</div>
)}
{/* Name Reservation Only confirmation popup */}
{showNameOnlyConfirm && (
<div
className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4"
onClick={() => setShowNameOnlyConfirm(false)}
>
<div
className="bg-surface border border-border rounded-lg p-6 max-w-md w-full shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<p className="text-xs uppercase tracking-[0.08em] text-purple-700 font-bold mb-2">Name Reservation Only</p>
<h3 className="text-lg font-bold text-fg mb-3">Apply just for the name reservation?</h3>
<p className="text-sm text-muted mb-4">
I will lodge a CIPC Name Reservation on BizPortal — <strong>R50</strong> is debited from your prepaid balance.
Once CIPC approves, this registration is <strong>done</strong>. No Steps 3/4.
</p>
<p className="text-sm text-fg mb-1 font-semibold">Names to enter:</p>
<ol className="text-sm text-fg list-decimal list-inside mb-5 space-y-1">
{proposedNames.length === 0 && <li className="text-muted italic">No names provided</li>}
{proposedNames.map((n, i) => (
<li key={i} className="font-mono">{n}</li>
))}
</ol>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowNameOnlyConfirm(false)}
className="px-4 py-2 rounded-md border border-border bg-surface text-fg text-sm font-semibold hover:bg-surface-warm transition-colors"
>
Cancel
</button>
<button
onClick={() => runBizportalApply(true)}
disabled={busy === "apply_name_only"}
className="px-4 py-2 rounded-md border border-purple-500 bg-purple-500 text-white text-sm font-bold hover:opacity-90 transition-opacity disabled:opacity-50"
>
{busy === "apply_name_only" ? "Running…" : "OK — Name Reservation Only"}
</button>
</div>
</div>
</div>
)}
{/* Revert to Step 1 (New Names) confirmation popup */}
{showRevertConfirm && (
<div
className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4"
onClick={() => setShowRevertConfirm(false)}
>
<div
className="bg-surface border border-border rounded-lg p-6 max-w-md w-full shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<p className="text-xs uppercase tracking-[0.08em] text-red-700 font-bold mb-2">CIPC Name Rejected</p>
<h3 className="text-lg font-bold text-fg mb-3">Revert to Step 1 — New Names?</h3>
<p className="text-sm text-muted mb-4">
This will <strong>clear the CIPC reference number and decision</strong>, and move the registration back to Step 1.
</p>
<p className="text-sm text-muted mb-1">
The client will receive the wizard link with their saved data and can submit <strong>4 new proposed names</strong>.
</p>
<p className="text-sm text-red-700 font-semibold mb-5">
This cannot be undone.
</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setShowRevertConfirm(false)}
className="px-4 py-2 rounded-md border border-border bg-surface text-fg text-sm font-semibold hover:bg-surface-warm transition-colors"
>
Cancel
</button>
<button
onClick={runRevertNewNames}
disabled={busy === "revert_new_names"}
className="px-4 py-2 rounded-md border border-red-500 bg-red-500 text-white text-sm font-bold hover:opacity-90 transition-opacity disabled:opacity-50"
>
{busy === "revert_new_names" ? "Reverting…" : "Revert to Step 1"}
</button>
</div>
</div>
</div>
)}
</div>
);
}