import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import i18n from '../i18n'; import { campaignsApi, CampaignListItem, CampaignDetail, CampaignStatistics, CampaignCreateRequest, CampaignUpdateRequest, CampaignBonusType, ServerSquadInfo, TariffListItem, CampaignRegistrationItem, } from '../api/campaigns'; // Icons const BackIcon = () => ( ); const PlusIcon = () => ( ); const EditIcon = () => ( ); const TrashIcon = () => ( ); const CheckIcon = () => ( ); const XIcon = () => ( ); const ChartIcon = () => ( ); const UsersIcon = () => ( ); const LinkIcon = () => ( ); const CopyIcon = () => ( ); // Bonus type labels and colors const bonusTypeConfig: Record< CampaignBonusType, { labelKey: string; color: string; bgColor: string } > = { balance: { labelKey: 'admin.campaigns.bonusType.balance', color: 'text-emerald-400', bgColor: 'bg-emerald-500/20', }, subscription: { labelKey: 'admin.campaigns.bonusType.subscription', color: 'text-blue-400', bgColor: 'bg-blue-500/20', }, tariff: { labelKey: 'admin.campaigns.bonusType.tariff', color: 'text-purple-400', bgColor: 'bg-purple-500/20', }, none: { labelKey: 'admin.campaigns.bonusType.none', color: 'text-gray-400', bgColor: 'bg-gray-500/20', }, }; // 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 }) + ' ₽' ); }; // Format date const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; const locale = localeMap[i18n.language] || 'ru-RU'; return new Date(dateStr).toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; // Campaign Modal interface CampaignModalProps { campaign?: CampaignDetail | null; servers: ServerSquadInfo[]; tariffs: TariffListItem[]; onSave: (data: CampaignCreateRequest | CampaignUpdateRequest) => void; onClose: () => void; isLoading?: boolean; } function CampaignModal({ campaign, servers, tariffs, onSave, onClose, isLoading, }: CampaignModalProps) { const { t } = useTranslation(); const isEdit = !!campaign; const [name, setName] = useState(campaign?.name || ''); const [startParameter, setStartParameter] = useState(campaign?.start_parameter || ''); const [bonusType, setBonusType] = useState(campaign?.bonus_type || 'balance'); const [isActive, setIsActive] = useState(campaign?.is_active ?? true); // Balance bonus const [balanceBonusRubles, setBalanceBonusRubles] = useState( (campaign?.balance_bonus_kopeks || 0) / 100, ); // Subscription bonus const [subscriptionDays, setSubscriptionDays] = useState( campaign?.subscription_duration_days || 7, ); const [subscriptionTraffic, setSubscriptionTraffic] = useState( campaign?.subscription_traffic_gb || 10, ); const [subscriptionDevices, setSubscriptionDevices] = useState( campaign?.subscription_device_limit || 1, ); const [selectedSquads, setSelectedSquads] = useState( campaign?.subscription_squads || [], ); // Tariff bonus const [tariffId, setTariffId] = useState(campaign?.tariff_id || null); const [tariffDays, setTariffDays] = useState(campaign?.tariff_duration_days || 30); const handleSubmit = () => { const data: CampaignCreateRequest | CampaignUpdateRequest = { name, start_parameter: startParameter, bonus_type: bonusType, is_active: isActive, }; if (bonusType === 'balance') { data.balance_bonus_kopeks = Math.round(balanceBonusRubles * 100); } else if (bonusType === 'subscription') { data.subscription_duration_days = subscriptionDays; data.subscription_traffic_gb = subscriptionTraffic; data.subscription_device_limit = subscriptionDevices; data.subscription_squads = selectedSquads; } else if (bonusType === 'tariff') { data.tariff_id = tariffId || undefined; data.tariff_duration_days = tariffDays; } onSave(data); }; const toggleServer = (uuid: string) => { setSelectedSquads((prev) => prev.includes(uuid) ? prev.filter((s) => s !== uuid) : [...prev, uuid], ); }; const isValid = name.trim() && startParameter.trim() && /^[a-zA-Z0-9_-]+$/.test(startParameter); return (
{/* Header */}

{isEdit ? t('admin.campaigns.modal.editTitle') : t('admin.campaigns.modal.createTitle')}

