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 { promocodesApi, PromoCode, PromoCodeDetail, PromoCodeType, PromoCodeCreateRequest, PromoCodeUpdateRequest, PromoGroup, PromoGroupCreateRequest, PromoGroupUpdateRequest, } from '../api/promocodes'; // Icons const BackIcon = () => ( ); const PlusIcon = () => ( ); const EditIcon = () => ( ); const TrashIcon = () => ( ); const CheckIcon = () => ( ); const XIcon = () => ( ); const CopyIcon = () => ( ); const UsersIcon = () => ( ); const ChartIcon = () => ( ); const ClockIcon = () => ( ); const UserIcon = () => ( ); // Helper functions const getTypeLabel = (type: PromoCodeType): string => { const labels: Record = { balance: i18n.t('admin.promocodes.type.balance'), subscription_days: i18n.t('admin.promocodes.type.subscriptionDays'), trial_subscription: i18n.t('admin.promocodes.type.trialSubscription'), promo_group: i18n.t('admin.promocodes.type.promoGroup'), discount: i18n.t('admin.promocodes.type.discount'), }; return labels[type] || type; }; const getTypeColor = (type: PromoCodeType): string => { const colors: Record = { balance: 'bg-emerald-500/20 text-emerald-400', subscription_days: 'bg-blue-500/20 text-blue-400', trial_subscription: 'bg-purple-500/20 text-purple-400', promo_group: 'bg-amber-500/20 text-amber-400', discount: 'bg-pink-500/20 text-pink-400', }; return colors[type] || 'bg-dark-600 text-dark-300'; }; const formatDate = (date: string | null): string => { if (!date) return '-'; const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; const locale = localeMap[i18n.language] || 'ru-RU'; return new Date(date).toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', }); }; const formatDateTime = (date: string | null): string => { if (!date) return '-'; const localeMap: Record = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' }; const locale = localeMap[i18n.language] || 'ru-RU'; return new Date(date).toLocaleString(locale, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); }; // Promocode Modal interface PromocodeModalProps { promocode?: PromoCodeDetail | null; promoGroups: PromoGroup[]; onSave: (data: PromoCodeCreateRequest | PromoCodeUpdateRequest) => void; onClose: () => void; isLoading?: boolean; } function PromocodeModal({ promocode, promoGroups, onSave, onClose, isLoading, }: PromocodeModalProps) { const { t } = useTranslation(); const isEdit = !!promocode; const [code, setCode] = useState(promocode?.code || ''); const [type, setType] = useState(promocode?.type || 'balance'); const [balanceBonusRubles, setBalanceBonusRubles] = useState( promocode?.balance_bonus_rubles || 0, ); const [subscriptionDays, setSubscriptionDays] = useState(promocode?.subscription_days || 0); const [maxUses, setMaxUses] = useState(promocode?.max_uses || 1); const [isActive, setIsActive] = useState(promocode?.is_active ?? true); const [firstPurchaseOnly, setFirstPurchaseOnly] = useState( promocode?.first_purchase_only || false, ); const [validUntil, setValidUntil] = useState( promocode?.valid_until ? promocode.valid_until.split('T')[0] : '', ); const [promoGroupId, setPromoGroupId] = useState( promocode?.promo_group_id || null, ); const handleSubmit = () => { // Для discount: balance_bonus_kopeks = процент (целое число), subscription_days = часы // Для balance: balance_bonus_kopeks = рубли * 100 const data: PromoCodeCreateRequest | PromoCodeUpdateRequest = { code: code.trim().toUpperCase(), type, balance_bonus_kopeks: type === 'discount' ? Math.round(balanceBonusRubles) // процент как целое число : Math.round(balanceBonusRubles * 100), // рубли в копейки subscription_days: subscriptionDays, max_uses: maxUses, is_active: isActive, first_purchase_only: firstPurchaseOnly, valid_until: validUntil ? new Date(validUntil).toISOString() : null, promo_group_id: type === 'promo_group' ? promoGroupId : null, }; onSave(data); }; const isValid = () => { if (!code.trim()) return false; if (type === 'balance' && balanceBonusRubles <= 0) return false; if ((type === 'subscription_days' || type === 'trial_subscription') && subscriptionDays <= 0) return false; if (type === 'promo_group' && !promoGroupId) return false; if ( type === 'discount' && (balanceBonusRubles <= 0 || balanceBonusRubles > 100 || subscriptionDays <= 0) ) return false; return true; }; return (
{/* Header */}

