diff --git a/src/api/adminSalesStats.ts b/src/api/adminSalesStats.ts index 7b5f261..5017520 100644 --- a/src/api/adminSalesStats.ts +++ b/src/api/adminSalesStats.ts @@ -165,6 +165,21 @@ export interface DepositsStats { daily_by_method: DailyDepositByMethodItem[]; } +export interface GatewaySuccessItem { + method: string; + total: number; + paid: number; + success_rate: number; +} + +export interface PaymentHealth { + total_attempts: number; + total_paid: number; + success_rate: number; + failed_purchases: number; + by_gateway: GatewaySuccessItem[]; +} + // ============ API ============ export const salesStatsApi = { @@ -197,4 +212,9 @@ export const salesStatsApi = { const response = await apiClient.get('/cabinet/admin/stats/sales/deposits', { params }); return response.data; }, + + getPaymentHealth: async (params: SalesStatsParams = {}): Promise => { + const response = await apiClient.get('/cabinet/admin/stats/sales/payment-health', { params }); + return response.data; + }, }; diff --git a/src/components/sales-stats/PaymentHealthTab.tsx b/src/components/sales-stats/PaymentHealthTab.tsx new file mode 100644 index 0000000..78c7326 --- /dev/null +++ b/src/components/sales-stats/PaymentHealthTab.tsx @@ -0,0 +1,115 @@ +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import type { SalesStatsParams } from '../../api/adminSalesStats'; +import { salesStatsApi } from '../../api/adminSalesStats'; +import { METHOD_LABELS } from '../../constants/paymentMethods'; +import { SALES_STATS } from '../../constants/salesStats'; +import { CardIcon, PercentIcon, WarningIcon } from '../../components/icons'; +import PaymentMethodIcon from '../PaymentMethodIcon'; +import { StatCard } from '../stats'; + +interface PaymentHealthTabProps { + params: SalesStatsParams; +} + +/** Green when healthy, amber when shaky, red when most attempts fail. */ +function rateColor(rate: number): string { + if (rate >= 90) return 'text-success-400'; + if (rate >= 70) return 'text-warning-400'; + return 'text-error-400'; +} + +function barColor(rate: number): string { + if (rate >= 90) return 'bg-success-500'; + if (rate >= 70) return 'bg-warning-500'; + return 'bg-error-500'; +} + +export function PaymentHealthTab({ params }: PaymentHealthTabProps) { + const { t } = useTranslation(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['sales-stats', 'payment-health', params], + queryFn: () => salesStatsApi.getPaymentHealth(params), + staleTime: SALES_STATS.STALE_TIME, + placeholderData: keepPreviousData, + }); + + if (isLoading) { + return ( +
+ {Array.from({ length: 3 }, (_, i) => ( +
+ ))} +
+ ); + } + + if (isError || !data) { + return
{t('admin.salesStats.loadError')}
; + } + + return ( +
+
+ } + tone={data.success_rate >= 90 ? 'success' : data.success_rate >= 70 ? 'warning' : 'error'} + /> + } + tone="accent" + /> + } + tone="warning" + /> +
+ +
+

+ {t('admin.salesStats.payments.byGateway')} +

