pelagia-portal/App/components/po/cancel-po-controls.tsx
Hardik 78478c4e17 fix(po): make cancel buttons red & visible (use defined danger tokens)
The theme only defines danger / danger-50 / danger-100 / danger-700, so
bg-danger-600 / danger-500 / danger-200 generated no CSS — the modal confirm
button was white-on-nothing (invisible) and the header button wasn't red.

- Header "Cancel PO" button → solid red (bg-danger text-white hover:bg-danger-700)
- Modal "Cancel this PO" confirm button → bg-danger (now visible)
- Inputs/asterisk/banner border → defined danger tokens

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:37:25 +05:30

158 lines
5.9 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { cancelPo, supersedePo } from "@/app/(portal)/po/[id]/actions";
// ── Cancel PO button + confirmation modal ──────────────────────────────────────
// The manager must type the word "cancel" and provide a reason before the action
// is enabled — a deliberate friction step for an irreversible, terminal action.
export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: string }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [reason, setReason] = useState("");
const [confirmText, setConfirmText] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
const confirmed = confirmText.trim().toLowerCase() === "cancel";
const canSubmit = confirmed && reason.trim().length > 0 && !pending;
function close() {
if (pending) return;
setOpen(false);
setReason("");
setConfirmText("");
setError("");
}
async function handleCancel() {
if (!canSubmit) return;
setPending(true);
setError("");
const result = await cancelPo({ poId, reason: reason.trim() });
if ("error" in result) {
setError(result.error);
setPending(false);
} else {
setPending(false);
setOpen(false);
router.refresh();
}
}
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-lg bg-danger px-3 py-2 text-sm font-semibold text-white hover:bg-danger-700 transition-colors"
>
Cancel PO
</button>
{open && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4" onClick={close}>
<div
className="w-full max-w-md rounded-xl bg-white p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-lg font-semibold text-neutral-900">Cancel {poNumber}?</h2>
<p className="mt-1.5 text-sm text-neutral-600">
This marks the purchase order as <strong>cancelled</strong> and removes its value from
all spend trackers and graphs. This cannot be undone.
</p>
<label className="mt-4 block text-xs font-medium text-neutral-700">
Reason for cancellation <span className="text-danger">*</span>
</label>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
autoFocus
placeholder="e.g. Duplicate order — superseded by a corrected PO"
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-danger focus:outline-none focus:ring-2 focus:ring-danger/20"
/>
<label className="mt-3 block text-xs font-medium text-neutral-700">
Type <span className="font-mono font-semibold">cancel</span> to confirm
</label>
<input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder="cancel"
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm font-mono focus:border-danger focus:outline-none focus:ring-2 focus:ring-danger/20"
/>
{error && <p className="mt-3 text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
<div className="mt-5 flex justify-end gap-3">
<button
type="button"
onClick={close}
disabled={pending}
className="rounded-lg border border-neutral-300 px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 disabled:opacity-60"
>
Keep PO
</button>
<button
type="button"
onClick={handleCancel}
disabled={!canSubmit}
className="rounded-lg bg-danger px-4 py-2 text-sm font-semibold text-white hover:bg-danger-700 disabled:opacity-50"
>
{pending ? "Cancelling…" : "Cancel this PO"}
</button>
</div>
</div>
</div>
)}
</>
);
}
// ── Supersede: link a cancelled PO to the existing PO that replaces it ──────────
export function SupersedeForm({ poId }: { poId: string }) {
const router = useRouter();
const [value, setValue] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
async function handleLink(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!value.trim()) return;
setPending(true);
setError("");
const result = await supersedePo({ poId, replacementPoNumber: value.trim() });
if ("error" in result) {
setError(result.error);
setPending(false);
} else {
setPending(false);
setValue("");
router.refresh();
}
}
return (
<form onSubmit={handleLink} className="mt-2 flex flex-wrap items-start gap-2">
<input
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder="Replacement PO number, e.g. PMS/HNR1/9001/2026-27"
className="min-w-[260px] flex-1 rounded-lg border border-neutral-300 px-3 py-2 text-sm font-mono focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
/>
<button
type="submit"
disabled={pending || !value.trim()}
className="rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm font-medium text-primary-700 hover:bg-primary-100 disabled:opacity-50"
>
{pending ? "Linking…" : "Link replacement"}
</button>
{error && <p className="w-full text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
</form>
);
}