{/* Content */}
{/* Name */}
setName(e.target.value)} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" placeholder={t('admin.campaigns.form.namePlaceholder')} />
{/* Start Parameter */}
setStartParameter(e.target.value.replace(/[^a-zA-Z0-9_-]/g, ''))} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-accent-500 focus:outline-none" placeholder="instagram_jan2024" />

{t('admin.campaigns.form.startParameterHint')}

{/* Bonus Type */}
{(Object.keys(bonusTypeConfig) as CampaignBonusType[]).map((type) => ( ))}
{/* Bonus Settings */} {bonusType === 'balance' && (
setBalanceBonusRubles(Math.max(0, parseFloat(e.target.value) || 0)) } className="w-32 rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-emerald-500 focus:outline-none" min={0} step={1} />
)} {bonusType === 'subscription' && (
setSubscriptionDays(Math.max(1, parseInt(e.target.value) || 1)) } className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-blue-500 focus:outline-none" min={1} />
setSubscriptionTraffic(Math.max(0, parseInt(e.target.value) || 0)) } className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-blue-500 focus:outline-none" min={0} />
setSubscriptionDevices(Math.max(1, parseInt(e.target.value) || 1)) } className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-blue-500 focus:outline-none" min={1} />
{servers.length > 0 && (
{servers.map((server) => ( ))}
)}
)} {bonusType === 'tariff' && (
setTariffDays(Math.max(1, parseInt(e.target.value) || 1))} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-dark-100 focus:border-purple-500 focus:outline-none" min={1} />
)} {bonusType === 'none' && (

{t('admin.campaigns.form.noBonusDescription')}

)} {/* Active toggle */}
{t('admin.campaigns.form.active')}
{/* Footer */}
); } // Statistics Modal interface StatsModalProps { stats: CampaignStatistics; onClose: () => void; onViewUsers: () => void; } function StatsModal({ stats, onClose, onViewUsers }: StatsModalProps) { const { t } = useTranslation(); const [copied, setCopied] = useState(false); const copyLink = () => { if (stats.deep_link) { navigator.clipboard.writeText(stats.deep_link); setCopied(true); setTimeout(() => setCopied(false), 2000); } }; return (
{/* Header */}

{stats.name}

{t(bonusTypeConfig[stats.bonus_type].labelKey)} {stats.is_active ? ( {t('admin.campaigns.stats.active')} ) : ( {t('admin.campaigns.stats.inactive')} )}
{/* Content */}
{/* Deep Link */} {stats.deep_link && (
{stats.deep_link}
)} {/* Main Stats */}
{stats.registrations}
{t('admin.campaigns.stats.registrations')}
{formatRubles(stats.total_revenue_kopeks)}
{t('admin.campaigns.stats.revenue')}
{stats.paid_users_count}
{t('admin.campaigns.stats.paidUsers')}
{stats.conversion_rate}%
{t('admin.campaigns.stats.conversion')}
{/* Detailed Stats */}
{t('admin.campaigns.stats.bonusesIssued')}
{stats.bonus_type === 'balance' && (
{formatRubles(stats.balance_issued_kopeks)}
)} {stats.bonus_type === 'subscription' && (
{t('admin.campaigns.stats.subscriptionsIssued', { count: stats.subscription_issued, })}
)} {stats.bonus_type === 'tariff' && (
{t('admin.campaigns.stats.tariffsIssued', { count: stats.subscription_issued })}
)} {stats.bonus_type === 'none' && (
-
)}
{t('admin.campaigns.stats.avgRevenuePerUser')}
{formatRubles(stats.avg_revenue_per_user_kopeks)}
{t('admin.campaigns.stats.avgFirstPayment')}
{formatRubles(stats.avg_first_payment_kopeks)}
{t('admin.campaigns.stats.trialSubscriptions')}
{t('admin.campaigns.stats.trialCount', { total: stats.trial_users_count, active: stats.active_trials_count, })}
{t('admin.campaigns.stats.trialConversion')}
{stats.trial_conversion_rate}%
{t('admin.campaigns.stats.lastRegistration')}
{formatDate(stats.last_registration)}
{/* Footer */}
); } // Users Modal interface UsersModalProps { campaignId: number; campaignName: string; onClose: () => void; } function UsersModal({ campaignId, campaignName, onClose }: UsersModalProps) { const { t } = useTranslation(); const [page, setPage] = useState(1); const { data, isLoading } = useQuery({ queryKey: ['campaign-registrations', campaignId, page], queryFn: () => campaignsApi.getCampaignRegistrations(campaignId, page, 20), }); const registrations = data?.registrations || []; const total = data?.total || 0; const totalPages = Math.ceil(total / 20); return (
{/* Header */}

