From 1027deb134d58f72f40b2f18878e8700683a4c86 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Mon, 2 Feb 2026 08:34:35 +0300 Subject: [PATCH] feat: move campaign statistics to dedicated page --- src/App.tsx | 11 + src/locales/en.json | 8 +- src/locales/fa.json | 8 +- src/locales/ru.json | 8 +- src/locales/zh.json | 8 +- src/pages/AdminCampaignStats.tsx | 378 ++++++++++++++++++++++++++++ src/pages/AdminCampaigns.tsx | 413 +------------------------------ 7 files changed, 418 insertions(+), 416 deletions(-) create mode 100644 src/pages/AdminCampaignStats.tsx diff --git a/src/App.tsx b/src/App.tsx index 50482aa..d86bd06 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,6 +47,7 @@ const AdminPromoGroups = lazy(() => import('./pages/AdminPromoGroups')); const AdminPromoGroupCreate = lazy(() => import('./pages/AdminPromoGroupCreate')); const AdminCampaigns = lazy(() => import('./pages/AdminCampaigns')); const AdminCampaignCreate = lazy(() => import('./pages/AdminCampaignCreate')); +const AdminCampaignStats = lazy(() => import('./pages/AdminCampaignStats')); const AdminUsers = lazy(() => import('./pages/AdminUsers')); const AdminPayments = lazy(() => import('./pages/AdminPayments')); const AdminPaymentMethods = lazy(() => import('./pages/AdminPaymentMethods')); @@ -456,6 +457,16 @@ function App() { } /> + + + + + + } + /> ( + + + +); + +const LinkIcon = () => ( + + + +); + +const UsersIcon = () => ( + + + +); + +const ChartIcon = () => ( + + + +); + +// Bonus type config +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-purple-400', + bgColor: 'bg-purple-500/20', + }, + none: { + labelKey: 'admin.campaigns.bonusType.none', + color: 'text-dark-400', + bgColor: 'bg-dark-600', + }, +}; + +// Helper functions +const formatRubles = (kopeks: number): string => { + const rubles = kopeks / 100; + return `${rubles.toLocaleString('ru-RU')} ₽`; +}; + +const formatDate = (date: string | null): string => { + if (!date) return '-'; + return new Date(date).toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +}; + +export default function AdminCampaignStats() { + const { t } = useTranslation(); + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [copied, setCopied] = useState(false); + const [showUsers, setShowUsers] = useState(false); + + // Fetch stats + const { + data: stats, + isLoading, + error, + } = useQuery({ + queryKey: ['campaign-stats', id], + queryFn: () => campaignsApi.getCampaignStats(Number(id)), + enabled: !!id, + }); + + // Fetch registrations when users section is open + const { data: registrationsData, isLoading: usersLoading } = useQuery({ + queryKey: ['campaign-registrations', id], + queryFn: () => campaignsApi.getCampaignRegistrations(Number(id), 1, 50), + enabled: !!id && showUsers, + }); + + const copyLink = () => { + if (stats?.deep_link) { + navigator.clipboard.writeText(stats.deep_link); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (error || !stats) { + return ( +
+
+ +

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

+
+
+

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

+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+ +
+ +
+
+

{stats.name}

+
+ + {t(bonusTypeConfig[stats.bonus_type].labelKey)} + + {stats.is_active ? ( + + {t('admin.campaigns.stats.active')} + + ) : ( + + {t('admin.campaigns.stats.inactive')} + + )} +
+
+
+
+ +
+ {/* 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.detailedStats')} +

+
+
+
+ {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)} +
+
+
+
+ + {/* Users Section */} +
+ + + {showUsers && ( +
+ {usersLoading ? ( +
+
+
+ ) : registrationsData?.registrations.length === 0 ? ( +
+ {t('admin.campaigns.stats.noUsers')} +
+ ) : ( +
+ {registrationsData?.registrations.map((reg) => ( + +
+
+ {reg.first_name || reg.username || `User #${reg.user_id}`} +
+
{reg.telegram_id}
+
+
+ {reg.has_paid && ( + + {t('admin.campaigns.stats.paid')} + + )} + {reg.has_subscription && ( + + {t('admin.campaigns.stats.hasSub')} + + )} + {formatDate(reg.created_at)} +
+ + ))} +
+ )} +
+ )} +
+
+
+ ); +} diff --git a/src/pages/AdminCampaigns.tsx b/src/pages/AdminCampaigns.tsx index cafa0f1..79078d8 100644 --- a/src/pages/AdminCampaigns.tsx +++ b/src/pages/AdminCampaigns.tsx @@ -7,12 +7,10 @@ import { campaignsApi, CampaignListItem, CampaignDetail, - CampaignStatistics, CampaignUpdateRequest, CampaignBonusType, ServerSquadInfo, TariffListItem, - CampaignRegistrationItem, } from '../api/campaigns'; import { AdminBackButton } from '../components/admin'; import { createNumberInputHandler, toNumber } from '../utils/inputHelpers'; @@ -67,36 +65,6 @@ const ChartIcon = () => ( ); -const UsersIcon = () => ( - - - -); - -const LinkIcon = () => ( - - - -); - -const CopyIcon = () => ( - - - -); - // Bonus type labels and colors const bonusTypeConfig: Record< CampaignBonusType, @@ -136,19 +104,6 @@ const formatRubles = (kopeks: number) => { ); }; -// 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 Edit Modal interface CampaignModalProps { campaign: CampaignDetail; @@ -485,340 +440,6 @@ function CampaignModal({ ); } -// 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(); @@ -826,8 +447,6 @@ export default function AdminCampaigns() { const queryClient = useQueryClient(); 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 @@ -886,15 +505,6 @@ export default function AdminCampaigns() { } }; - 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: CampaignUpdateRequest) => { if (editingCampaign) { updateMutation.mutate({ id: editingCampaign.id, data }); @@ -1011,7 +621,7 @@ export default function AdminCampaigns() {
{/* Stats */}