import { useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Cell, Funnel, FunnelChart, LabelList, ResponsiveContainer } from 'recharts'; import { adminLandingsApi, resolveLocaleDisplay, type PurchaseItemStatus, type LandingPurchaseItem, } from '../api/landings'; import { useCurrency } from '../hooks/useCurrency'; import { CHART_COMMON } from '../constants/charts'; import { AdminBackButton } from '../components/admin'; import { StatCard } from '../components/stats'; import { BreakdownList } from '../components/sales-stats/BreakdownList'; import { DonutChart } from '../components/sales-stats/DonutChart'; import { SimpleAreaChart } from '../components/sales-stats/SimpleAreaChart'; import { MultiSeriesAreaChart } from '../components/sales-stats/MultiSeriesAreaChart'; import { ChartIcon, EmailIcon, TelegramSmallIcon, ArrowRightIcon, GiftIcon, EyeIcon, CheckCircleIcon, BanknotesIcon, PercentIcon, TicketIcon, CardIcon, WalletIcon, ChevronLeftIcon as ChevronLeftSmall, ChevronRightIcon as ChevronRightSmall, } from '@/components/icons'; const FUNNEL_COLORS = ['#f59e0b', '#34d399']; const GIFT_DONUT = { regular: '#818cf8', gift: '#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; // 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', }); const timeStr = new Date(item.created_at).toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit', }); const referrerHost = item.referrer ? (() => { try { return new URL(item.referrer).hostname; } catch { return item.referrer; } })() : null; 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')}
)} {/* Referrer */} {referrerHost && (
{referrerHost}
)} {/* Date + Time */}
{dateStr} {timeStr}
); } 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 divisor = CHART_COMMON.KOPEKS_DIVISOR; const money = (kopeks: number) => formatWithCurrency(kopeks / divisor); // 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; // Daily counts (created / paid) — flat data for MultiSeriesAreaChart const dailyCounts = useMemo(() => { if (!stats) return []; const createdLabel = t('admin.landings.stats.created', 'Created'); const paidLabel = t('admin.landings.stats.paid', 'Paid'); return stats.daily_stats.flatMap((d) => [ { date: d.date, key: createdLabel, value: d.created }, { date: d.date, key: paidLabel, value: d.purchases }, ]); }, [stats, t]); // Daily revenue — for SimpleAreaChart const dailyRevenue = useMemo(() => { if (!stats) return []; return stats.daily_stats.map((d) => ({ date: d.date, value: d.revenue_kopeks / divisor })); }, [stats, divisor]); // Tariff breakdown (by revenue) const tariffItems = useMemo( () => (stats?.tariff_stats ?? []).map((t2) => ({ key: String(t2.tariff_id ?? t2.tariff_name), label: t2.tariff_name, value: t2.revenue_kopeks / divisor, })), [stats, divisor], ); // Payment method breakdown (by purchases) const paymentDonut = useMemo( () => (stats?.payment_method_stats ?? []).map((p) => ({ name: p.method, value: p.purchases })), [stats], ); // Traffic source breakdown (by purchases) const sourceItems = useMemo( () => (stats?.source_stats ?? []).map((s) => ({ key: s.source, label: s.source, value: s.purchases, })), [stats], ); // Funnel: created -> paid const funnelData = useMemo(() => { if (!stats) return []; return [ { name: t('admin.landings.stats.created', 'Created'), value: stats.total_created }, { name: t('admin.landings.stats.paid', 'Paid'), value: stats.total_successful }, ]; }, [stats, t]); // Loading state if (isLoading) { return (
); } // Error state if (error || !stats) { return (

{t('admin.landings.stats.title')}

{t('admin.landings.stats.loadError')}

); } const landingTitle = landing ? resolveLocaleDisplay(landing.title) : `#${numericId}`; const giftsClaimed = stats.total_gifts_claimed ?? 0; const giftClaimRate = stats.total_gifts > 0 ? Math.round((giftsClaimed / stats.total_gifts) * 100) : 0; return (
{/* Header */}

{landingTitle}

{landing?.is_active ? ( {t('admin.landings.active')} ) : ( {t('admin.landings.inactive')} )}
{/* Summary Cards */}
} tone="warning" /> } tone="success" /> } tone="success" /> } tone="accent" />
{/* Breakdown Cards */}
} tone="accent" /> } /> } tone="accent" /> } />
{/* Funnel + gift activation */}

{t('admin.landings.stats.funnelTitle', 'Conversion funnel')}

{stats.total_created === 0 ? (
{t('admin.landings.stats.noPurchases')}
) : ( <> {/* Center value labels (dark text reads on the light segments) — left/right labels can clip in the half-width column. */} {funnelData.map((_, i) => ( ))}
{funnelData.map((s, i) => ( {s.name}: {s.value} ))} {stats.conversion_rate}%
)}

{t('admin.landings.stats.giftClaimTitle', 'Gift activation')}

{giftsClaimed} / {stats.total_gifts}
{t('admin.landings.stats.giftClaimLabel', 'claimed / sent')}
{giftClaimRate}%
{/* Daily charts */}
{/* Tariff + payment method */}
formatWithCurrency(v)} />
{/* Source + gift composition */}
{/* Purchases List */}
{/* Header row: title + status filter */}

{t('admin.landings.purchases.title')}

{/* Content */} {purchasesLoading ? (
) : purchaseItems.length === 0 ? (
{t('admin.landings.purchases.noPurchases')}
) : ( <>
{purchaseItems.map((item) => ( money(kopeks)} 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, })}
{t('admin.landings.purchases.page', { current: purchaseCurrentPage, total: purchaseTotalPages, })}
)} )}
); }