import { useState } from 'react'; import { useNavigate } from 'react-router'; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import i18n from '../i18n'; import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns'; import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon, ChartIcon } from '../components/icons'; import { usePlatform } from '../platform/hooks/usePlatform'; import { useFocusTrap } from '../hooks/useFocusTrap'; const PAGE_SIZE = 50; // Bonus type labels and colors const bonusTypeConfig: Record< CampaignBonusType, { labelKey: string; color: string; bgColor: string } > = { balance: { labelKey: 'admin.campaigns.bonusType.balance', color: 'text-success-400', bgColor: 'bg-success-500/20', }, subscription: { labelKey: 'admin.campaigns.bonusType.subscription', color: 'text-accent-400', bgColor: 'bg-accent-500/20', }, tariff: { labelKey: 'admin.campaigns.bonusType.tariff', color: 'text-accent-400', bgColor: 'bg-accent-500/20', }, none: { labelKey: 'admin.campaigns.bonusType.none', color: 'text-dark-400', bgColor: 'bg-dark-500/20', }, }; // Icons const BackIcon = () => ( ); // Locale mapping for formatting const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; // Format number as rubles const formatRubles = (kopeks: number) => { const locale = localeMap[i18n.language] || 'ru-RU'; return ( (kopeks / 100).toLocaleString(locale, { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + ' ₽' ); }; // Main Component export default function AdminCampaigns() { const { t } = useTranslation(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const [deleteConfirm, setDeleteConfirm] = useState(null); const deleteDialogRef = useFocusTrap(deleteConfirm !== null, { onEscape: () => setDeleteConfirm(null), }); // Queries const { data: campaignsData, isLoading, hasNextPage, fetchNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['admin-campaigns'], queryFn: ({ pageParam = 0 }) => campaignsApi.getCampaigns(true, pageParam, PAGE_SIZE), initialPageParam: 0, getNextPageParam: (lastPage, allPages) => { const loaded = allPages.reduce((sum, p) => sum + p.campaigns.length, 0); return loaded < lastPage.total ? loaded : undefined; }, }); const { data: overview } = useQuery({ queryKey: ['admin-campaigns-overview'], queryFn: () => campaignsApi.getOverview(), }); // Mutations const deleteMutation = useMutation({ mutationFn: campaignsApi.deleteCampaign, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] }); queryClient.invalidateQueries({ queryKey: ['admin-campaigns-overview'] }); setDeleteConfirm(null); }, }); const toggleMutation = useMutation({ mutationFn: campaignsApi.toggleCampaign, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] }); }, }); const campaigns = campaignsData?.pages.flatMap((p) => p.campaigns) ?? []; 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.campaigns.title')} {t('admin.campaigns.subtitle')} navigate('/admin/campaigns/create')} className="flex items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600" > {t('admin.campaigns.createButton')} {/* Overview */} {overview && ( {overview.total} {t('admin.campaigns.overview.totalCampaigns')} {overview.active} {t('admin.campaigns.overview.active')} {overview.total_registrations} {t('admin.campaigns.overview.registrations')} {formatRubles(overview.total_balance_issued_kopeks)} {t('admin.campaigns.overview.bonusesIssued')} )} {/* Campaigns List */} {isLoading ? ( ) : campaigns.length === 0 ? ( {t('admin.campaigns.noData')} ) : ( {campaigns.map((campaign: CampaignListItem) => ( {campaign.name} {t(bonusTypeConfig[campaign.bonus_type].labelKey)} {campaign.partner_name && ( {campaign.partner_name} )} {!campaign.is_active && ( {t('admin.campaigns.table.inactive')} )} ?start={campaign.start_parameter} {t('admin.campaigns.table.registrations', { count: campaign.registrations_count, })} {t('admin.campaigns.table.revenue', { amount: formatRubles(campaign.total_revenue_kopeks), })} {t('admin.campaigns.table.conversion', { rate: campaign.conversion_rate })} {/* Stats */} navigate(`/admin/campaigns/${campaign.id}/stats`)} className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100" title={t('admin.campaigns.table.statistics')} > {/* Toggle Active */} toggleMutation.mutate(campaign.id)} className={`rounded-lg p-2 transition-colors ${ campaign.is_active ? 'bg-success-500/20 text-success-400 hover:bg-success-500/30' : 'bg-dark-700 text-dark-400 hover:bg-dark-600' }`} title={ campaign.is_active ? t('admin.campaigns.table.deactivate') : t('admin.campaigns.table.activate') } > {campaign.is_active ? : } {/* Edit */} navigate(`/admin/campaigns/${campaign.id}/edit`)} className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-dark-600 hover:text-dark-100" title={t('admin.campaigns.table.edit')} > {/* Delete */} setDeleteConfirm(campaign.id)} className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400" title={t('admin.campaigns.table.delete')} disabled={campaign.registrations_count > 0} > ))} {/* Load more */} {hasNextPage && ( fetchNextPage()} disabled={isFetchingNextPage} className="flex w-full items-center justify-center gap-2 rounded-xl border border-dark-700 bg-dark-800 py-3 text-sm font-medium text-dark-300 transition-colors hover:border-dark-600 hover:text-dark-100 disabled:opacity-50" > {isFetchingNextPage ? ( ) : ( t('admin.campaigns.loadMore', 'Load more') )} )} )} {/* Delete Confirmation */} {deleteConfirm !== null && ( setDeleteConfirm(null)} aria-hidden="true" /> {t('admin.campaigns.confirm.deleteTitle')} {t('admin.campaigns.confirm.deleteText')} setDeleteConfirm(null)} className="px-4 py-2 text-dark-300 transition-colors hover:text-dark-100" > {t('admin.campaigns.confirm.cancel')} deleteMutation.mutate(deleteConfirm)} className="rounded-lg bg-error-500 px-4 py-2 text-white transition-colors hover:bg-error-600" > {t('admin.campaigns.confirm.delete')} )} ); }
{t('admin.campaigns.subtitle')}
{t('admin.campaigns.noData')}
{t('admin.campaigns.confirm.deleteText')}