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>
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { paymentApi } from "@/modules/payment/services/payment-api";
|
||
import { usePaymentMetrics } from "@/modules/payment/hooks/usePaymentCapabilities";
|
||
import { PaymentPageLoader, PaymentPageError } from "@/modules/payment/pages/shared";
|
||
import { PageHeader, Card, CardContent } from "@/components/ds";
|
||
import {
|
||
PaymentBarChart,
|
||
PaymentDonutChart,
|
||
PaymentStatCards,
|
||
} from "@/modules/payment/components/PaymentCharts";
|
||
|
||
export const PaymentKpis = function PaymentKpisPage() {
|
||
const { tenantId } = useTenantId();
|
||
const metrics = usePaymentMetrics();
|
||
|
||
const kpiQ = useQuery({
|
||
queryKey: ["payment", tenantId, "kpis"],
|
||
queryFn: async () => {
|
||
const [requests, transactions] = await Promise.all([
|
||
paymentApi.requests.list(tenantId!),
|
||
paymentApi.transactions.list(tenantId!),
|
||
]);
|
||
const byStatus: Record<string, number> = {};
|
||
for (const r of requests) {
|
||
const s = String(r.status ?? "unknown");
|
||
byStatus[s] = (byStatus[s] ?? 0) + 1;
|
||
}
|
||
const totalAmount = requests.reduce((sum, r) => sum + Number(r.amount_minor ?? 0), 0);
|
||
const paidCount = requests.filter((r) => r.status === "paid").length;
|
||
const successRate = requests.length ? Math.round((paidCount / requests.length) * 100) : 0;
|
||
return {
|
||
totalRequests: requests.length,
|
||
totalTransactions: transactions.length,
|
||
totalAmount,
|
||
successRate,
|
||
statusChart: Object.entries(byStatus).map(([name, value]) => ({ name, value })),
|
||
sourceChart: Object.entries(
|
||
requests.reduce<Record<string, number>>((acc, r) => {
|
||
const k = String(r.payment_source ?? "unknown");
|
||
acc[k] = (acc[k] ?? 0) + 1;
|
||
return acc;
|
||
}, {})
|
||
).map(([name, value]) => ({ name, value })),
|
||
};
|
||
},
|
||
enabled: !!tenantId,
|
||
});
|
||
|
||
if (!tenantId || kpiQ.isLoading || metrics.isLoading) return <PaymentPageLoader />;
|
||
if (kpiQ.error) return <PaymentPageError error={kpiQ.error} onRetry={() => kpiQ.refetch()} />;
|
||
|
||
const k = kpiQ.data!;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader title="شاخصهای کلیدی" description="نرخ موفقیت، حجم و توزیع وضعیت درخواستها." />
|
||
<PaymentStatCards
|
||
items={[
|
||
{ label: "کل درخواستها", value: k.totalRequests },
|
||
{ label: "تراکنشهای ثبتشده", value: k.totalTransactions },
|
||
{ label: "نرخ موفقیت", value: `${k.successRate}%` },
|
||
{ label: "مجموع مبلغ (ریال)", value: k.totalAmount.toLocaleString("fa-IR") },
|
||
]}
|
||
/>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-4 text-sm font-semibold">توزیع وضعیت</h3>
|
||
<PaymentDonutChart data={k.statusChart} />
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-4 text-sm font-semibold">منبع پرداخت</h3>
|
||
<PaymentBarChart data={k.sourceChart} />
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
{metrics.data?.metrics ? (
|
||
<Card>
|
||
<CardContent className="p-4">
|
||
<h3 className="mb-2 text-sm font-semibold">متریکهای سرویس</h3>
|
||
<pre className="overflow-x-auto text-xs">{JSON.stringify(metrics.data.metrics, null, 2)}</pre>
|
||
</CardContent>
|
||
</Card>
|
||
) : null}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PaymentKpis;
|