WORKFLOW-APP/components/Step2Actions.tsx

513 lines
23 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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>
);
}