import { useState, useCallback } from 'react'; import { useNavigate, useParams } from 'react-router'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { createNumberInputHandler } from '../utils/inputHelpers'; import { promocodesApi, PromoCodeDetail, PromoCodeType, PromoCodeCreateRequest, PromoCodeUpdateRequest, PromoGroup, } from '../api/promocodes'; import { tariffsApi } from '../api/tariffs'; import { usePlatform } from '../platform/hooks/usePlatform'; // Icons const BackIcon = () => ( ); const RefreshIcon = () => ( ); export default function AdminPromocodeCreate() { const { t } = useTranslation(); const navigate = useNavigate(); const { id } = useParams<{ id: string }>(); const queryClient = useQueryClient(); const { capabilities } = usePlatform(); const isEdit = !!id; // Form state const [code, setCode] = useState(''); const [type, setType] = useState('balance'); const [balanceBonusRubles, setBalanceBonusRubles] = useState(0); const [subscriptionDays, setSubscriptionDays] = useState(0); const [maxUses, setMaxUses] = useState(1); const [isActive, setIsActive] = useState(true); const [firstPurchaseOnly, setFirstPurchaseOnly] = useState(false); const [validUntil, setValidUntil] = useState(''); const [promoGroupId, setPromoGroupId] = useState(null); const [tariffId, setTariffId] = useState(null); // Fetch promo groups (for promo_group type) const { data: promoGroupsData } = useQuery({ queryKey: ['admin-promo-groups'], queryFn: () => promocodesApi.getPromoGroups({ limit: 100 }), }); const promoGroups: PromoGroup[] = promoGroupsData?.items || []; // Fetch tariffs to show trial tariff info const { data: tariffsData } = useQuery({ queryKey: ['admin-tariffs-for-promo'], queryFn: () => tariffsApi.getTariffs(true), enabled: type === 'trial_subscription', }); const trialTariff = tariffsData?.tariffs?.find((t) => t.is_trial_available) || null; // Fetch promocode for editing const { isLoading: isLoadingPromocode } = useQuery({ queryKey: ['admin-promocode', id], queryFn: () => promocodesApi.getPromocode(Number(id)), enabled: isEdit, staleTime: 0, gcTime: 0, refetchOnMount: true, refetchOnWindowFocus: false, select: useCallback((data: PromoCodeDetail) => { setCode(data.code); setType(data.type); // For discount type, balance_bonus_kopeks is percentage directly // For balance type, balance_bonus_kopeks needs to be converted to rubles if (data.type === 'discount') { setBalanceBonusRubles(data.balance_bonus_kopeks); } else { setBalanceBonusRubles(data.balance_bonus_rubles || 0); } setSubscriptionDays(data.subscription_days || 0); setMaxUses(data.max_uses || 1); setIsActive(data.is_active ?? true); setFirstPurchaseOnly(data.first_purchase_only || false); setValidUntil(data.valid_until ? data.valid_until.split('T')[0] : ''); setPromoGroupId(data.promo_group_id || null); setTariffId(data.tariff_id || null); return data; }, []), }); // Create mutation const createMutation = useMutation({ mutationFn: promocodesApi.createPromocode, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promocodes'] }); navigate('/admin/promocodes'); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: PromoCodeUpdateRequest }) => promocodesApi.updatePromocode(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-promocodes'] }); navigate('/admin/promocodes'); }, }); const handleSubmit = () => { // For discount: balance_bonus_kopeks = percent (integer), subscription_days = hours // For balance: balance_bonus_kopeks = rubles * 100 const balanceValue = balanceBonusRubles === '' ? 0 : balanceBonusRubles; const daysValue = subscriptionDays === '' ? 0 : subscriptionDays; const maxUsesValue = maxUses === '' ? 0 : maxUses; const data: PromoCodeCreateRequest | PromoCodeUpdateRequest = { code: code.trim().toUpperCase(), type, balance_bonus_kopeks: type === 'discount' ? Math.round(balanceValue) // percent as integer : Math.round(balanceValue * 100), // rubles to kopeks subscription_days: daysValue, max_uses: maxUsesValue, is_active: isActive, first_purchase_only: firstPurchaseOnly, valid_until: validUntil ? new Date(validUntil).toISOString() : null, promo_group_id: type === 'promo_group' ? promoGroupId : null, ...(type === 'trial_subscription' && tariffId ? { tariff_id: tariffId } : {}), }; if (isEdit) { updateMutation.mutate({ id: Number(id), data }); } else { createMutation.mutate(data as PromoCodeCreateRequest); } }; const isLoading = createMutation.isPending || updateMutation.isPending; // Validation const isCodeValid = code.trim().length > 0; const balanceValue = balanceBonusRubles === '' ? 0 : balanceBonusRubles; const daysValue = subscriptionDays === '' ? 0 : subscriptionDays; const isValid = (): boolean => { if (!isCodeValid) return false; if (type === 'balance' && balanceValue <= 0) return false; if ((type === 'subscription_days' || type === 'trial_subscription') && daysValue <= 0) return false; if (type === 'promo_group' && !promoGroupId) return false; if (type === 'discount' && (balanceValue <= 0 || balanceValue > 100 || daysValue <= 0)) return false; return true; }; // Collect validation errors for display const validationErrors: string[] = []; if (!isCodeValid) { validationErrors.push('codeRequired'); } if (type === 'balance' && balanceValue <= 0) { validationErrors.push('balanceRequired'); } if ((type === 'subscription_days' || type === 'trial_subscription') && daysValue <= 0) { validationErrors.push('daysRequired'); } if (type === 'promo_group' && !promoGroupId) { validationErrors.push('groupRequired'); } if (type === 'discount') { if (balanceValue <= 0 || balanceValue > 100) { validationErrors.push('discountPercentInvalid'); } if (daysValue <= 0) { validationErrors.push('discountHoursRequired'); } } // Loading state if (isEdit && isLoadingPromocode) { return ( ); } return ( {/* Header */} {/* Show back button only on web, not in Telegram Mini App */} {!capabilities.hasBackButton && ( navigate('/admin/promocodes')} className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600" > )} {isEdit ? t('admin.promocodes.modal.editPromocode') : t('admin.promocodes.modal.newPromocode')} {t('admin.promocodes.subtitle')} {/* Form */} {/* Code */} {t('admin.promocodes.form.code')} * setCode(e.target.value.toUpperCase())} className={`input uppercase ${!isCodeValid && code.length > 0 ? 'border-error-500/50' : ''}`} placeholder="SUMMER2025" maxLength={50} /> {/* Type */} {t('admin.promocodes.form.type')} setType(e.target.value as PromoCodeType)} className="input" > {t('admin.promocodes.form.typeBalance')} {t('admin.promocodes.form.typeSubscriptionDays')} {t('admin.promocodes.form.typeTrialSubscription')} {t('admin.promocodes.form.typePromoGroup')} {t('admin.promocodes.form.typeDiscount')} {/* Type-specific fields */} {type === 'balance' && ( {t('admin.promocodes.form.bonusAmount')} * {t('admin.promocodes.form.rub')} )} {(type === 'subscription_days' || type === 'trial_subscription') && ( {t('admin.promocodes.form.daysCount')} * {t('admin.promocodes.form.days')} )} {type === 'trial_subscription' && ( {t('admin.promocodes.form.tariff', 'Тариф')} setTariffId(e.target.value ? parseInt(e.target.value) : null)} className="input" > {trialTariff ? t('admin.promocodes.form.defaultTrialTariff', 'По умолчанию: {{name}}', { name: trialTariff.name, }) : t('admin.promocodes.form.selectTariff', '— Выберите тариф —')} {tariffsData?.tariffs?.map((tariff) => ( {tariff.name} ({tariff.traffic_limit_gb} GB, {tariff.device_limit} устр.) ))} {!tariffId && !trialTariff && ( {t( 'admin.promocodes.form.noTrialTariffHint', 'Выберите тариф или отметьте тариф как «доступен для триала» в настройках.', )} )} )} {type === 'promo_group' && ( {t('admin.promocodes.form.discountGroup')} * setPromoGroupId(e.target.value ? parseInt(e.target.value) : null)} className="input" > {t('admin.promocodes.form.selectGroup')} {promoGroups.map((group) => ( {group.name} ))} )} {type === 'discount' && ( <> {t('admin.promocodes.form.discountPercent')} * { const val = e.target.value; if (val === '') { setBalanceBonusRubles(''); } else { setBalanceBonusRubles(Math.min(100, Math.max(0, parseFloat(val) || 0))); } }} className="input w-32" min={1} max={100} placeholder="0" /> % {t('admin.promocodes.form.discountHint')} {t('admin.promocodes.form.validityPeriod')} * { const val = e.target.value; if (val === '') { setSubscriptionDays(''); } else { setSubscriptionDays(Math.max(0, parseInt(val) || 0)); } }} className="input w-32" min={1} placeholder="0" /> {t('admin.promocodes.form.hours')} {t('admin.promocodes.form.validityHint')} > )} {/* Max Uses */} {t('admin.promocodes.form.maxUses')} {t('admin.promocodes.form.unlimitedHint')} {/* Valid Until */} {t('admin.promocodes.form.validUntil')} setValidUntil(e.target.value)} className="input" /> {t('admin.promocodes.form.validUntilHint')} {/* Options */} setIsActive(!isActive)} className={`relative h-6 w-10 rounded-full transition-colors ${ isActive ? 'bg-accent-500' : 'bg-dark-600' }`} > {t('admin.promocodes.form.active')} setFirstPurchaseOnly(!firstPurchaseOnly)} className={`relative h-6 w-10 rounded-full transition-colors ${ firstPurchaseOnly ? 'bg-accent-500' : 'bg-dark-600' }`} > {t('admin.promocodes.form.firstPurchaseOnly')} {/* Footer */} {validationErrors.length > 0 && ( {t('admin.tariffs.cannotSave')} {validationErrors.map((error) => ( {t(`admin.promocodes.validation.${error}`)} ))} )} navigate('/admin/promocodes')} className="px-4 py-2 text-dark-300 transition-colors hover:text-dark-100" > {t('admin.promocodes.form.cancel')} {isLoading && } {isLoading ? t('admin.promocodes.form.saving') : t('admin.promocodes.form.save')} ); }
{t('admin.promocodes.subtitle')}
{t('admin.promocodes.form.discountHint')}
{t('admin.promocodes.form.validityHint')}
{t('admin.promocodes.form.validUntilHint')}
{t('admin.tariffs.cannotSave')}