Files
setrip/features/booking/components/cancel-booking-button.tsx
T
2026-05-11 13:04:20 +07:00

162 lines
5.1 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 { cancelBookingWithRefundAction } from "@/features/booking/actions";
import { formatRupiah } from "@/lib/utils";
interface CancelBookingButtonProps {
tripId: string;
/** Hasil preview server-side (dihitung di trip detail page). */
preview: {
days: number;
refundAmount: number;
bookingAmount: number;
tierLabel: string;
};
}
type ServerResult =
| { kind: "REFUND_PENDING"; refundAmount: number; days: number }
| { kind: "CANCELLED_NO_REFUND"; days: number };
export function CancelBookingButton({ tripId, preview }: CancelBookingButtonProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState<ServerResult | null>(null);
async function handleConfirm() {
setLoading(true);
setError("");
const res = await cancelBookingWithRefundAction(tripId);
setLoading(false);
if ("error" in res) {
setError(res.error ?? "Terjadi kesalahan");
return;
}
setResult({
kind: res.kind,
refundAmount: res.refundAmount,
days: res.days,
} as ServerResult);
router.refresh();
}
if (result?.kind === "REFUND_PENDING") {
return (
<div className="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-900">
<p className="font-semibold">Request refund dibuat.</p>
<p className="mt-1 text-xs">
Refund <span className="font-bold">{formatRupiah(result.refundAmount)}</span>{" "}
menunggu review admin. Setelah disetujui dan ditransfer manual, slot
kamu di trip akan otomatis dibebaskan.
</p>
</div>
);
}
if (result?.kind === "CANCELLED_NO_REFUND") {
return (
<div className="rounded-2xl border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-700">
<p className="font-semibold">Booking dibatalkan.</p>
<p className="mt-1 text-xs">
Pembatalan di H-{result.days} berada di luar window refund tidak
ada nominal yang dikembalikan.
</p>
</div>
);
}
if (!open) {
return (
<button
type="button"
onClick={() => setOpen(true)}
className="w-full rounded-xl border-2 border-red-200 py-3 text-sm font-bold text-red-600 transition-colors hover:bg-red-50"
>
Cancel & Request Refund
</button>
);
}
const percentage = preview.bookingAmount
? Math.floor((preview.refundAmount * 100) / preview.bookingAmount)
: 0;
const noRefund = preview.refundAmount === 0;
return (
<div className="space-y-3 rounded-2xl border-2 border-red-200 bg-red-50 p-4 text-sm">
<div>
<p className="font-bold text-red-900">Cancel booking?</p>
<p className="mt-1 text-xs text-red-800/80">
Kamu cancel di <span className="font-semibold">H-{preview.days}</span>{" "}
dari tanggal berangkat.
</p>
</div>
<div className="rounded-xl border border-red-200 bg-white p-3">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">
Estimasi refund (sesuai policy)
</p>
<p className="mt-1 text-lg font-bold text-neutral-900">
{formatRupiah(preview.refundAmount)}
{!noRefund && (
<span className="ml-2 text-xs font-medium text-neutral-500">
({percentage}% dari {formatRupiah(preview.bookingAmount)})
</span>
)}
</p>
<p className="mt-1 text-[11px] text-neutral-500">
Tier: {preview.tierLabel}
</p>
{noRefund ? (
<p className="mt-2 text-xs text-red-700">
Di luar window refund uang tidak dikembalikan. Booking akan
di-cancel langsung.
</p>
) : (
<p className="mt-2 text-xs text-neutral-600">
Refund akan masuk antrian review admin. Setelah disetujui & uang
ditransfer, booking otomatis ditandai{" "}
{percentage === 100 ? "REFUNDED" : "PARTIALLY_REFUNDED"}.
</p>
)}
</div>
{error && (
<div className="rounded-lg bg-white px-3 py-2 text-xs font-medium text-red-700">
{error}
</div>
)}
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={handleConfirm}
disabled={loading}
className="rounded-xl bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700 disabled:opacity-50"
>
{loading
? "Memproses…"
: noRefund
? "Konfirmasi Cancel"
: "Konfirmasi & Request Refund"}
</button>
<button
type="button"
onClick={() => {
setOpen(false);
setError("");
}}
disabled={loading}
className="rounded-xl border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
>
Batal
</button>
</div>
</div>
);
}