"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; // Manager's advance decision (issue #92) — absolute amount. Prefills the FIRST // payment's amount field; ignored once any payment has been recorded. suggestedAdvancePayment?: number | null; } // Today's date as a local yyyy-mm-dd string (for default + max) function todayLocal(): string { const d = new Date(); const off = d.getTimezoneOffset(); return new Date(d.getTime() - off * 60_000).toISOString().slice(0, 10); } export function PaymentActions({ poId, poStatus, totalAmount = 0, paidAmount = 0, suggestedAdvancePayment = null, }: Props) { const router = useRouter(); const remaining = totalAmount - paidAmount; // Prefill the first payment with the Manager's advance, when it's a genuine // partial of the (untouched) total. Nothing paid yet ⇒ first payment; a full // (>= total) advance leaves the field blank so "Confirm Full Payment" is used. const advancePrefill = paidAmount === 0 && suggestedAdvancePayment != null && suggestedAdvancePayment > 0 && suggestedAdvancePayment < remaining ? String(suggestedAdvancePayment) : ""; const [ref, setRef] = useState(""); const [amount, setAmount] = useState(advancePrefill); const [paymentDate, setPaymentDate] = useState(todayLocal()); const [pending, setPending] = useState(false); const [error, setError] = useState(""); const today = todayLocal(); 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; } if (!paymentDate) { setError("Payment date is required."); return; } if (paymentDate > today) { setError("Payment date cannot be in the future."); 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, paymentDate }); if ("error" in result) { setError(result.error); setPending(false); } else { setPending(false); router.refresh(); } } if (poStatus === "MGR_APPROVED") { return (
{error && {error}}
); } if ( poStatus === "SENT_FOR_PAYMENT" || poStatus === "PARTIALLY_PAID" || poStatus === "PARTIALLY_CLOSED" ) { const parsedAmount = parseFloat(amount); const isPartialPayment = !isNaN(parsedAmount) && parsedAmount > 0 && parsedAmount < remaining; return (
handleMarkPaid(e)} className="flex flex-col gap-2 w-full sm:w-auto" >
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" /> setPaymentDate(e.target.value)} className="w-full sm:w-40 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" /> 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" />
{advancePrefill && ( Manager set an advance of {Number(suggestedAdvancePayment).toFixed(2)} — prefilled below; adjust if needed. )} {error && {error}}
{isPartialPayment && ( )}
); } return null; }