+ {data.by_gateway.length === 0 ? ( +
{t('common.noData')}
+ ) : ( +
+ {data.by_gateway.map((g) => ( +
+ +
+
+ + {METHOD_LABELS[g.method] || g.method} + + + {g.paid}/{g.total} + +
+
+
+
+
+ + {g.success_rate}% + +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/components/sales-stats/index.ts b/src/components/sales-stats/index.ts index f9eba7d..bda2294 100644 --- a/src/components/sales-stats/index.ts +++ b/src/components/sales-stats/index.ts @@ -3,6 +3,7 @@ export { DepositsTab } from './DepositsTab'; export { DonutChart } from './DonutChart'; export { DualAreaChart } from './DualAreaChart'; export { MultiSeriesAreaChart } from './MultiSeriesAreaChart'; +export { PaymentHealthTab } from './PaymentHealthTab'; export { PeriodSelector } from './PeriodSelector'; export { RenewalsTab } from './RenewalsTab'; export { SalesTab } from './SalesTab'; diff --git a/src/locales/en.json b/src/locales/en.json index 6a482c4..eef7a36 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1296,7 +1296,8 @@ "sales": "Sales", "renewals": "Renewals", "addons": "Add-ons", - "deposits": "Deposits" + "deposits": "Deposits", + "payment": "Payments" }, "summary": { "revenue": "Revenue", @@ -1360,6 +1361,12 @@ "dailyByMethod": "Daily Deposits by Payment Method", "revenue": "Revenue" }, + "payments": { + "successRate": "Payment success rate", + "attempts": "Payment attempts", + "failedPurchases": "Failed purchases", + "byGateway": "Success rate by gateway" + }, "loadError": "Failed to load statistics" }, "referralNetwork": { diff --git a/src/locales/fa.json b/src/locales/fa.json index 26cde24..377d506 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1169,7 +1169,8 @@ "sales": "فروش", "renewals": "تمدید", "addons": "خدمات اضافی", - "deposits": "شارژ" + "deposits": "شارژ", + "payment": "پرداخت‌ها" }, "summary": { "revenue": "درآمد", @@ -1233,6 +1234,12 @@ "dailyByMethod": "شارژ روزانه بر اساس روش پرداخت", "revenue": "درآمد" }, + "payments": { + "successRate": "نرخ موفقیت پرداخت", + "attempts": "تلاش‌های پرداخت", + "failedPurchases": "خریدهای ناموفق", + "byGateway": "نرخ موفقیت بر اساس درگاه" + }, "loadError": "بارگذاری آمار ناموفق بود" }, "referralNetwork": { diff --git a/src/locales/ru.json b/src/locales/ru.json index 872bd7c..3ad0fe5 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1318,7 +1318,8 @@ "sales": "Продажи", "renewals": "Продления", "addons": "Доп. услуги", - "deposits": "Пополнения" + "deposits": "Пополнения", + "payment": "Оплаты" }, "summary": { "revenue": "Доход", @@ -1382,6 +1383,12 @@ "dailyByMethod": "Пополнения по дням по платёжкам", "revenue": "Доход" }, + "payments": { + "successRate": "Успешность оплат", + "attempts": "Попыток оплат", + "failedPurchases": "Неудачные покупки", + "byGateway": "Успешность по шлюзам" + }, "loadError": "Не удалось загрузить статистику" }, "referralNetwork": { diff --git a/src/locales/zh.json b/src/locales/zh.json index dca06ba..9a188d9 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1169,7 +1169,8 @@ "sales": "销售", "renewals": "续订", "addons": "附加服务", - "deposits": "充值" + "deposits": "充值", + "payment": "支付" }, "summary": { "revenue": "收入", @@ -1233,6 +1234,12 @@ "dailyByMethod": "每日按支付方式充值", "revenue": "收入" }, + "payments": { + "successRate": "支付成功率", + "attempts": "支付尝试", + "failedPurchases": "失败购买", + "byGateway": "各网关成功率" + }, "loadError": "加载统计失败" }, "referralNetwork": { diff --git a/src/pages/AdminSalesStats.tsx b/src/pages/AdminSalesStats.tsx index 55cdb8d..78a366e 100644 --- a/src/pages/AdminSalesStats.tsx +++ b/src/pages/AdminSalesStats.tsx @@ -22,13 +22,14 @@ import { StatCard } from '../components/stats'; import { AddonsTab, DepositsTab, + PaymentHealthTab, PeriodSelector, RenewalsTab, SalesTab, TrialsTab, } from '../components/sales-stats'; -type TabId = 'trials' | 'sales' | 'renewals' | 'addons' | 'deposits'; +type TabId = 'trials' | 'sales' | 'renewals' | 'addons' | 'deposits' | 'payment'; type Delta = { percent: number; trend: 'up' | 'down' | 'stable' }; @@ -124,6 +125,7 @@ export default function AdminSalesStats() { { id: 'renewals', label: t('admin.salesStats.tabs.renewals') }, { id: 'addons', label: t('admin.salesStats.tabs.addons') }, { id: 'deposits', label: t('admin.salesStats.tabs.deposits') }, + { id: 'payment', label: t('admin.salesStats.tabs.payment') }, ]; return ( @@ -258,6 +260,7 @@ export default function AdminSalesStats() { {activeTab === 'renewals' && } {activeTab === 'addons' && } {activeTab === 'deposits' && } + {activeTab === 'payment' && }
)}