pelagia-portal/App/app/(portal)/payments/payment-actions.tsx
Hardik c92f136b09 fix(ui): reset pending state on success for all save/confirm buttons
Parent button components (EditVendorButton, EditAccountButton, etc.) stay
mounted even when their AdminDialog closes — so pending was never cleared
on success, causing buttons to show 'Saving...' on the next open. The
payment confirm button (no dialog) was stuck in 'Confirming...' until the
page re-render eventually unmounted it. Added setPending(false) before
setOpen/router.refresh in every success path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:36:29 +05:30

127 lines
4.5 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { processPayment, markPaid } from "./actions";
import type { POStatus } from "@prisma/client";
interface Props {
poId: string;
poStatus: POStatus;
totalAmount?: number;
paidAmount?: number;
}
export function PaymentActions({ poId, poStatus, totalAmount = 0, paidAmount = 0 }: Props) {
const router = useRouter();
const [ref, setRef] = useState("");
const [amount, setAmount] = useState<string>("");
const [pending, setPending] = useState(false);
const [error, setError] = useState("");
const remaining = totalAmount - paidAmount;
async function handleProcessPayment() {
setPending(true);
setError("");
const result = await processPayment({ poId });
if ("error" in result) { setError(result.error); setPending(false); }
else { setPending(false); router.refresh(); }
}
async function handleMarkPaid(e: React.FormEvent, forceFullPayment = false) {
e.preventDefault();
if (!ref.trim()) { setError("Payment reference is required."); return; }
const paymentAmount = forceFullPayment ? remaining : (parseFloat(amount) || undefined);
if (paymentAmount !== undefined && paymentAmount <= 0) {
setError("Payment amount must be greater than 0.");
return;
}
if (paymentAmount !== undefined && paymentAmount > remaining) {
setError(`Payment amount cannot exceed the remaining balance of ${remaining.toFixed(2)}.`);
return;
}
setPending(true);
setError("");
const result = await markPaid({ poId, paymentRef: ref, paymentAmount });
if ("error" in result) { setError(result.error); setPending(false); }
else { setPending(false); router.refresh(); }
}
if (poStatus === "MGR_APPROVED") {
return (
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
{error && <span className="text-xs text-danger-700">{error}</span>}
<button
onClick={handleProcessPayment}
disabled={pending}
className="w-full sm:w-auto rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-700 disabled:opacity-60 transition-colors"
>
{pending ? "Processing…" : "Start Payment Processing"}
</button>
</div>
);
}
if (
poStatus === "SENT_FOR_PAYMENT" ||
poStatus === "PARTIALLY_PAID" ||
poStatus === "PARTIALLY_CLOSED"
) {
const parsedAmount = parseFloat(amount);
const isPartialPayment =
!isNaN(parsedAmount) && parsedAmount > 0 && parsedAmount < remaining;
return (
<form
onSubmit={(e) => handleMarkPaid(e)}
className="flex flex-col gap-2 w-full sm:w-auto"
>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<input
type="text"
placeholder="Payment reference / transaction ID"
value={ref}
onChange={(e) => setRef(e.target.value)}
className="flex-1 rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
/>
<input
type="number"
placeholder={`Amount (max ${remaining.toFixed(2)})`}
value={amount}
onChange={(e) => setAmount(e.target.value)}
min={0.01}
max={remaining}
step="0.01"
className="w-full sm:w-36 rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"
/>
</div>
{error && <span className="text-xs text-danger-700">{error}</span>}
<div className="flex gap-2 justify-end">
{isPartialPayment && (
<button
type="submit"
disabled={pending}
className="rounded-lg bg-warning-600 px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
>
{pending ? "Confirming…" : "Confirm Partial Payment"}
</button>
)}
<button
type="button"
disabled={pending}
onClick={(e) => handleMarkPaid(e as unknown as React.FormEvent, true)}
className="rounded-lg bg-success px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-60 transition-opacity"
>
{pending ? "Confirming…" : "Confirm Full Payment"}
</button>
</div>
</form>
);
}
return null;
}