Files
starr-philproperties/src/modules/admin/pages/tiers/ViewPayment.jsx
T
2026-08-09 17:33:21 +08:00

224 lines
8.7 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState, useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, House, CreditCard, BadgeCheck, User } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Separator } from "@/components/ui/separator";
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
import { useTiers } from "@/contexts/AdminTiersContext";
import { useDateFormat } from "@/hooks/useDateFormat";
import { PageMeta } from "@/contexts/MetadataContext";
import api from "@/utils/api.util";
import { resolveTierBadge } from "@/utils/tierBadge.util";
const STATUS_BADGE = {
pending: "secondary",
completed: "default",
failed: "destructive",
cancelled: "outline",
expired: "outline",
refunded: "outline",
};
// ─── Provider label map (extend as you add more providers) ───────────────────
const PROVIDER_LABELS = {
paypal: "PayPal",
stripe: "Stripe",
gcash: "GCash",
};
function InfoRow({ label, children }) {
return (
<div className="flex flex-col gap-0.5">
<span className="text-xs text-muted-foreground uppercase tracking-wide">{label}</span>
<span className="text-sm font-medium break-all">
{children ?? <span className="text-muted-foreground italic">—</span>}
</span>
</div>
);
}
function SectionCard({ icon: Icon, title, children }) {
return (
<div className="rounded-lg border bg-card p-5 space-y-4">
<div className="flex items-center gap-2">
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
<h2 className="text-sm font-semibold">{title}</h2>
</div>
<Separator />
{children}
</div>
);
}
// ─── Provider reference section — renders differently per provider ────────────
function ProviderReference({ payment }) {
const provider = payment.provider ?? "paypal";
const payload = payment.provider_payload ?? {};
const title = `${PROVIDER_LABELS[provider] ?? provider} Reference`;
return (
<SectionCard icon={CreditCard} title={title}>
<div className="grid grid-cols-1 gap-3">
{/* PayPal */}
{provider === "paypal" && (
<>
<InfoRow label="Order ID">{payload.order_id}</InfoRow>
<InfoRow label="Capture ID">{payload.capture_id}</InfoRow>
<InfoRow label="Payer ID">{payload.payer_id}</InfoRow>
{payload.checkout && (
<>
<InfoRow label="Subtotal">
{payment.currency} {Number(payload.checkout.subtotal ?? 0).toFixed(2)}
</InfoRow>
{Number(payload.checkout.discount) > 0 && (
<InfoRow label="Promo Applied">
{payload.checkout.promo_code} — -{payment.currency}{" "}
{Number(payload.checkout.discount).toFixed(2)}
</InfoRow>
)}
</>
)}
</>
)}
{/* Stripe — extend when needed */}
{provider === "stripe" && (
<>
<InfoRow label="Payment Intent ID">{payload.payment_intent_id}</InfoRow>
<InfoRow label="Charge ID">{payload.charge_id}</InfoRow>
<InfoRow label="Customer ID">{payload.customer_id}</InfoRow>
</>
)}
{/* GCash / other — generic fallback */}
{provider !== "paypal" && provider !== "stripe" && (
<>
<InfoRow label="Reference ID">{payload.reference_id}</InfoRow>
<InfoRow label="Transaction ID">{payload.transaction_id}</InfoRow>
</>
)}
{/* Cancelled info — shown for any provider */}
{payload.cancelled_at && (
<InfoRow label="Cancelled At">
{fmtDateTime(payload.cancelled_at)}
</InfoRow>
)}
</div>
</SectionCard>
);
}
// ─── Main ─────────────────────────────────────────────────────────────────────
export default function ViewPayment() {
const navigate = useNavigate();
const { paymentId } = useParams();
const { fetchPayment, payment, loading } = useTiers();
const { fmtDateTime } = useDateFormat();
const [tierCategories, setTierCategories] = useState([]);
const tierMap = useMemo(() => Object.fromEntries(tierCategories.map((c) => [c.slug, c])), [tierCategories]);
useEffect(() => {
fetchPayment(paymentId);
api.get("/admin/tiers/categories").then(({ data }) => setTierCategories(data.data ?? [])).catch(() => {});
}, [paymentId]);
return (
<section className="bg-muted/60 min-h-full">
<PageMeta title={payment ? `${payment.plan?.label ?? 'Payment'} – Payment - STARR` : undefined} />
<div className="lg:container lg:mx-auto lg:px-0 flex flex-col items-start px-4 py-10">
<div className="w-full max-w-2xl mx-auto">
<div className="flex flex-col gap-2 mb-6">
<AppBreadcrumb items={[
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
{ label: "Payments", to: "/admin/tiers/payments" },
{ label: `Payment #${paymentId}` },
]} />
</div>
<div className="flex items-center gap-3 mb-6">
<Button type="button" variant="ghost" size="icon" onClick={() => navigate("/admin/tiers/payments")}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-semibold">Payment Details</h1>
<p className="text-sm text-muted-foreground">Payment #{paymentId}</p>
</div>
</div>
{loading && !payment ? (
<div className="space-y-5">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-32 w-full" />)}
</div>
) : !payment ? (
<p className="text-sm text-muted-foreground">Payment not found.</p>
) : (
<div className="space-y-5">
{/* Transaction */}
<SectionCard icon={CreditCard} title="Transaction">
<div className="grid grid-cols-2 gap-4">
<InfoRow label="Status">
<Badge variant={STATUS_BADGE[payment.status] ?? "outline"} className="capitalize mt-0.5">
{payment.status}
</Badge>
</InfoRow>
<InfoRow label="Provider">
{PROVIDER_LABELS[payment.provider] ?? payment.provider ?? "—"}
</InfoRow>
<InfoRow label="Amount">
{payment.currency} {Number(payment.amount).toFixed(2)}
</InfoRow>
{Number(payment.discount) > 0 && (
<InfoRow label="Discount">
-{payment.currency} {Number(payment.discount).toFixed(2)}
{payment.promo_code && ` (${payment.promo_code})`}
</InfoRow>
)}
<InfoRow label="Paid At">
{payment.paid_at ? fmtDateTime(payment.paid_at) : "—"}
</InfoRow>
<InfoRow label="Created At">
{payment.createdAt ? fmtDateTime(payment.createdAt) : "—"}
</InfoRow>
</div>
</SectionCard>
{/* User */}
<SectionCard icon={User} title="User">
<div className="grid grid-cols-2 gap-4">
<InfoRow label="User ID">{String(payment.user_id)}</InfoRow>
<InfoRow label="Email">{payment.user?.email}</InfoRow>
</div>
</SectionCard>
{/* Plan */}
{payment.plan && (
<SectionCard icon={BadgeCheck} title="Plan">
<div className="grid grid-cols-2 gap-4">
<InfoRow label="Label">{payment.plan.label}</InfoRow>
<InfoRow label="Subscription">
{(() => { const { cls, label } = resolveTierBadge(payment.plan.tier, tierMap); return <Badge className={`${cls} mt-0.5`}>{label}</Badge>; })()}
</InfoRow>
<InfoRow label="Duration">{payment.plan.duration_days} days</InfoRow>
</div>
</SectionCard>
)}
{/* Provider reference — dynamic per provider */}
<ProviderReference payment={payment} />
</div>
)}
</div>
</div>
</section>
);
}