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 { useCurrency } from '../hooks/useCurrency'; import type { PendingPayment, PaginatedResponse } from '../types'; import { usePlatform } from '../platform/hooks/usePlatform'; // BackIcon 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 ( {value} {label} ); } 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 && ( navigate('/admin')} className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" > )} {t('admin.payments.title')} {t('admin.payments.description')} refetch()} className="btn-secondary flex items-center gap-2"> {t('common.refresh')} {/* 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, })} {t('admin.payments.resetSearch')} )} {/* Filters row */} {/* Status filter pills */} {statusOptions.map((option) => ( setStatusFilter(option.value)} className={`rounded-lg px-3 py-1.5 text-sm transition-all ${ statusFilter === option.value ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > {option.label} ))} {/* Period filter pills + Method filter */} {periodOptions.map((option) => ( handlePeriodChange(option.value)} className={`rounded-lg px-3 py-1.5 text-sm transition-all ${ periodFilter === option.value ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > {option.label} ))} handlePeriodChange('custom')} className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm transition-all ${ periodFilter === 'custom' ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > {t('admin.payments.periodCustom')} {/* Method filter dropdown */} {methodOptions.length > 0 && ( setMethodFilter(e.target.value)} className="rounded-lg border border-dark-700 bg-dark-800 px-3 py-1.5 text-sm text-dark-300 transition-colors focus:border-accent-500 focus:outline-none" > {t('admin.payments.allMethods')} {methodOptions.map((method) => ( {method} ))} )} {/* Date range panel */} {showDateRange && ( {t('admin.payments.dateFrom')} 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" /> {t('admin.payments.dateTo')} 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" /> refetch()} className="btn-primary px-4 py-2 text-sm"> {t('admin.payments.applyDate')} )} {/* Stats cards */} {stats && ( handleStatusCardClick('all')} /> handleStatusCardClick('pending')} /> handleStatusCardClick('paid')} /> handleStatusCardClick('cancelled')} /> )} {/* Payments list */} {isError ? ( {t('common.error')} refetch()} className="btn-secondary mt-3"> {t('common.retry')} ) : 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 ? ( { e.stopPropagation(); navigate(`/admin/users/${payment.user_id}`); }} className="inline-flex items-center gap-1 transition-colors hover:underline" > {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} )} ) : ( <> {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 && ( handleCheckPayment(payment)} disabled={isChecking} className="btn-primary px-3 py-1.5 text-xs" > {isChecking ? ( {t('admin.payments.checking')} ) : ( t('admin.payments.checkStatus') )} )} {/* 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 && ( setPage((prev) => Math.max(1, prev - 1))} disabled={payments.page <= 1} className={`btn-secondary min-w-[100px] flex-1 text-xs sm:flex-none sm:text-sm ${ payments.page <= 1 ? 'cursor-not-allowed opacity-50' : '' }`} > {t('admin.payments.prev')} {t('balance.page', { current: payments.page, total: payments.pages, })} setPage((prev) => Math.min(payments.pages, prev + 1))} disabled={payments.page >= payments.pages} className={`btn-secondary min-w-[100px] flex-1 text-xs sm:flex-none sm:text-sm ${ payments.page >= payments.pages ? 'cursor-not-allowed opacity-50' : '' }`} > {t('admin.payments.next')} )} ); }
{t('admin.payments.description')}
{t('admin.payments.searchHint')}
{payment.identifier}