158 lines
5.6 KiB
TypeScript
158 lines
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>
|
||
|
|
);
|
||
|
|
}
|