import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; 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, { label: string; color: string; bgColor: string } > = { balance: { label: 'Баланс', color: 'text-emerald-400', bgColor: 'bg-emerald-500/20' }, subscription: { label: 'Подписка', color: 'text-blue-400', bgColor: 'bg-blue-500/20' }, tariff: { label: 'Тариф', color: 'text-purple-400', bgColor: 'bg-purple-500/20' }, none: { label: 'Только ссылка', color: 'text-gray-400', bgColor: 'bg-gray-500/20' }, }; // Format number as rubles const formatRubles = (kopeks: number) => (kopeks / 100).toLocaleString('ru-RU', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) + ' ₽'; // Format date const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleDateString('ru-RU', { 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 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 ? 'Редактирование кампании' : 'Новая кампания'}

{/* 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="Например: Instagram Реклама" />
{/* 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" />

Только латиница, цифры, _ и -

{/* 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' && (

Кампания без бонусов - только для отслеживания переходов и регистраций.

)} {/* Active toggle */}
Активна
{/* Footer */}
); } // Statistics Modal interface StatsModalProps { stats: CampaignStatistics; onClose: () => void; onViewUsers: () => void; } function StatsModal({ stats, onClose, onViewUsers }: StatsModalProps) { 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}

{bonusTypeConfig[stats.bonus_type].label} {stats.is_active ? ( Активна ) : ( Неактивна )}
{/* Content */}
{/* Deep Link */} {stats.deep_link && (
{stats.deep_link}
)} {/* Main Stats */}
{stats.registrations}
Регистраций
{formatRubles(stats.total_revenue_kopeks)}
Доход
{stats.paid_users_count}
Оплатили
{stats.conversion_rate}%
Конверсия
{/* Detailed Stats */}
Бонусы выданы
{stats.bonus_type === 'balance' && (
{formatRubles(stats.balance_issued_kopeks)}
)} {stats.bonus_type === 'subscription' && (
{stats.subscription_issued} подписок
)} {stats.bonus_type === 'tariff' && (
{stats.subscription_issued} тарифов
)} {stats.bonus_type === 'none' && (
-
)}
Средний доход с пользователя
{formatRubles(stats.avg_revenue_per_user_kopeks)}
Средний первый платёж
{formatRubles(stats.avg_first_payment_kopeks)}
Пробных подписок
{stats.trial_users_count} (активных: {stats.active_trials_count})
Конверсия из триала
{stats.trial_conversion_rate}%
Последняя регистрация
{formatDate(stats.last_registration)}
{/* Footer */}
); } // Users Modal interface UsersModalProps { campaignId: number; campaignName: string; onClose: () => void; } function UsersModal({ campaignId, campaignName, onClose }: UsersModalProps) { 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 */}

Пользователи кампании

{campaignName} - {total} регистраций

{/* Content */}
{isLoading ? (
) : registrations.length === 0 ? (
Нет зарегистрированных пользователей
) : ( {registrations.map((reg: CampaignRegistrationItem) => ( ))}
Пользователь Бонус Баланс Статус Дата
{reg.first_name || 'Без имени'} {reg.username && ( @{reg.username} )}
{reg.telegram_id}
{bonusTypeConfig[reg.bonus_type as CampaignBonusType]?.label || reg.bonus_type} {formatRubles(reg.user_balance_kopeks)}
{reg.has_subscription && ( VPN )} {reg.has_paid && ( Платил )}
{formatDate(reg.created_at)}
)}
{/* Pagination */} {totalPages > 1 && (
{page} из {totalPages}
)}
); } // Main Component export default function AdminCampaigns() { 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 */}

Рекламные кампании

Управление рекламными ссылками

{/* Overview */} {overview && (
{overview.total}
Всего кампаний
{overview.active}
Активных
{overview.total_registrations}
Регистраций
{formatRubles(overview.total_balance_issued_kopeks)}
Выдано бонусов
)} {/* Campaigns List */} {isLoading ? (
) : campaigns.length === 0 ? (

Нет рекламных кампаний

) : (
{campaigns.map((campaign: CampaignListItem) => (

{campaign.name}

{bonusTypeConfig[campaign.bonus_type].label} {!campaign.is_active && ( Неактивна )}
?start={campaign.start_parameter} {campaign.registrations_count} регистраций {formatRubles(campaign.total_revenue_kopeks)} доход {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 && (

Удалить кампанию?

Это действие нельзя отменить. Кампании с регистрациями удалить нельзя.

)}
); }