{t('admin.campaigns.users.title')}

{t('admin.campaigns.users.campaignRegistrations', { name: campaignName, count: total, })}

{/* Content */}
{isLoading ? (
) : registrations.length === 0 ? (
{t('admin.campaigns.users.noUsers')}
) : ( {registrations.map((reg: CampaignRegistrationItem) => ( ))}
{t('admin.campaigns.users.user')} {t('admin.campaigns.users.bonus')} {t('admin.campaigns.users.balance')} {t('admin.campaigns.users.status')} {t('admin.campaigns.users.date')}
{reg.first_name || t('admin.campaigns.users.noName')} {reg.username && ( @{reg.username} )}
{reg.telegram_id}
{bonusTypeConfig[reg.bonus_type as CampaignBonusType]?.labelKey ? t(bonusTypeConfig[reg.bonus_type as CampaignBonusType].labelKey) : reg.bonus_type} {formatRubles(reg.user_balance_kopeks)}
{reg.has_subscription && ( VPN )} {reg.has_paid && ( {t('admin.campaigns.users.paid')} )}
{formatDate(reg.created_at)}
)}
{/* Pagination */} {totalPages > 1 && (
{t('admin.campaigns.users.pageOf', { page, totalPages })}
)}
); } // Main Component export default function AdminCampaigns() { const { t } = useTranslation(); const queryClient = useQueryClient(); const [showModal, setShowModal] = useState(false); const [editingCampaign, setEditingCampaign] = useState(null); const [showStats, setShowStats] = useState(null); const [showUsers, setShowUsers] = useState<{ id: number; name: string } | null>(null); const [deleteConfirm, setDeleteConfirm] = useState(null); // Queries const { data: campaignsData, isLoading } = useQuery({ queryKey: ['admin-campaigns'], queryFn: () => campaignsApi.getCampaigns(true), }); const { data: overview } = useQuery({ queryKey: ['admin-campaigns-overview'], queryFn: () => campaignsApi.getOverview(), }); const { data: servers = [] } = useQuery({ queryKey: ['admin-campaigns-servers'], queryFn: () => campaignsApi.getAvailableServers(), }); const { data: tariffs = [] } = useQuery({ queryKey: ['admin-campaigns-tariffs'], queryFn: () => campaignsApi.getAvailableTariffs(), }); // Mutations const createMutation = useMutation({ mutationFn: campaignsApi.createCampaign, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] }); queryClient.invalidateQueries({ queryKey: ['admin-campaigns-overview'] }); setShowModal(false); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: CampaignUpdateRequest }) => campaignsApi.updateCampaign(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-campaigns'] }); setShowModal(false); setEditingCampaign(null); }, }); 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 handleEdit = async (campaignId: number) => { try { const detail = await campaignsApi.getCampaign(campaignId); setEditingCampaign(detail); setShowModal(true); } catch (error) { console.error('Failed to load campaign:', error); } }; const handleViewStats = async (campaignId: number) => { try { const stats = await campaignsApi.getCampaignStats(campaignId); setShowStats(stats); } catch (error) { console.error('Failed to load stats:', error); } }; const handleSave = (data: CampaignCreateRequest | CampaignUpdateRequest) => { if (editingCampaign) { updateMutation.mutate({ id: editingCampaign.id, data }); } else { createMutation.mutate(data as CampaignCreateRequest); } }; const handleCloseModal = () => { setShowModal(false); setEditingCampaign(null); }; const campaigns = campaignsData?.campaigns || []; return (
{/* Header */}

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

{t('admin.campaigns.subtitle')}

{/* 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.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 */} {/* Toggle Active */} {/* Edit */} {/* Delete */}
))}
)} {/* Create/Edit Modal */} {showModal && ( )} {/* Stats Modal */} {showStats && ( setShowStats(null)} onViewUsers={() => { setShowUsers({ id: showStats.id, name: showStats.name }); setShowStats(null); }} /> )} {/* Users Modal */} {showUsers && ( setShowUsers(null)} /> )} {/* Delete Confirmation */} {deleteConfirm !== null && (

{t('admin.campaigns.confirm.deleteTitle')}

{t('admin.campaigns.confirm.deleteText')}

)}
); }