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, type SearchStats } from '../api/adminPayments'; import { DateField } from '../components/DateField'; import { useCurrency } from '../hooks/useCurrency'; import type { PendingPayment, PaginatedResponse } from '../types'; import { usePlatform } from '../platform/hooks/usePlatform'; import { StatCard } from '@/components/stats'; import { BackIcon, SearchIcon, CalendarIcon, RefreshIcon, CheckCircleIcon, ChartBarIcon, ClockIcon, XCircleIcon, } from '@/components/icons'; interface StatusBadgeProps { status: string; } function StatusBadge({ status }: StatusBadgeProps) { const styles: Record = { paid: 'bg-success-500/20 text-success-400', pending: 'bg-warning-500/20 text-warning-400', cancelled: 'bg-error-500/20 text-error-400', }; const normalized = status.toLowerCase(); const match = Object.keys(styles).find((key) => normalized.includes(key)); return ( {status} ); } export default function AdminPayments() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { formatAmount, currencySymbol } = useCurrency(); const { capabilities } = usePlatform(); 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-search', queryParams, page], queryFn: () => adminPaymentsApi.searchPayments({ ...queryParams, page, per_page: 20, }), refetchInterval: isDefaultFilters ? 30000 : false, }); // Fetch stats 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 () => { await refetch(); queryClient.invalidateQueries({ queryKey: ['admin-payments-search-stats'] }); }, onSettled: () => { setCheckingPaymentId(null); }, }); const handleCheckPayment = (payment: PendingPayment) => { setCheckingPaymentId(`${payment.method}_${payment.id}`); checkPaymentMutation.mutate({ method: payment.method, paymentId: payment.id }); }; 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 */}
{/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( )}

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

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

{/* Search bar */}
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, })}
)} {/* 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 && (
)} {/* Stats cards */} {stats && (
)} {/* Payments list */}
{isError ? (
{t('common.error')}
) : isLoading ? (
) : payments?.items && payments.items.length > 0 ? (
{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.is_paid && ( {t('admin.payments.paid')} )}
{/* Amount */}
{formatAmount(payment.amount_rubles)} {currencySymbol}
{/* Invoice ID */}
{payment.identifier}
{/* User info */} {(payment.user_username || payment.user_telegram_id || payment.user_email) && (
{t('admin.payments.user')}:{' '} {payment.user_id ? ( ) : ( <> {payment.user_username && ( @{payment.user_username} )} {payment.user_username && (payment.user_telegram_id || payment.user_email) && ( · )} {payment.user_telegram_id && ( TG: {payment.user_telegram_id} )} {!payment.user_telegram_id && payment.user_email && ( {payment.user_email} )} )}
)} {/* Timestamp */}
{new Date(payment.created_at).toLocaleString()}
{/* Action buttons */}
{payment.payment_url && ( {t('admin.payments.openLink')} )} {payment.is_checkable && ( )}
{/* Show result after check */} {checkPaymentMutation.isSuccess && 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')}
)}
); })}
) : (
{t('admin.payments.noPayments')}
)} {/* Pagination */} {payments && payments.pages > 1 && (
{t('balance.page', { current: payments.page, total: payments.pages, })}
)}
); }