Workflow-App/Workflow/components/ResolutionActions.tsx
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
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.
2026-10-18 15:21:45 +02:00

158 lines
No EOL
5.6 KiB
TypeScript

// components/ResolutionActions.tsx
// Phase 2B — action bar for the resolution detail page.
//
// Status-aware actions:
// - draft → Send for signature (disabled — Phase 6)
// - draft → Cancel (re-enables)
// - pending_signatures → Cancel
// - pending_signatures → Resend (uses existing /api/resolutions/[id]/resend)
// - signed → (no destructive actions; status is final)
// - cancelled → (locked — status is final)
//
// Cancel + Resend are wired to the existing endpoints (Phase 2A).
// "Send for signature" is disabled until Phase 6 lands.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import type { NewResolutionStatus } from "@/lib/types";
interface Props {
resolutionId: string;
status: NewResolutionStatus;
}
export default function ResolutionActions({ resolutionId, status }: Props) {
const router = useRouter();
const [acting, setActing] = useState<string | null>(null);
const [feedback, setFeedback] = useState<{ kind: "ok" | "err"; msg: string } | null>(null);
const canSendForSignature = status === "draft";
const canCancel = status === "draft" || status === "pending_signatures";
const canResend = status === "pending_signatures";
const handleCancel = async () => {
if (!confirm("Cancel this resolution? Pending signers will be marked expired.")) return;
setActing("cancel");
setFeedback(null);
try {
const res = await fetch(`/api/resolutions/${resolutionId}/cancel`, { method: "POST" });
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j.error || `HTTP ${res.status}`);
}
setFeedback({ kind: "ok", msg: "Resolution cancelled." });
router.refresh();
} catch (err) {
setFeedback({ kind: "err", msg: err instanceof Error ? err.message : "Cancel failed" });
} finally {
setActing(null);
}
};
const handleResend = async () => {
setActing("resend");
setFeedback(null);
try {
const res = await fetch(`/api/resolutions/${resolutionId}/resend`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j.error || `HTTP ${res.status}`);
}
const j = await res.json();
setFeedback({ kind: "ok", msg: `Re-sent to ${j.resent} pending signer(s).` });
router.refresh();
} catch (err) {
setFeedback({ kind: "err", msg: err instanceof Error ? err.message : "Resend failed" });
} finally {
setActing(null);
}
};
const handleSendForSignature = async () => {
setActing("send");
setFeedback(null);
try {
const res = await fetch(`/api/resolutions/${resolutionId}/attendees`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ send_for_signature: true }),
});
const j = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(j.error || `HTTP ${res.status}`);
}
const sentCount =
j.emails_sent?.filter((e: { ok: boolean }) => e.ok).length ?? 0;
const failCount =
j.emails_sent?.filter((e: { ok: boolean }) => !e.ok).length ?? 0;
const msg =
`Sent for signature — ${j.signers_created} signers created` +
(sentCount > 0 ? `, ${sentCount} emails sent` : "") +
(failCount > 0 ? `, ${failCount} email(s) failed (see server logs)` : "");
setFeedback({ kind: failCount > 0 ? "err" : "ok", msg });
router.refresh();
} catch (err) {
setFeedback({
kind: "err",
msg: err instanceof Error ? err.message : "Send failed",
});
} finally {
setActing(null);
}
};
// No actions for terminal states
if (status === "signed" || status === "cancelled") {
return (
<div className="bg-surface border border-border rounded-lg p-4 text-sm text-muted">
Status is final — no actions available.
</div>
);
}
return (
<div className="bg-surface border border-border rounded-lg p-4 space-y-3">
<div className="flex flex-wrap items-center gap-3">
{canSendForSignature && (
<button
onClick={handleSendForSignature}
disabled={acting !== null}
className="px-4 py-2 bg-accent text-accent-on rounded font-semibold text-sm hover:opacity-90 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
{acting === "send" ? "Sending…" : "Send for signature"}
</button>
)}
{canResend && (
<button
onClick={handleResend}
disabled={acting !== null}
className="px-4 py-2 bg-surface border border-border text-fg rounded font-semibold text-sm hover:bg-surface-warm transition-colors disabled:opacity-50"
>
{acting === "resend" ? "Resending…" : "Resend to signers"}
</button>
)}
{canCancel && (
<button
onClick={handleCancel}
disabled={acting !== null}
className="px-4 py-2 bg-surface border border-danger/40 text-danger rounded font-semibold text-sm hover:bg-danger/5 transition-colors disabled:opacity-50"
>
{acting === "cancel" ? "Cancelling…" : "Cancel resolution"}
</button>
)}
</div>
{feedback && (
<p className={`text-sm font-semibold ${feedback.kind === "ok" ? "text-success" : "text-danger"}`}>
{feedback.msg}
</p>
)}
</div>
);
}