import Link from "next/link"; import { notFound, redirect } from "next/navigation"; import { getServerSession } from "next-auth"; import { ArrowLeft, CalendarDays, CircleAlert, MapPin } from "lucide-react"; import { authOptions } from "@/lib/auth"; import { isAdminEmail } from "@/lib/admin"; import { bookingRepo } from "@/server/repositories/booking.repo"; import { formatRupiah } from "@/lib/utils"; import { formatTripCalendarDateRangeLong } from "@/lib/trip-dates"; import { AdminReconcileButton } from "@/features/booking/components/admin-reconcile-button"; import { RawCallbackViewer } from "@/features/booking/components/raw-callback-viewer"; interface PageProps { params: Promise<{ id: string }>; } export default async function AdminBookingDetailPage({ params }: PageProps) { const session = await getServerSession(authOptions); if (!session?.user) { redirect("/login?callbackUrl=/admin/bookings"); } if (!isAdminEmail(session.user.email)) { return (

Halaman ini hanya untuk admin SeTrip.

); } const { id } = await params; const booking = await bookingRepo.findByIdForAdmin(id); if (!booking) notFound(); // Build chronological timeline lintas Payment + Refund + Payout. type TimelineEvent = | { kind: "payment"; at: Date; payment: (typeof booking.payments)[number]; } | { kind: "refund"; at: Date; refund: (typeof booking.refunds)[number]; } | { kind: "payout"; at: Date; payout: NonNullable; }; const timeline: TimelineEvent[] = []; for (const p of booking.payments) { timeline.push({ kind: "payment", at: p.createdAt, payment: p }); } for (const r of booking.refunds) { timeline.push({ kind: "refund", at: r.createdAt, refund: r }); } if (booking.payout) { timeline.push({ kind: "payout", at: booking.payout.createdAt, payout: booking.payout, }); } timeline.sort((a, b) => a.at.getTime() - b.at.getTime()); return (
Dashboard Trip terkait

Booking

{booking.trip.title}

{formatTripCalendarDateRangeLong(booking.trip.date, booking.trip.endDate)} · {booking.trip.destination}, {booking.trip.location}

Booking ID:{" "} {booking.id}

Timeline Money Flow ({timeline.length} event)

{timeline.length === 0 ? (

Belum ada event payment / refund / payout untuk booking ini.

) : (
    {timeline.map((ev, idx) => (
  1. {ev.kind === "payment" && ( )} {ev.kind === "refund" && } {ev.kind === "payout" && }
  2. ))}
)}
); } function FieldRow({ label, value, sub, strong, badge, }: { label: string; value: string; sub?: string; strong?: boolean; badge?: boolean; }) { return (

{label}

{badge ? ( {value} ) : (

{value}

)} {sub && (

{sub}

)}
); } function EventHeader({ kind, title, at, }: { kind: "payment" | "refund" | "payout"; title: string; at: Date; }) { const dotCls = kind === "payment" ? "bg-secondary-500" : kind === "refund" ? "bg-amber-500" : "bg-emerald-500"; return (

{title}

{at.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", })}

); } function PaymentEventCard({ payment, }: { payment: { id: string; provider: string; method: string | null; amount: number; status: string; externalOrderId: string; externalTxId: string | null; snapToken: string | null; expiresAt: Date | null; paidAt: Date | null; failedAt: Date | null; rejectionReason: string | null; rawCallback: unknown; createdAt: Date; updatedAt: Date; }; }) { const canReconcile = payment.provider === "MIDTRANS"; return (

Order ID:{" "} {payment.externalOrderId}

{payment.externalTxId && (

Transaction ID:{" "} {payment.externalTxId}

)}

Nominal:{" "} {formatRupiah(payment.amount)}

Status:{" "} {payment.method && ( via {payment.method} )}

{payment.expiresAt && (

Expires:{" "} {payment.expiresAt.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", })}

)} {payment.paidAt && (

Paid at:{" "} {payment.paidAt.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", })}

)} {payment.rejectionReason && (

{payment.rejectionReason}

)}
{canReconcile && ( )}
); } function RefundEventCard({ refund, }: { refund: { id: string; amount: number; reason: string; status: string; adminNote: string | null; reportNote: string; createdAt: Date; reviewedAt: Date | null; succeededAt: Date | null; failedAt: Date | null; reviewedBy: { id: string; name: string; email: string } | null; }; }) { return (

Nominal:{" "} {formatRupiah(refund.amount)}

Status:{" "}

{refund.reportNote && (

Report: {refund.reportNote}

)} {refund.adminNote && (

Admin note:{" "} {refund.adminNote}

)} {refund.reviewedBy && (

Reviewed by {refund.reviewedBy.email} {refund.reviewedAt && ( <> {" "} ·{" "} {refund.reviewedAt.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", })} )}

)}
); } function PayoutEventCard({ payout, }: { payout: { id: string; amount: number; status: string; heldUntil: Date; releasedAt: Date | null; paidAt: Date | null; cancelledAt: Date | null; adminNote: string | null; createdAt: Date; processedBy: { id: string; name: string; email: string } | null; }; }) { return (

Nominal:{" "} {formatRupiah(payout.amount)}

Status:{" "}

Held until:{" "} {payout.heldUntil.toLocaleDateString("id-ID", { day: "2-digit", month: "short", year: "numeric", })}

{payout.paidAt && (

Paid at:{" "} {payout.paidAt.toLocaleString("id-ID", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", })}

)} {payout.adminNote && (

Admin note:{" "} {payout.adminNote}

)} {payout.processedBy && (

Processed by {payout.processedBy.email}

)}
); } function StatusBadge({ value }: { value: string }) { const finalStatuses = ["PAID", "SUCCEEDED", "RELEASED"]; const negativeStatuses = ["FAILED", "EXPIRED", "CANCELLED", "REJECTED"]; const cls = finalStatuses.includes(value) ? "bg-emerald-100 text-emerald-800" : negativeStatuses.includes(value) ? "bg-red-100 text-red-800" : "bg-amber-100 text-amber-800"; return ( {value} ); }