Files
setrip/features/payout/components/payout-review-card.tsx
T

272 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { markPayoutPaidAction } from "@/features/payout/actions";
import { formatRupiah } from "@/lib/utils";
type PayoutStatus = "HELD" | "RELEASED" | "PAID" | "CANCELLED";
export type PayoutCardData = {
id: string;
amount: number;
currency: string;
status: PayoutStatus;
heldUntil: Date;
releasedAt: Date | null;
paidAt: Date | null;
cancelledAt: Date | null;
bankName: string | null;
bankAccountNumber: string | null;
bankAccountName: string | null;
adminNote: string | null;
createdAt: Date;
trip: { id: string; title: string; date: Date; endDate: Date | null; status: string };
organizer: { id: string; name: string; email: string };
booking: {
id: string;
amount: number;
status: string;
user: { id: string; name: string; email: string };
};
processedBy: { id: string; name: string; email: string } | null;
};
function formatDate(d: Date | null | string): string {
if (!d) return "—";
return new Date(d).toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
function formatDay(d: Date | null | string): string {
if (!d) return "—";
return new Date(d).toLocaleDateString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
});
}
export function PayoutReviewCard({ payout }: { payout: PayoutCardData }) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function submit() {
setError("");
setLoading(true);
const fd = new FormData();
fd.set("payoutId", payout.id);
fd.set("adminNote", note);
const result = await markPayoutPaidAction(fd);
setLoading(false);
if (result.error) {
setError(result.error);
return;
}
setOpen(false);
setNote("");
router.refresh();
}
const bankAvailable =
payout.bankName && payout.bankAccountNumber && payout.bankAccountName;
return (
<article className="rounded-2xl border border-neutral-200 bg-white p-5 shadow-sm sm:p-6">
<header className="flex flex-wrap items-start justify-between gap-3 border-b border-neutral-100 pb-4">
<div className="min-w-0">
<h3 className="truncate text-base font-bold text-neutral-900">
{payout.trip.title}
</h3>
<p className="mt-0.5 text-xs text-neutral-500">
Booking {payout.booking.user.name} · trip{" "}
{formatDay(payout.trip.date)}
{" · "}
<span className="font-mono">{payout.id.slice(0, 8)}</span>
</p>
</div>
<StatusPill status={payout.status} />
</header>
<div className="mt-4 grid gap-4 sm:grid-cols-2">
<Field
label="Organizer"
value={`${payout.organizer.name} · ${payout.organizer.email}`}
/>
<Field
label="Nominal payout"
value={`${formatRupiah(payout.amount)} ${payout.currency !== "IDR" ? `(${payout.currency})` : ""}`}
highlight
/>
<Field label="Booking" value={`${formatRupiah(payout.booking.amount)} · ${payout.booking.status}`} />
<Field
label="Hold sampai"
value={formatDay(payout.heldUntil)}
/>
{payout.releasedAt && (
<Field label="Direlease" value={formatDate(payout.releasedAt)} />
)}
{payout.paidAt && (
<Field label="Dibayar" value={formatDate(payout.paidAt)} />
)}
{payout.cancelledAt && (
<Field label="Dibatalkan" value={formatDate(payout.cancelledAt)} />
)}
</div>
<div className="mt-4 rounded-xl bg-neutral-50 p-3 text-sm">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-neutral-500">
Rekening tujuan transfer
</p>
{bankAvailable ? (
<div className="space-y-0.5 text-neutral-800">
<p className="font-semibold">{payout.bankName}</p>
<p className="font-mono text-sm">{payout.bankAccountNumber}</p>
<p className="text-xs text-neutral-600">
a/n {payout.bankAccountName}
</p>
</div>
) : (
<p className="text-amber-700">
Organizer belum menyelesaikan verifikasi (KYC) tidak ada rekening
snapshot. Hubungi organizer untuk konfirmasi rekening sebelum transfer.
</p>
)}
</div>
{payout.adminNote && (
<div className="mt-3 rounded-xl bg-blue-50 p-3 text-sm text-blue-800">
<p className="mb-1 text-xs font-semibold uppercase tracking-wide text-blue-600">
Catatan admin
</p>
<p className="whitespace-pre-wrap">{payout.adminNote}</p>
</div>
)}
{payout.processedBy && (
<p className="mt-3 text-xs text-neutral-500">
Diproses oleh {payout.processedBy.name}
{payout.paidAt && ` · transfer ${formatDate(payout.paidAt)}`}
</p>
)}
{payout.status === "RELEASED" && (
<div className="mt-5 border-t border-neutral-100 pt-4">
{error && (
<div className="mb-3 rounded-lg bg-red-50 px-3 py-2 text-xs text-red-600">
{error}
</div>
)}
{open ? (
<div className="space-y-2">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-600">
Tandai Sudah Transfer
</p>
<textarea
value={note}
onChange={(e) => setNote(e.target.value)}
rows={2}
placeholder="Referensi transfer / nomor mutasi bank (wajib)"
className="w-full rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm focus:bg-white"
/>
<div className="flex gap-2">
<button
type="button"
onClick={submit}
disabled={loading || note.trim().length < 3}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
>
{loading ? "Memproses…" : "Tandai PAID"}
</button>
<button
type="button"
onClick={() => {
setOpen(false);
setNote("");
setError("");
}}
disabled={loading}
className="rounded-xl border border-neutral-200 bg-white px-4 py-2 text-sm font-medium text-neutral-600 hover:bg-neutral-50 disabled:opacity-50"
>
Batal
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setOpen(true)}
disabled={loading}
className="rounded-xl bg-primary-600 px-4 py-2 text-sm font-bold text-white hover:bg-primary-700 disabled:opacity-50"
>
💸 Tandai sudah ditransfer ke organizer
</button>
)}
</div>
)}
</article>
);
}
function Field({
label,
value,
highlight,
}: {
label: string;
value: string;
highlight?: boolean;
}) {
return (
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
{label}
</p>
<p
className={`mt-0.5 text-sm ${
highlight ? "font-bold text-primary-700" : "text-neutral-800"
}`}
>
{value}
</p>
</div>
);
}
function StatusPill({ status }: { status: PayoutStatus }) {
const cfg: Record<PayoutStatus, { label: string; cls: string }> = {
HELD: {
label: "Ditahan (escrow)",
cls: "bg-amber-50 text-amber-700 ring-amber-200",
},
RELEASED: {
label: "Siap transfer",
cls: "bg-blue-50 text-blue-700 ring-blue-200",
},
PAID: {
label: "Selesai",
cls: "bg-primary-50 text-primary-700 ring-primary-200",
},
CANCELLED: {
label: "Dibatalkan",
cls: "bg-neutral-100 text-neutral-600 ring-neutral-200",
},
};
const c = cfg[status];
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ring-1 ${c.cls}`}
>
{c.label}
</span>
);
}