{isEdit ? t('admin.promocodes.modal.editPromocode') : t('admin.promocodes.modal.newPromocode')}

{/* Content */}
{/* Code */}
setCode(e.target.value.toUpperCase())} className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 uppercase text-dark-100 focus:border-accent-500 focus:outline-none" placeholder="SUMMER2025" />
{/* Type */}
{/* Type-specific fields */} {type === '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-accent-500 focus:outline-none" min={0} step={1} /> {t('admin.promocodes.form.rub')}
)} {(type === 'subscription_days' || type === 'trial_subscription') && (
setSubscriptionDays(Math.max(0, parseInt(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-accent-500 focus:outline-none" min={1} /> {t('admin.promocodes.form.days')}
)} {type === 'promo_group' && (
)} {type === 'discount' && ( <>
setBalanceBonusRubles( Math.min(100, 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-accent-500 focus:outline-none" min={1} max={100} /> %

{t('admin.promocodes.form.discountHint')}

setSubscriptionDays(Math.max(0, parseInt(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-accent-500 focus:outline-none" min={1} /> {t('admin.promocodes.form.hours')}

{t('admin.promocodes.form.validityHint')}

)} {/* Max Uses */}
setMaxUses(Math.max(0, parseInt(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-accent-500 focus:outline-none" min={0} /> {t('admin.promocodes.form.unlimitedHint')}
{/* Valid Until */}
setValidUntil(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" />

{t('admin.promocodes.form.validUntilHint')}

{/* Options */}
{/* Footer */}
); } // PromoGroup Modal interface PromoGroupModalProps { group?: PromoGroup | null; onSave: (data: PromoGroupCreateRequest | PromoGroupUpdateRequest) => void; onClose: () => void; isLoading?: boolean; } interface PeriodDiscount { days: number; percent: number; } function PromoGroupModal({ group, onSave, onClose, isLoading }: PromoGroupModalProps) { const { t } = useTranslation(); const isEdit = !!group; const [name, setName] = useState(group?.name || ''); const [serverDiscount, setServerDiscount] = useState(group?.server_discount_percent || 0); const [trafficDiscount, setTrafficDiscount] = useState(group?.traffic_discount_percent || 0); const [deviceDiscount, setDeviceDiscount] = useState(group?.device_discount_percent || 0); const [applyToAddons, setApplyToAddons] = useState(group?.apply_discounts_to_addons ?? true); const [autoAssignSpent, setAutoAssignSpent] = useState( group?.auto_assign_total_spent_kopeks ? group.auto_assign_total_spent_kopeks / 100 : 0, ); // Period discounts state const [periodDiscounts, setPeriodDiscounts] = useState(() => { if (group?.period_discounts && typeof group.period_discounts === 'object') { return Object.entries(group.period_discounts).map(([days, percent]) => ({ days: parseInt(days), percent: typeof percent === 'number' ? percent : 0, })); } return []; }); const addPeriodDiscount = () => { setPeriodDiscounts([...periodDiscounts, { days: 30, percent: 0 }]); }; const removePeriodDiscount = (index: number) => { setPeriodDiscounts(periodDiscounts.filter((_, i) => i !== index)); }; const updatePeriodDiscount = (index: number, field: 'days' | 'percent', value: number) => { const updated = [...periodDiscounts]; updated[index][field] = value; setPeriodDiscounts(updated); }; const handleSubmit = () => { // Convert periodDiscounts array to Record const periodDiscountsRecord: Record = {}; periodDiscounts.forEach((pd) => { if (pd.days > 0 && pd.percent > 0) { periodDiscountsRecord[pd.days] = pd.percent; } }); const data: PromoGroupCreateRequest | PromoGroupUpdateRequest = { name, server_discount_percent: serverDiscount, traffic_discount_percent: trafficDiscount, device_discount_percent: deviceDiscount, period_discounts: Object.keys(periodDiscountsRecord).length > 0 ? periodDiscountsRecord : undefined, apply_discounts_to_addons: applyToAddons, auto_assign_total_spent_kopeks: autoAssignSpent > 0 ? Math.round(autoAssignSpent * 100) : null, }; onSave(data); }; return (
{/* Header */}

{isEdit ? t('admin.promocodes.modal.editGroup') : t('admin.promocodes.modal.newGroup')}

{/* 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.promocodes.groups.namePlaceholder')} />
{/* Category Discounts */}

{t('admin.promocodes.groups.categoryDiscounts')}

{t('admin.promocodes.groups.servers')}: setServerDiscount(Math.min(100, Math.max(0, parseInt(e.target.value) || 0))) } className="w-20 rounded border border-dark-500 bg-dark-600 px-2 py-1 text-dark-100 focus:border-accent-500 focus:outline-none" min={0} max={100} /> %
{t('admin.promocodes.groups.traffic')}: setTrafficDiscount(Math.min(100, Math.max(0, parseInt(e.target.value) || 0))) } className="w-20 rounded border border-dark-500 bg-dark-600 px-2 py-1 text-dark-100 focus:border-accent-500 focus:outline-none" min={0} max={100} /> %
{t('admin.promocodes.groups.devices')}: setDeviceDiscount(Math.min(100, Math.max(0, parseInt(e.target.value) || 0))) } className="w-20 rounded border border-dark-500 bg-dark-600 px-2 py-1 text-dark-100 focus:border-accent-500 focus:outline-none" min={0} max={100} /> %
{/* Period Discounts */}

{t('admin.promocodes.groups.periodDiscounts')}

{t('admin.promocodes.groups.periodDiscountsHint')}

{periodDiscounts.length === 0 ? (

{t('admin.promocodes.groups.noPeriodDiscounts')}

) : (
{periodDiscounts.map((pd, index) => (
updatePeriodDiscount( index, 'days', Math.max(1, parseInt(e.target.value) || 1), ) } className="w-20 rounded border border-dark-500 bg-dark-600 px-2 py-1 text-dark-100 focus:border-accent-500 focus:outline-none" min={1} placeholder={t('admin.promocodes.groups.daysPlaceholder')} /> {t('admin.promocodes.groups.daysArrow')} updatePeriodDiscount( index, 'percent', Math.min(100, Math.max(0, parseInt(e.target.value) || 0)), ) } className="w-20 rounded border border-dark-500 bg-dark-600 px-2 py-1 text-dark-100 focus:border-accent-500 focus:outline-none" min={0} max={100} placeholder="%" /> %
))}
)}
{/* Auto-assign */}
setAutoAssignSpent(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-accent-500 focus:outline-none" min={0} /> {t('admin.promocodes.form.rub')}

{t('admin.promocodes.groups.autoAssignHint')}

{/* Apply to addons */}
{/* Footer */}
); } // Promocode Stats Modal interface PromocodeStatsModalProps { promocode: PromoCodeDetail; onClose: () => void; onEdit: () => void; } function PromocodeStatsModal({ promocode, onClose, onEdit }: PromocodeStatsModalProps) { const { t } = useTranslation(); return (
{/* Header */}
{promocode.code}
{getTypeLabel(promocode.type)} {!promocode.is_active && ( {t('admin.promocodes.stats.inactive')} )}
{/* Content */}
{/* Stats Cards */}
{promocode.total_uses}
{t('admin.promocodes.stats.totalUses')}
{promocode.today_uses}
{t('admin.promocodes.stats.today')}
{promocode.max_uses === 0 ? '∞' : promocode.uses_left}
{t('admin.promocodes.stats.remaining')}
{/* Details */}

{t('admin.promocodes.stats.details')}

{t('admin.promocodes.stats.type')}: {getTypeLabel(promocode.type)}
{promocode.type === 'balance' && (
{t('admin.promocodes.stats.bonus')}: +{promocode.balance_bonus_rubles} {t('admin.promocodes.form.rub')}
)} {(promocode.type === 'subscription_days' || promocode.type === 'trial_subscription') && (
{t('admin.promocodes.stats.daysLabel')}: +{promocode.subscription_days}
)} {promocode.type === 'discount' && ( <>
{t('admin.promocodes.stats.discountLabel')}: -{promocode.balance_bonus_kopeks}%
{t('admin.promocodes.stats.validFor')}: {t('admin.promocodes.stats.hoursValue', { count: promocode.subscription_days, })}
)}
{t('admin.promocodes.stats.limit')}: {promocode.current_uses}/{promocode.max_uses === 0 ? '∞' : promocode.max_uses}
{t('admin.promocodes.stats.status')}: {promocode.is_valid ? t('admin.promocodes.stats.active') : t('admin.promocodes.stats.inactive')}
{t('admin.promocodes.stats.created')}: {formatDateTime(promocode.created_at)}
{t('admin.promocodes.stats.validUntil')}: {promocode.valid_until ? formatDate(promocode.valid_until) : t('admin.promocodes.stats.unlimited')}
{promocode.first_purchase_only && (
{t('admin.promocodes.stats.restriction')}: {t('admin.promocodes.stats.firstPurchaseOnly')}
)}
{/* Usage History */}

{t('admin.promocodes.stats.usageHistory')}

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

{t('admin.promocodes.stats.noUsages')}

) : (
{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 { t } = useTranslation(); 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); const [editingGroup, setEditingGroup] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ type: 'promocode' | 'group'; id: number; } | null>(null); const [copiedCode, setCopiedCode] = useState(null); // Queries const { data: promocodesData, isLoading: promocodesLoading } = useQuery({ queryKey: ['admin-promocodes'], queryFn: () => promocodesApi.getPromocodes({ limit: 100 }), }); const { data: groupsData, isLoading: groupsLoading } = useQuery({ queryKey: ['admin-promo-groups'], queryFn: () => promocodesApi.getPromoGroups({ limit: 100 }), }); // Promocode Mutations const createPromocodeMutation = useMutation({ mutationFn: promocodesApi.createPromocode, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promocodes'] }); setShowPromocodeModal(false); }, }); const updatePromocodeMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: PromoCodeUpdateRequest }) => promocodesApi.updatePromocode(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promocodes'] }); setShowPromocodeModal(false); setEditingPromocode(null); }, }); const deletePromocodeMutation = useMutation({ mutationFn: promocodesApi.deletePromocode, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promocodes'] }); setDeleteConfirm(null); }, }); // PromoGroup Mutations const createGroupMutation = useMutation({ mutationFn: promocodesApi.createPromoGroup, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promo-groups'] }); setShowGroupModal(false); }, }); const updateGroupMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: PromoGroupUpdateRequest }) => promocodesApi.updatePromoGroup(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promo-groups'] }); setShowGroupModal(false); setEditingGroup(null); }, }); const deleteGroupMutation = useMutation({ mutationFn: promocodesApi.deletePromoGroup, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promo-groups'] }); setDeleteConfirm(null); }, }); const handleEditPromocode = async (id: number) => { try { const detail = await promocodesApi.getPromocode(id); setEditingPromocode(detail); setShowPromocodeModal(true); } catch (error) { console.error('Failed to load promocode:', error); } }; 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 }); } else { createPromocodeMutation.mutate(data as PromoCodeCreateRequest); } }; const handleSaveGroup = (data: PromoGroupCreateRequest | PromoGroupUpdateRequest) => { if (editingGroup) { updateGroupMutation.mutate({ id: editingGroup.id, data }); } else { createGroupMutation.mutate(data as PromoGroupCreateRequest); } }; const handleCopyCode = (code: string) => { navigator.clipboard.writeText(code); setCopiedCode(code); setTimeout(() => setCopiedCode(null), 2000); }; const handleDelete = () => { if (!deleteConfirm) return; if (deleteConfirm.type === 'promocode') { deletePromocodeMutation.mutate(deleteConfirm.id); } else { deleteGroupMutation.mutate(deleteConfirm.id); } }; const promocodes = promocodesData?.items || []; const groups = groupsData?.items || []; return (
{/* Header */}

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

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

{/* Stats Overview */} {activeTab === 'promocodes' && promocodes.length > 0 && (
{promocodes.length}
{t('admin.promocodes.stats.totalPromocodes')}
{promocodes.filter((p) => p.is_active && p.is_valid).length}
{t('admin.promocodes.stats.activeCount')}
{promocodes.reduce((sum, p) => sum + p.current_uses, 0)}
{t('admin.promocodes.stats.usagesCount')}
{promocodes.filter((p) => p.uses_left === 0 && p.max_uses > 0).length}
{t('admin.promocodes.stats.exhausted')}
)} {/* Tabs */}
{/* Promocodes List */} {activeTab === 'promocodes' && ( <> {promocodesLoading ? (
) : promocodes.length === 0 ? (

{t('admin.promocodes.noPromocodes')}

) : (
{promocodes.map((promo: PromoCode) => (
{getTypeLabel(promo.type)} {!promo.is_active && ( {t('admin.promocodes.stats.inactive')} )} {promo.first_purchase_only && ( {t('admin.promocodes.firstPurchase')} )}
{promo.type === 'balance' && ( +{promo.balance_bonus_rubles} {t('admin.promocodes.form.rub')} )} {(promo.type === 'subscription_days' || promo.type === 'trial_subscription') && ( +{promo.subscription_days} {t('admin.promocodes.form.days')} )} {promo.type === 'discount' && ( {t('admin.promocodes.discountForHours', { percent: promo.balance_bonus_kopeks, hours: promo.subscription_days, })} )} {t('admin.promocodes.used')}: {promo.current_uses}/ {promo.max_uses === 0 ? '∞' : promo.max_uses} {promo.valid_until && ( {t('admin.promocodes.until')}: {formatDate(promo.valid_until)} )}
))}
)} )} {/* Groups List */} {activeTab === 'groups' && ( <> {groupsLoading ? (
) : groups.length === 0 ? (

{t('admin.promocodes.noGroups')}

) : (
{groups.map((group: PromoGroup) => (

{group.name}

{group.is_default && ( {t('admin.promocodes.groups.default')} )}
{group.server_discount_percent > 0 && ( {t('admin.promocodes.groups.servers')}: -{group.server_discount_percent} % )} {group.traffic_discount_percent > 0 && ( {t('admin.promocodes.groups.traffic')}: - {group.traffic_discount_percent}% )} {group.device_discount_percent > 0 && ( {t('admin.promocodes.groups.devices')}: -{group.device_discount_percent} % )} {group.period_discounts && Object.keys(group.period_discounts).length > 0 && Object.entries(group.period_discounts).map(([days, percent]) => ( {t('admin.promocodes.groups.daysShort', { days })}: -{percent}% ))} {group.auto_assign_total_spent_kopeks && group.auto_assign_total_spent_kopeks > 0 && ( {t('admin.promocodes.groups.autoFrom', { amount: group.auto_assign_total_spent_kopeks / 100, })} )} {t('admin.promocodes.groups.members', { count: group.members_count })}
))}
)} )} {/* Promocode Modal */} {showPromocodeModal && ( { setShowPromocodeModal(false); setEditingPromocode(null); }} isLoading={createPromocodeMutation.isPending || updatePromocodeMutation.isPending} /> )} {/* PromoGroup Modal */} {showGroupModal && ( { setShowGroupModal(false); setEditingGroup(null); }} isLoading={createGroupMutation.isPending || updateGroupMutation.isPending} /> )} {/* Promocode Stats Modal */} {viewingPromocode && ( setViewingPromocode(null)} onEdit={() => { setEditingPromocode(viewingPromocode); setViewingPromocode(null); setShowPromocodeModal(true); }} /> )} {/* Delete Confirmation */} {deleteConfirm && (

{deleteConfirm.type === 'promocode' ? t('admin.promocodes.confirm.deletePromocode') : t('admin.promocodes.confirm.deleteGroup')}

{deleteConfirm.type === 'promocode' ? t('admin.promocodes.confirm.deletePromocodeText') : t('admin.promocodes.confirm.deleteGroupText')}

)}
); }