import { useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import { adminLandingsApi, resolveLocaleDisplay, type PurchaseItemStatus, type LandingPurchaseItem, } from '../api/landings'; import { useCurrency } from '../hooks/useCurrency'; import { useChartColors } from '../hooks/useChartColors'; import { CHART_COMMON } from '../constants/charts'; import { AdminBackButton } from '../components/admin'; // Icons const ChartIcon = () => ( ); const TARIFF_PALETTE = ['#818cf8', '#34d399', '#f59e0b', '#ec4899', '#06b6d4', '#8b5cf6']; const GIFT_COLOR = '#a855f7'; const PURCHASE_STATUS_STYLES: Record = { pending: 'bg-warning-500/20 text-warning-400', paid: 'bg-accent-500/20 text-accent-400', delivered: 'bg-success-500/20 text-success-400', pending_activation: 'bg-accent-500/20 text-accent-400', failed: 'bg-error-500/20 text-error-400', expired: 'bg-dark-500/20 text-dark-400', }; const PURCHASE_STATUS_OPTIONS: Array = [ 'all', 'pending', 'paid', 'delivered', 'pending_activation', 'failed', 'expired', ]; const PURCHASES_PAGE_SIZE = 20; // Small icons for the purchase cards const EmailIcon = () => ( ); const TelegramSmallIcon = () => ( ); const ArrowRightIcon = () => ( ); const GiftIcon = () => ( ); const ChevronLeftSmall = () => ( ); const ChevronRightSmall = () => ( ); // Contact display helper function ContactDisplay({ type, value }: { type: 'email' | 'telegram'; value: string }) { return ( {type === 'email' ? : } {value} ); } // Purchase card component interface PurchaseCardProps { item: LandingPurchaseItem; formatPrice: (kopeks: number) => string; lang: string; t: (key: string, opts?: Record) => string; } function PurchaseCard({ item, formatPrice, lang, t }: PurchaseCardProps) { const statusStyle = PURCHASE_STATUS_STYLES[item.status] || 'bg-dark-600 text-dark-300'; const dateStr = new Date(item.created_at).toLocaleDateString(lang, { day: 'numeric', month: 'short', year: 'numeric', }); return ( {/* Mobile: stacked | Desktop: horizontal */} {/* Status badge */} {t(`admin.landings.purchases.status_${item.status}`)} {/* Contact info */} {item.is_gift && item.gift_recipient_type && item.gift_recipient_value && ( )} {/* Tariff + period */} {item.tariff_name} {' '} · {item.period_days} {t('admin.landings.purchases.days')} {/* Price */} {formatPrice(item.amount_kopeks)} {/* Payment method */} {item.payment_method} {/* Gift badge */} {item.is_gift && ( {t('admin.landings.purchases.gift')} )} {/* Date */} {dateStr} ); } export default function AdminLandingStats() { const { t, i18n } = useTranslation(); const { id } = useParams<{ id: string }>(); const numericId = id ? Number(id) : NaN; const isValidId = !isNaN(numericId); const navigate = useNavigate(); const { formatWithCurrency } = useCurrency(); const colors = useChartColors(); // Purchases list state const [purchaseOffset, setPurchaseOffset] = useState(0); const [purchaseStatusFilter, setPurchaseStatusFilter] = useState( 'all', ); // Fetch stats const { data: stats, isLoading, error, } = useQuery({ queryKey: ['landing-stats', numericId], queryFn: () => adminLandingsApi.getStats(numericId), enabled: isValidId, staleTime: CHART_COMMON.STALE_TIME, }); // Fetch landing detail for header const { data: landing } = useQuery({ queryKey: ['admin-landing', numericId], queryFn: () => adminLandingsApi.get(numericId), enabled: isValidId, staleTime: CHART_COMMON.STALE_TIME, }); // Fetch purchases list const { data: purchasesData, isLoading: purchasesLoading } = useQuery({ queryKey: [ 'landing-purchases', numericId, purchaseOffset, PURCHASES_PAGE_SIZE, purchaseStatusFilter, ], queryFn: () => adminLandingsApi.getPurchases( numericId, purchaseOffset, PURCHASES_PAGE_SIZE, purchaseStatusFilter === 'all' ? undefined : purchaseStatusFilter, ), enabled: isValidId, staleTime: CHART_COMMON.STALE_TIME, }); const purchaseItems = purchasesData?.items ?? []; const purchaseTotal = purchasesData?.total ?? 0; const purchaseTotalPages = Math.ceil(purchaseTotal / PURCHASES_PAGE_SIZE); const purchaseCurrentPage = Math.floor(purchaseOffset / PURCHASES_PAGE_SIZE) + 1; // Prepare daily chart data const dailyData = useMemo(() => { if (!stats) return []; return stats.daily_stats.map((item) => ({ label: new Date(item.date + 'T00:00:00').toLocaleDateString(i18n.language, { month: 'short', day: 'numeric', }), purchases: item.purchases, revenue: item.revenue_kopeks / CHART_COMMON.KOPEKS_DIVISOR, gifts: item.gifts, })); }, [stats, i18n.language]); // Prepare tariff chart data const tariffData = useMemo(() => { if (!stats) return []; return stats.tariff_stats.map((item) => ({ name: item.tariff_name, purchases: item.purchases, revenue: item.revenue_kopeks / CHART_COMMON.KOPEKS_DIVISOR, })); }, [stats]); // Donut data for gift vs regular const donutData = useMemo(() => { if (!stats) return []; return [ { name: t('admin.landings.stats.regular'), value: stats.total_regular, color: colors.referrals, }, { name: t('admin.landings.stats.gifts'), value: stats.total_gifts, color: GIFT_COLOR }, ]; }, [stats, t, colors.referrals]); // Bar chart height based on tariff count const barChartHeight = useMemo(() => { const count = tariffData.length; return Math.max(220, count * 45 + 40); }, [tariffData.length]); // Loading state if (isLoading) { return ( ); } // Error state if (error || !stats) { return ( {t('admin.landings.stats.title')} {t('admin.landings.stats.loadError')} navigate('/admin/landings')} className="mt-4 text-sm text-dark-400 hover:text-dark-200" > {t('common.back')} ); } const landingTitle = landing ? resolveLocaleDisplay(landing.title) : `#${numericId}`; return ( {/* Header */} {landingTitle} {landing?.is_active ? ( {t('admin.landings.active')} ) : ( {t('admin.landings.inactive')} )} {/* Summary Cards */} {stats.total_purchases} {t('admin.landings.stats.totalPurchases')} {formatWithCurrency(stats.total_revenue_kopeks / CHART_COMMON.KOPEKS_DIVISOR)} {t('admin.landings.stats.revenue')} {stats.total_gifts} {t('admin.landings.stats.giftPurchases')} {stats.conversion_rate}% {t('admin.landings.stats.conversionRate')} {/* Charts */} {/* Daily Purchases & Revenue */} {t('admin.landings.stats.dailyChart')} {dailyData.length === 0 ? ( {t('admin.landings.stats.noPurchases')} ) : ( )} {/* Tariff Distribution */} {t('admin.landings.stats.tariffChart')} {tariffData.length === 0 ? ( {t('admin.landings.stats.noPurchases')} ) : ( { return [value ?? 0, t('admin.landings.stats.purchases')]; }} /> {tariffData.map((_, index) => ( ))} )} {/* Additional Stats Row */} {t('admin.landings.stats.avgPurchase')} {formatWithCurrency(stats.avg_purchase_kopeks / CHART_COMMON.KOPEKS_DIVISOR)} {t('admin.landings.stats.regularPurchases')} {stats.total_regular} {t('admin.landings.stats.funnel')} {stats.total_created}{' '} {t('admin.landings.stats.created')} {' / '} {stats.total_successful}{' '} {t('admin.landings.stats.successful')} {/* Gift vs Regular Donut */} {stats.total_purchases > 0 && ( {t('admin.landings.stats.giftBreakdown')} {donutData.map((entry, index) => ( ))} {/* Center text */} {stats.total_purchases} {t('admin.landings.stats.regular')}: {stats.total_regular} {t('admin.landings.stats.gifts')}: {stats.total_gifts} )} {/* Purchases List */} {/* Header row: title + status filter */} {t('admin.landings.purchases.title')} { setPurchaseStatusFilter(e.target.value as PurchaseItemStatus | 'all'); setPurchaseOffset(0); }} className="rounded-lg border border-dark-600 bg-dark-900 px-3 py-1.5 text-sm text-dark-200 outline-none transition-colors focus:border-accent-500" aria-label={t('admin.landings.purchases.allStatuses')} > {PURCHASE_STATUS_OPTIONS.map((opt) => ( {opt === 'all' ? t('admin.landings.purchases.allStatuses') : t(`admin.landings.purchases.status_${opt}`)} ))} {/* Content */} {purchasesLoading ? ( ) : purchaseItems.length === 0 ? ( {t('admin.landings.purchases.noPurchases')} ) : ( <> {purchaseItems.map((item) => ( formatWithCurrency(kopeks / CHART_COMMON.KOPEKS_DIVISOR) } lang={i18n.language} t={t} /> ))} {/* Pagination */} {purchaseTotalPages > 1 && ( {t('admin.landings.purchases.showing', { from: purchaseOffset + 1, to: Math.min(purchaseOffset + PURCHASES_PAGE_SIZE, purchaseTotal), total: purchaseTotal, })} setPurchaseOffset((prev) => Math.max(0, prev - PURCHASES_PAGE_SIZE)) } disabled={purchaseOffset === 0} className="flex items-center gap-1 rounded-lg border border-dark-700 bg-dark-800 px-3 py-1.5 text-sm text-dark-300 transition-colors hover:border-dark-600 hover:text-dark-100 disabled:cursor-not-allowed disabled:opacity-40" aria-label={t('admin.landings.purchases.prev')} > {t('admin.landings.purchases.prev')} {t('admin.landings.purchases.page', { current: purchaseCurrentPage, total: purchaseTotalPages, })} setPurchaseOffset((prev) => prev + PURCHASES_PAGE_SIZE)} disabled={purchaseOffset + PURCHASES_PAGE_SIZE >= purchaseTotal} className="flex items-center gap-1 rounded-lg border border-dark-700 bg-dark-800 px-3 py-1.5 text-sm text-dark-300 transition-colors hover:border-dark-600 hover:text-dark-100 disabled:cursor-not-allowed disabled:opacity-40" aria-label={t('admin.landings.purchases.next')} > {t('admin.landings.purchases.next')} )} > )} ); }
{t('admin.landings.stats.loadError')}