Add payment module, BFF proxy, hub/dashboard/ops/PSP/merchant screens wired to real APIs, and mark payment-frontend complete in project status. Co-authored-by: Cursor <cursoragent@cursor.com>
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import Link from "next/link";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { paymentApi } from "@/modules/payment/services/payment-api";
|
||
import {
|
||
usePaymentCapabilities,
|
||
usePaymentMetrics,
|
||
} from "@/modules/payment/hooks/usePaymentCapabilities";
|
||
import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
|
||
import { PageHeader, Card, CardContent, Button, Badge } from "@/components/ds";
|
||
import {
|
||
PaymentBarChart,
|
||
PaymentStatCards,
|
||
} from "@/modules/payment/components/PaymentCharts";
|
||
|
||
export const PaymentDashboard = function PaymentExecutiveDashboard() {
|
||
const { tenantId } = useTenantId();
|
||
const caps = usePaymentCapabilities();
|
||
const metrics = usePaymentMetrics();
|
||
|
||
const countsQ = useQuery({
|
||
queryKey: ["payment", tenantId, "dashboard-counts"],
|
||
queryFn: async () => {
|
||
const [requests, transactions, merchants, connections, audit] = await Promise.all([
|
||
paymentApi.requests.list(tenantId!),
|
||
paymentApi.transactions.list(tenantId!),
|
||
paymentApi.merchants.list(tenantId!).catch(() => []),
|
||
paymentApi.pspConnections.list(tenantId!).catch(() => []),
|
||
paymentApi.audit.list(tenantId!),
|
||
]);
|
||
return {
|
||
requests: requests.length,
|
||
transactions: transactions.length,
|
||
merchants: merchants.length,
|
||
connections: connections.length,
|
||
paid: requests.filter((r) => r.status === "paid").length,
|
||
pending: requests.filter((r) =>
|
||
["pending", "redirect_issued"].includes(String(r.status))
|
||
).length,
|
||
recentRequests: requests.slice(0, 5),
|
||
recentAudit: audit.slice(0, 8),
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || countsQ.isLoading || caps.isLoading) return <PaymentPageLoader />;
|
||
if (countsQ.error) return <PaymentPageError error={countsQ.error} onRetry={() => countsQ.refetch()} />;
|
||
|
||
const c = countsQ.data!;
|
||
const chartData = [
|
||
{ name: "درخواست", value: c.requests },
|
||
{ name: "تراکنش", value: c.transactions },
|
||
{ name: "پذیرنده", value: c.merchants },
|
||
{ name: "درگاه", value: c.connections },
|
||
];
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title="داشبورد اجرایی"
|
||
description="نمای کلی تربتپی — درخواست، تراکنش، درگاه و پذیرنده."
|
||
actions={<Badge tone="success">Torbat Pay v{caps.data?.version}</Badge>}
|
||
/>
|
||
<PaymentStatCards
|
||
items={[
|
||
{ label: "درخواستهای پرداخت", value: c.requests },
|
||
{ label: "تراکنشهای ثبتشده", value: c.transactions },
|
||
{ label: "پرداخت موفق", value: c.paid, hint: `${c.pending} در انتظار` },
|
||
{ label: "درگاههای PSP", value: c.connections },
|
||
]}
|
||
/>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-4 text-sm font-semibold text-secondary">توزیع منابع</h3>
|
||
<PaymentBarChart data={chartData} />
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold text-secondary">اقدامات سریع</h3>
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/payment/requests"><Button variant="outline" size="sm">درخواستها</Button></Link>
|
||
<Link href="/payment/transactions"><Button variant="outline" size="sm">تراکنشها</Button></Link>
|
||
<Link href="/payment/psp"><Button variant="outline" size="sm">درگاه PSP</Button></Link>
|
||
<Link href="/payment/merchants"><Button variant="outline" size="sm">پذیرندگان</Button></Link>
|
||
<Link href="/payment/verification"><Button variant="outline" size="sm">تأیید پرداخت</Button></Link>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">آخرین درخواستها</h3>
|
||
<ul className="space-y-1 text-sm">
|
||
{c.recentRequests.map((r) => (
|
||
<li key={String(r.id)}>
|
||
<Link href={`/payment/requests/${r.id}`} className="text-teal-600 hover:underline">
|
||
{String(r.amount_minor)} {String(r.currency)} — {String(r.status)}
|
||
</Link>
|
||
</li>
|
||
))}
|
||
{c.recentRequests.length === 0 ? <li className="text-[var(--muted)]">درخواستی نیست</li> : null}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">فعالیت اخیر (حسابرسی)</h3>
|
||
<ul className="space-y-1 text-xs text-[var(--muted)]">
|
||
{c.recentAudit.map((a) => (
|
||
<li key={String(a.id)}>
|
||
{String(a.action)} — {String(a.entity_type)} —{" "}
|
||
{a.created_at ? new Date(String(a.created_at)).toLocaleString("fa-IR") : "—"}
|
||
</li>
|
||
))}
|
||
{c.recentAudit.length === 0 ? <li>فعالیتی ثبت نشده</li> : null}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
{metrics.data ? (
|
||
<Card>
|
||
<CardContent className="p-4 text-xs text-[var(--muted)]">
|
||
فاز {metrics.data.phase} — سرویس {metrics.data.service}
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PaymentDashboard;
|