diff --git a/src/pages/AdminPromocodes.tsx b/src/pages/AdminPromocodes.tsx index 06944fb..12575cd 100644 --- a/src/pages/AdminPromocodes.tsx +++ b/src/pages/AdminPromocodes.tsx @@ -61,6 +61,24 @@ const UsersIcon = () => ( ) +const ChartIcon = () => ( + + + +) + +const ClockIcon = () => ( + + + +) + +const UserIcon = () => ( + + + +) + // Helper functions const getTypeLabel = (type: PromoCodeType): string => { const labels: Record = { @@ -91,6 +109,17 @@ const formatDate = (date: string | null): string => { }) } +const formatDateTime = (date: string | null): string => { + if (!date) return '-' + return new Date(date).toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) +} + // Promocode Modal interface PromocodeModalProps { promocode?: PromoCodeDetail | null @@ -477,10 +506,170 @@ function PromoGroupModal({ group, onSave, onClose, isLoading }: PromoGroupModalP ) } +// Promocode Stats Modal +interface PromocodeStatsModalProps { + promocode: PromoCodeDetail + onClose: () => void + onEdit: () => void +} + +function PromocodeStatsModal({ promocode, onClose, onEdit }: PromocodeStatsModalProps) { + return ( +
+
+ {/* Header */} +
+
+
+ {promocode.code} +
+ + {getTypeLabel(promocode.type)} + + {!promocode.is_active && ( + + Неактивен + + )} +
+ +
+ + {/* Content */} +
+ {/* Stats Cards */} +
+
+
{promocode.total_uses}
+
Всего использований
+
+
+
{promocode.today_uses}
+
Сегодня
+
+
+
+ {promocode.max_uses === 0 ? '∞' : promocode.uses_left} +
+
Осталось
+
+
+ + {/* Details */} +
+

Детали промокода

+
+
+ Тип: + {getTypeLabel(promocode.type)} +
+ {promocode.type === 'balance' && ( +
+ Бонус: + +{promocode.balance_bonus_rubles} руб. +
+ )} + {(promocode.type === 'subscription_days' || promocode.type === 'trial_subscription') && ( +
+ Дней: + +{promocode.subscription_days} +
+ )} +
+ Лимит: + + {promocode.current_uses}/{promocode.max_uses === 0 ? '∞' : promocode.max_uses} + +
+
+ Статус: + + {promocode.is_valid ? 'Активен' : 'Неактивен'} + +
+
+ Создан: + {formatDateTime(promocode.created_at)} +
+
+ Действует до: + {promocode.valid_until ? formatDate(promocode.valid_until) : 'Бессрочно'} +
+ {promocode.first_purchase_only && ( +
+ Ограничение: + Только первая покупка +
+ )} +
+
+ + {/* Usage History */} +
+

+ + История использования +

+ {promocode.recent_uses.length === 0 ? ( +

Промокод еще не использовался

+ ) : ( +
+ {promocode.recent_uses.map((use) => ( +
+
+
+ +
+
+
+ {use.user_full_name || use.user_username || `User #${use.user_id}`} +
+ {use.user_username && ( +
@{use.user_username}
+ )} +
+
+
+ {formatDateTime(use.used_at)} +
+
+ ))} +
+ )} +
+
+ + {/* Footer */} +
+ + +
+
+
+ ) +} + export default function AdminPromocodes() { const queryClient = useQueryClient() const [activeTab, setActiveTab] = useState<'promocodes' | 'groups'>('promocodes') + const [viewingPromocode, setViewingPromocode] = useState(null) const [showPromocodeModal, setShowPromocodeModal] = useState(false) const [showGroupModal, setShowGroupModal] = useState(false) const [editingPromocode, setEditingPromocode] = useState(null) @@ -563,6 +752,15 @@ export default function AdminPromocodes() { } } + const handleViewStats = async (id: number) => { + try { + const detail = await promocodesApi.getPromocode(id) + setViewingPromocode(detail) + } catch (error) { + console.error('Failed to load promocode stats:', error) + } + } + const handleSavePromocode = (data: PromoCodeCreateRequest | PromoCodeUpdateRequest) => { if (editingPromocode) { updatePromocodeMutation.mutate({ id: editingPromocode.id, data }) @@ -627,6 +825,34 @@ export default function AdminPromocodes() { + {/* Stats Overview */} + {activeTab === 'promocodes' && promocodes.length > 0 && ( +
+
+
{promocodes.length}
+
Всего промокодов
+
+
+
+ {promocodes.filter(p => p.is_active && p.is_valid).length} +
+
Активных
+
+
+
+ {promocodes.reduce((sum, p) => sum + p.current_uses, 0)} +
+
Использований
+
+
+
+ {promocodes.filter(p => p.uses_left === 0 && p.max_uses > 0).length} +
+
Исчерпано
+
+
+ )} + {/* Tabs */}