diff --git a/src/api/adminPayments.ts b/src/api/adminPayments.ts index 091e867..a3ad7fc 100644 --- a/src/api/adminPayments.ts +++ b/src/api/adminPayments.ts @@ -6,6 +6,14 @@ export interface PaymentsStats { by_method: Record; } +export interface SearchStats { + total: number; + pending: number; + paid: number; + cancelled: number; + by_method: Record; +} + export const adminPaymentsApi = { // Get all pending payments (admin) getPendingPayments: async (params?: { @@ -28,6 +36,39 @@ export const adminPaymentsApi = { return response.data; }, + // Search payments with filters + searchPayments: async (params?: { + search?: string; + status_filter?: string; + method_filter?: string; + period?: string; + date_from?: string; + date_to?: string; + page?: number; + per_page?: number; + }): Promise> => { + const response = await apiClient.get>( + '/cabinet/admin/payments/search', + { params }, + ); + return response.data; + }, + + // Get search statistics with filters + getSearchStats: async (params?: { + search?: string; + status_filter?: string; + method_filter?: string; + period?: string; + date_from?: string; + date_to?: string; + }): Promise => { + const response = await apiClient.get('/cabinet/admin/payments/search/stats', { + params, + }); + return response.data; + }, + // Get specific payment details getPayment: async (method: string, paymentId: number): Promise => { const response = await apiClient.get( diff --git a/src/locales/en.json b/src/locales/en.json index e433e2d..2e70d06 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1286,18 +1286,40 @@ "previewNotAvailable": "Preview not available" }, "payments": { - "title": "Payment verification", - "description": "Pending payments in the last 24 hours", + "title": "Payments", + "description": "Search and manage payments", + "searchPlaceholder": "Search: invoice, TG ID, @username, email...", + "searchHint": "Examples: SP-78291, @username, 123456789, user@mail.ru", + "searchResults": "Results for: {{query}} — found {{count}}", + "resetSearch": "Reset", + "statusAll": "All", + "statusPending": "Pending", + "statusPaid": "Paid", + "statusCancelled": "Cancelled", + "period24h": "24h", + "period7d": "7d", + "period30d": "30d", + "periodAll": "All", + "periodCustom": "Range", + "dateFrom": "From", + "dateTo": "To", + "applyDate": "Apply", + "allMethods": "All methods", + "totalCount": "Total", + "pendingCount": "Pending", + "paidCount": "Paid", + "cancelledCount": "Cancelled", "totalPending": "Total pending", "filterByMethod": "Filter by method", - "noPayments": "No pending payments", + "noPayments": "No payments found", "paid": "Paid", "user": "User", "openLink": "Open link", "checkStatus": "Check status", "checking": "Checking...", - "prev": "Previous", - "next": "Next" + "prev": "Prev", + "next": "Next", + "checkError": "Status check failed" }, "paymentMethods": { "title": "Payment Methods", diff --git a/src/locales/fa.json b/src/locales/fa.json index 4311f6b..b416e8d 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1326,18 +1326,40 @@ } }, "payments": { - "title": "تأیید پرداخت", - "description": "پرداخت‌های معلق در ۲۴ ساعت گذشته", + "title": "پرداخت‌ها", + "description": "جستجو و مدیریت پرداخت‌ها", + "searchPlaceholder": "جستجو: فاکتور، شناسه تلگرام، @نام‌کاربری، ایمیل...", + "searchHint": "مثال‌ها: SP-78291, @username, 123456789, user@mail.ru", + "searchResults": "نتایج برای: {{query}} — {{count}} یافت شد", + "resetSearch": "بازنشانی", + "statusAll": "همه", + "statusPending": "در انتظار", + "statusPaid": "پرداخت شده", + "statusCancelled": "لغو شده", + "period24h": "۲۴ساعت", + "period7d": "۷روز", + "period30d": "۳۰روز", + "periodAll": "همه", + "periodCustom": "بازه", + "dateFrom": "از", + "dateTo": "تا", + "applyDate": "اعمال", + "allMethods": "همه روش‌ها", + "totalCount": "کل", + "pendingCount": "در انتظار", + "paidCount": "پرداخت شده", + "cancelledCount": "لغو شده", "totalPending": "کل معلق", "filterByMethod": "فیلتر بر اساس روش", + "noPayments": "پرداختی یافت نشد", "paid": "پرداخت شده", "user": "کاربر", "openLink": "باز کردن لینک", - "checking": "در حال بررسی...", "checkStatus": "بررسی وضعیت", - "noPayments": "هیچ پرداخت معلقی نیست", + "checking": "در حال بررسی...", "prev": "قبلی", - "next": "بعدی" + "next": "بعدی", + "checkError": "خطا در بررسی وضعیت" }, "settings": { "title": "تنظیمات سیستم", diff --git a/src/locales/ru.json b/src/locales/ru.json index 66108e5..e1d74f3 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1307,18 +1307,40 @@ "previewNotAvailable": "Предпросмотр недоступен" }, "payments": { - "title": "Проверка платежей", - "description": "Ожидающие платежи за последние 24 часа", + "title": "Платежи", + "description": "Поиск и управление платежами", + "searchPlaceholder": "Поиск: инвойс, TG ID, @username, email...", + "searchHint": "Примеры: SP-78291, @username, 123456789, user@mail.ru", + "searchResults": "Результаты по запросу: {{query}} — найдено {{count}}", + "resetSearch": "Сбросить", + "statusAll": "Все", + "statusPending": "В ожидании", + "statusPaid": "Оплачено", + "statusCancelled": "Отменено", + "period24h": "24ч", + "period7d": "7д", + "period30d": "30д", + "periodAll": "Все", + "periodCustom": "Период", + "dateFrom": "С", + "dateTo": "По", + "applyDate": "Применить", + "allMethods": "Все методы", + "totalCount": "Всего", + "pendingCount": "В ожидании", + "paidCount": "Оплачено", + "cancelledCount": "Отменено", "totalPending": "Всего ожидает", "filterByMethod": "Фильтр по методу", - "noPayments": "Нет ожидающих платежей", + "noPayments": "Платежи не найдены", "paid": "Оплачено", "user": "Пользователь", "openLink": "Открыть ссылку", "checkStatus": "Проверить статус", "checking": "Проверка...", "prev": "Назад", - "next": "Далее" + "next": "Далее", + "checkError": "Ошибка проверки статуса" }, "paymentMethods": { "title": "Платёжные методы", diff --git a/src/locales/zh.json b/src/locales/zh.json index 5e1948c..0e5231b 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1158,18 +1158,40 @@ "previewNotAvailable": "预览不可用" }, "payments": { - "title": "支付验证", - "description": "最近24小时内的待处理支付", + "title": "支付", + "description": "搜索和管理支付", + "searchPlaceholder": "搜索:发票、TG ID、@用户名、邮箱...", + "searchHint": "示例:SP-78291、@username、123456789、user@mail.ru", + "searchResults": "搜索结果:{{query}} — 找到 {{count}} 条", + "resetSearch": "重置", + "statusAll": "全部", + "statusPending": "待处理", + "statusPaid": "已支付", + "statusCancelled": "已取消", + "period24h": "24小时", + "period7d": "7天", + "period30d": "30天", + "periodAll": "全部", + "periodCustom": "范围", + "dateFrom": "从", + "dateTo": "到", + "applyDate": "应用", + "allMethods": "所有方式", + "totalCount": "总计", + "pendingCount": "待处理", + "paidCount": "已支付", + "cancelledCount": "已取消", "totalPending": "总待处理", - "filterByMethod": "按方法筛选", + "filterByMethod": "按方式筛选", + "noPayments": "未找到支付记录", "paid": "已支付", "user": "用户", "openLink": "打开链接", - "checking": "检查中...", "checkStatus": "检查状态", - "noPayments": "没有待处理的支付", + "checking": "检查中...", "prev": "上一页", - "next": "下一页" + "next": "下一页", + "checkError": "状态检查失败" }, "wheel": { "title": "幸运转盘设置", diff --git a/src/pages/AdminPayments.tsx b/src/pages/AdminPayments.tsx index 95b8a42..eea7686 100644 --- a/src/pages/AdminPayments.tsx +++ b/src/pages/AdminPayments.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { adminPaymentsApi } from '../api/adminPayments'; +import { adminPaymentsApi, type SearchStats } from '../api/adminPayments'; import { useCurrency } from '../hooks/useCurrency'; import type { PendingPayment, PaginatedResponse } from '../types'; import { usePlatform } from '../platform/hooks/usePlatform'; @@ -20,6 +20,83 @@ const BackIcon = () => ( ); +// SearchIcon +const SearchIcon = () => ( + + + +); + +// CalendarIcon +const CalendarIcon = () => ( + + + +); + +interface StatusBadgeProps { + status: string; +} + +function StatusBadge({ status }: StatusBadgeProps) { + const styles: Record = { + paid: 'bg-green-500/20 text-green-400', + pending: 'bg-amber-500/20 text-amber-400', + cancelled: 'bg-red-500/20 text-red-400', + }; + + const normalized = status.toLowerCase(); + const match = Object.keys(styles).find((key) => normalized.includes(key)); + + return ( + + {status} + + ); +} + +interface StatCardProps { + label: string; + value: number; + color: 'blue' | 'amber' | 'green' | 'red'; + isActive: boolean; + onClick: () => void; +} + +function StatCard({ label, value, color, isActive, onClick }: StatCardProps) { + const colors: Record = { + blue: 'border-accent-500/30 bg-accent-500/20 text-accent-400', + amber: 'border-amber-500/30 bg-amber-500/20 text-amber-400', + green: 'border-green-500/30 bg-green-500/20 text-green-400', + red: 'border-red-500/30 bg-red-500/20 text-red-400', + }; + + return ( + + ); +} + export default function AdminPayments() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -27,44 +104,76 @@ export default function AdminPayments() { const { formatAmount, currencySymbol } = useCurrency(); const { capabilities } = usePlatform(); - const [page, setPage] = useState(1); + const [searchInput, setSearchInput] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [periodFilter, setPeriodFilter] = useState('24h'); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); + const [showDateRange, setShowDateRange] = useState(false); const [methodFilter, setMethodFilter] = useState(''); + const [page, setPage] = useState(1); const [checkingPaymentId, setCheckingPaymentId] = useState(null); + // Debounce search input (300ms) + useEffect(() => { + const timer = setTimeout(() => { + setSearchQuery(searchInput); + setPage(1); + }, 300); + return () => clearTimeout(timer); + }, [searchInput]); + + // Reset page on filter changes + useEffect(() => { + setPage(1); + }, [statusFilter, periodFilter, methodFilter, dateFrom, dateTo]); + + // Auto-refresh only when filters are at defaults and no search + const isDefaultFilters = + !searchQuery && statusFilter === 'all' && periodFilter === '24h' && !methodFilter; + + // Shared query params + const queryParams = { + search: searchQuery || undefined, + status_filter: statusFilter, + method_filter: methodFilter || undefined, + period: periodFilter === 'custom' ? undefined : periodFilter, + date_from: periodFilter === 'custom' && dateFrom ? dateFrom : undefined, + date_to: periodFilter === 'custom' && dateTo ? dateTo : undefined, + }; + // Fetch payments const { data: payments, isLoading, + isError, refetch, } = useQuery>({ - queryKey: ['admin-payments', page, methodFilter], + queryKey: ['admin-payments-search', queryParams, page], queryFn: () => - adminPaymentsApi.getPendingPayments({ + adminPaymentsApi.searchPayments({ + ...queryParams, page, per_page: 20, - method_filter: methodFilter || undefined, }), - refetchInterval: 30000, // Auto-refresh every 30 seconds + refetchInterval: isDefaultFilters ? 30000 : false, }); // Fetch stats - const { data: stats } = useQuery({ - queryKey: ['admin-payments-stats'], - queryFn: adminPaymentsApi.getStats, - refetchInterval: 30000, + const { data: stats } = useQuery({ + queryKey: ['admin-payments-search-stats', queryParams], + queryFn: () => adminPaymentsApi.getSearchStats(queryParams), + refetchInterval: isDefaultFilters ? 30000 : false, }); // Check payment mutation const checkPaymentMutation = useMutation({ mutationFn: ({ method, paymentId }: { method: string; paymentId: number }) => adminPaymentsApi.checkPaymentStatus(method, paymentId), - onSuccess: async (result) => { - if (result.status_changed) { - await refetch(); - queryClient.invalidateQueries({ queryKey: ['admin-payments-stats'] }); - } else { - await refetch(); - } + onSuccess: async () => { + await refetch(); + queryClient.invalidateQueries({ queryKey: ['admin-payments-search-stats'] }); }, onSettled: () => { setCheckingPaymentId(null); @@ -76,11 +185,47 @@ export default function AdminPayments() { checkPaymentMutation.mutate({ method: payment.method, paymentId: payment.id }); }; - // Get unique methods from stats for filter + const handleResetSearch = () => { + setSearchInput(''); + setSearchQuery(''); + setPage(1); + }; + + const handleStatusCardClick = (status: string) => { + setStatusFilter(statusFilter === status ? 'all' : status); + }; + + const handlePeriodChange = (period: string) => { + if (period === 'custom') { + setPeriodFilter('custom'); + setShowDateRange(true); + } else { + setPeriodFilter(period); + setShowDateRange(false); + } + }; + + // Get unique methods from stats for filter dropdown const methodOptions = stats?.by_method ? Object.keys(stats.by_method) : []; + // Period filter options + const periodOptions = [ + { value: '24h', label: t('admin.payments.period24h') }, + { value: '7d', label: t('admin.payments.period7d') }, + { value: '30d', label: t('admin.payments.period30d') }, + { value: 'all', label: t('admin.payments.periodAll') }, + ]; + + // Status filter options + const statusOptions = [ + { value: 'all', label: t('admin.payments.statusAll') }, + { value: 'pending', label: t('admin.payments.statusPending') }, + { value: 'paid', label: t('admin.payments.statusPaid') }, + { value: 'cancelled', label: t('admin.payments.statusCancelled') }, + ]; + return ( -
+
{/* Header */}
@@ -94,7 +239,7 @@ export default function AdminPayments() { )}
-

