mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
224 lines
8.7 KiB
React
224 lines
8.7 KiB
React
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>
|
||
);
|
||
} |