{t('admin.payments.title')}

+

{t('admin.payments.title')}

{t('admin.payments.description')}

@@ -116,63 +261,178 @@ export default function AdminPayments() {
- {/* Stats cards */} - {stats && ( -
-
-
{stats.total_pending}
-
{t('admin.payments.totalPending')}
+ {/* Search bar */} +
+
+
+
- {Object.entries(stats.by_method).map(([method, count]) => ( -
setMethodFilter(methodFilter === method ? '' : method)} - > -
{count}
-
{method}
-
- ))} + setSearchInput(e.target.value)} + placeholder={t('admin.payments.searchPlaceholder')} + className="w-full rounded-xl border border-dark-700 bg-dark-800 py-3 pl-10 pr-4 text-dark-100 placeholder-dark-500 transition-colors focus:border-accent-500 focus:outline-none focus:ring-1 focus:ring-accent-500" + /> +
+

{t('admin.payments.searchHint')}

+
+ + {/* Search result banner */} + {searchQuery && ( +
+ + {t('admin.payments.searchResults', { + query: searchQuery, + count: payments?.total ?? 0, + })} + +
)} - {/* Filter */} - {methodOptions.length > 0 && ( -
- {t('admin.payments.filterByMethod')}: - - {methodOptions.map((method) => ( + {/* Filters row */} +
+ {/* Status filter pills */} +
+ {statusOptions.map((option) => ( ))}
+ + {/* Period filter pills + Method filter */} +
+ {periodOptions.map((option) => ( + + ))} + + + {/* Method filter dropdown */} + {methodOptions.length > 0 && ( + + )} +
+
+ + {/* Date range panel */} + {showDateRange && ( +
+
+ + setDateFrom(e.target.value)} + className="w-full rounded-lg border border-dark-700 bg-dark-800 px-3 py-2 text-sm text-dark-100 focus:border-accent-500 focus:outline-none" + /> +
+
+ + setDateTo(e.target.value)} + className="w-full rounded-lg border border-dark-700 bg-dark-800 px-3 py-2 text-sm text-dark-100 focus:border-accent-500 focus:outline-none" + /> +
+ +
+ )} + + {/* Stats cards */} + {stats && ( +
+ handleStatusCardClick('all')} + /> + handleStatusCardClick('pending')} + /> + handleStatusCardClick('paid')} + /> + handleStatusCardClick('cancelled')} + /> +
)} {/* Payments list */}
- {isLoading ? ( + {isError ? ( +
+
{t('common.error')}
+ +
+ ) : isLoading ? (
@@ -181,6 +441,7 @@ export default function AdminPayments() { {payments.items.map((payment) => { const paymentKey = `${payment.method}_${payment.id}`; const isChecking = checkingPaymentId === paymentKey; + const isCancelled = payment.status.toLowerCase().includes('cancel'); return (
+ {/* Status badge + method */}
+ {payment.method_display} - - {payment.status_emoji} {payment.status_text} - {payment.is_paid && ( - + {t('admin.payments.paid')} )}
-
+ + {/* Amount */} +
{formatAmount(payment.amount_rubles)} {currencySymbol}
+ + {/* Invoice ID */}
- ID: {payment.identifier} -
-
- {new Date(payment.created_at).toLocaleString()} + {payment.identifier}
+ {/* User info */} {(payment.user_username || payment.user_telegram_id) && (
{t('admin.payments.user')}:{' '} - {payment.user_username ? ( + {payment.user_username && ( @{payment.user_username} - ) : ( - ID: {payment.user_telegram_id} + )} + {payment.user_username && payment.user_telegram_id && ( + · + )} + {payment.user_telegram_id && ( + TG: {payment.user_telegram_id} )}
)} + + {/* Timestamp */} +
+ {new Date(payment.created_at).toLocaleString()} +
+ + {/* Action buttons */}
+ {/* Show result after check */} {checkPaymentMutation.isSuccess && - checkPaymentMutation.variables?.paymentId === payment.id && ( + checkPaymentMutation.variables?.paymentId === payment.id && + checkPaymentMutation.variables?.method === payment.method && (
{checkPaymentMutation.data?.message}
)} + {checkPaymentMutation.isError && + checkPaymentMutation.variables?.paymentId === payment.id && + checkPaymentMutation.variables?.method === payment.method && ( +
+ {t('admin.payments.checkError')} +
+ )}
); })} @@ -318,7 +604,7 @@ export default function AdminPayments() { {t('admin.payments.prev')}
- {t('balance.page', '{current} / {total}', { + {t('balance.page', { current: payments.page, total: payments.pages, })}