mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Merge PR #493: состав промокода чекбоксами — баланс / дни / промогруппа
This commit is contained in:
@@ -7,7 +7,8 @@ export type PromoCodeType =
|
||||
| 'subscription_days'
|
||||
| 'trial_subscription'
|
||||
| 'promo_group'
|
||||
| 'discount';
|
||||
| 'discount'
|
||||
| 'balance_and_days';
|
||||
|
||||
export interface PromoCode {
|
||||
id: number;
|
||||
|
||||
@@ -3144,7 +3144,8 @@
|
||||
"subscriptionDays": "Subscription days",
|
||||
"trialSubscription": "Trial subscription",
|
||||
"promoGroup": "Discount group",
|
||||
"discount": "Discount %"
|
||||
"discount": "Discount %",
|
||||
"balanceAndDays": "Balance + days"
|
||||
},
|
||||
"modal": {
|
||||
"editPromocode": "Edit promo code",
|
||||
@@ -3162,6 +3163,11 @@
|
||||
"typeTrialSubscription": "Trial subscription",
|
||||
"typePromoGroup": "Discount group",
|
||||
"typeDiscount": "Percentage discount",
|
||||
"typeBonusSet": "Bonus set (balance / days / promo group)",
|
||||
"bonusComposition": "Promo code contents",
|
||||
"includeBalance": "Balance bonus",
|
||||
"includeDays": "Subscription days",
|
||||
"includePromoGroup": "Promo group",
|
||||
"bonusAmount": "Bonus amount",
|
||||
"rub": "RUB",
|
||||
"daysCount": "Number of days",
|
||||
@@ -3253,6 +3259,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"codeRequired": "Enter promo code",
|
||||
"bonusSetEmpty": "Select at least one bonus for the promo code",
|
||||
"balanceRequired": "Bonus amount must be greater than 0",
|
||||
"daysRequired": "Number of days must be greater than 0",
|
||||
"groupRequired": "Select a discount group",
|
||||
|
||||
@@ -2669,7 +2669,8 @@
|
||||
"subscriptionDays": "روزهای اشتراک",
|
||||
"trialSubscription": "اشتراک آزمایشی",
|
||||
"promoGroup": "گروه تخفیف",
|
||||
"discount": "تخفیف %"
|
||||
"discount": "تخفیف %",
|
||||
"balanceAndDays": "موجودی + روزها"
|
||||
},
|
||||
"modal": {
|
||||
"editPromocode": "ویرایش کد تخفیف",
|
||||
@@ -2687,6 +2688,11 @@
|
||||
"typeTrialSubscription": "اشتراک آزمایشی",
|
||||
"typePromoGroup": "گروه تخفیف",
|
||||
"typeDiscount": "تخفیف درصدی",
|
||||
"typeBonusSet": "مجموعه پاداش (موجودی / روزها / گروه تخفیف)",
|
||||
"bonusComposition": "محتوای کد تخفیف",
|
||||
"includeBalance": "پاداش موجودی",
|
||||
"includeDays": "روزهای اشتراک",
|
||||
"includePromoGroup": "گروه تخفیف",
|
||||
"bonusAmount": "مبلغ پاداش",
|
||||
"rub": "روبل",
|
||||
"daysCount": "تعداد روزها",
|
||||
@@ -2776,6 +2782,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"codeRequired": "کد تخفیف را وارد کنید",
|
||||
"bonusSetEmpty": "حداقل یک پاداش برای کد تخفیف انتخاب کنید",
|
||||
"balanceRequired": "مبلغ پاداش باید بیشتر از 0 باشد",
|
||||
"daysRequired": "تعداد روزها باید بیشتر از 0 باشد",
|
||||
"groupRequired": "یک گروه تخفیف انتخاب کنید",
|
||||
|
||||
@@ -3538,7 +3538,8 @@
|
||||
"subscriptionDays": "Дни подписки",
|
||||
"trialSubscription": "Тестовая подписка",
|
||||
"promoGroup": "Группа скидок",
|
||||
"discount": "Скидка %"
|
||||
"discount": "Скидка %",
|
||||
"balanceAndDays": "Баланс + дни"
|
||||
},
|
||||
"modal": {
|
||||
"editPromocode": "Редактирование промокода",
|
||||
@@ -3556,6 +3557,11 @@
|
||||
"typeTrialSubscription": "Тестовая подписка",
|
||||
"typePromoGroup": "Группа скидок",
|
||||
"typeDiscount": "Процентная скидка",
|
||||
"typeBonusSet": "Набор бонусов (баланс / дни / промогруппа)",
|
||||
"bonusComposition": "Состав промокода",
|
||||
"includeBalance": "Бонус на баланс",
|
||||
"includeDays": "Дни подписки",
|
||||
"includePromoGroup": "Промогруппа",
|
||||
"bonusAmount": "Сумма бонуса",
|
||||
"rub": "руб.",
|
||||
"daysCount": "Количество дней",
|
||||
@@ -3649,6 +3655,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"codeRequired": "Введите код промокода",
|
||||
"bonusSetEmpty": "Выберите хотя бы один бонус в составе промокода",
|
||||
"balanceRequired": "Сумма бонуса должна быть больше 0",
|
||||
"daysRequired": "Количество дней должно быть больше 0",
|
||||
"groupRequired": "Выберите группу скидок",
|
||||
|
||||
@@ -2668,7 +2668,8 @@
|
||||
"subscriptionDays": "订阅天数",
|
||||
"trialSubscription": "试用订阅",
|
||||
"promoGroup": "折扣组",
|
||||
"discount": "折扣 %"
|
||||
"discount": "折扣 %",
|
||||
"balanceAndDays": "余额 + 天数"
|
||||
},
|
||||
"modal": {
|
||||
"editPromocode": "编辑促销码",
|
||||
@@ -2686,6 +2687,11 @@
|
||||
"typeTrialSubscription": "试用订阅",
|
||||
"typePromoGroup": "折扣组",
|
||||
"typeDiscount": "百分比折扣",
|
||||
"typeBonusSet": "奖励组合(余额 / 天数 / 优惠组)",
|
||||
"bonusComposition": "促销码内容",
|
||||
"includeBalance": "余额奖励",
|
||||
"includeDays": "订阅天数",
|
||||
"includePromoGroup": "优惠组",
|
||||
"bonusAmount": "奖金金额",
|
||||
"rub": "卢布",
|
||||
"daysCount": "天数",
|
||||
@@ -2775,6 +2781,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"codeRequired": "请输入促销码",
|
||||
"bonusSetEmpty": "请为促销码至少选择一项奖励",
|
||||
"balanceRequired": "奖金金额必须大于0",
|
||||
"daysRequired": "天数必须大于0",
|
||||
"groupRequired": "请选择折扣组",
|
||||
|
||||
@@ -6,11 +6,11 @@ import { DateField } from '../components/DateField';
|
||||
import { createNumberInputHandler } from '../utils/inputHelpers';
|
||||
import {
|
||||
promocodesApi,
|
||||
PromoCodeDetail,
|
||||
PromoCodeType,
|
||||
PromoCodeCreateRequest,
|
||||
PromoCodeUpdateRequest,
|
||||
PromoGroup,
|
||||
type PromoCodeDetail,
|
||||
type PromoCodeType,
|
||||
type PromoCodeCreateRequest,
|
||||
type PromoCodeUpdateRequest,
|
||||
type PromoGroup,
|
||||
} from '../api/promocodes';
|
||||
import { tariffsApi } from '../api/tariffs';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
@@ -34,9 +34,16 @@ export default function AdminPromocodeCreate() {
|
||||
const { capabilities } = usePlatform();
|
||||
const isEdit = !!id;
|
||||
|
||||
// Form state
|
||||
// Form state.
|
||||
// Режим формы: «набор бонусов» (баланс/дни/промогруппа чекбоксами — тип
|
||||
// промокода выводится из выбранной комбинации) либо особые типы, которые не
|
||||
// комбинируются: триал и процентная скидка (скидка переиспользует те же
|
||||
// колонки с другой семантикой — процент/часы).
|
||||
const [code, setCode] = useState('');
|
||||
const [type, setType] = useState<PromoCodeType>('balance');
|
||||
const [mode, setMode] = useState<'bonus_set' | 'trial_subscription' | 'discount'>('bonus_set');
|
||||
const [includeBalance, setIncludeBalance] = useState(true);
|
||||
const [includeDays, setIncludeDays] = useState(false);
|
||||
const [includeGroup, setIncludeGroup] = useState(false);
|
||||
const [balanceBonusRubles, setBalanceBonusRubles] = useState<number | ''>(0);
|
||||
const [subscriptionDays, setSubscriptionDays] = useState<number | ''>(0);
|
||||
const [maxUses, setMaxUses] = useState<number | ''>(1);
|
||||
@@ -58,7 +65,7 @@ export default function AdminPromocodeCreate() {
|
||||
const { data: tariffsData } = useQuery({
|
||||
queryKey: ['admin-tariffs-for-promo'],
|
||||
queryFn: () => tariffsApi.getTariffs(true),
|
||||
enabled: type === 'trial_subscription',
|
||||
enabled: mode === 'trial_subscription',
|
||||
});
|
||||
|
||||
const trialTariff = tariffsData?.tariffs?.find((t) => t.is_trial_available) || null;
|
||||
@@ -74,7 +81,16 @@ export default function AdminPromocodeCreate() {
|
||||
refetchOnWindowFocus: false,
|
||||
select: useCallback((data: PromoCodeDetail) => {
|
||||
setCode(data.code);
|
||||
setType(data.type);
|
||||
if (data.type === 'trial_subscription' || data.type === 'discount') {
|
||||
setMode(data.type);
|
||||
} else {
|
||||
setMode('bonus_set');
|
||||
setIncludeBalance(data.type === 'balance' || data.type === 'balance_and_days');
|
||||
setIncludeDays(data.type === 'subscription_days' || data.type === 'balance_and_days');
|
||||
// Промогруппа комбинируется с любым составом (bэкенд назначает её
|
||||
// независимо от типа), поэтому чекбокс — по факту наличия группы
|
||||
setIncludeGroup(data.type === 'promo_group' || !!data.promo_group_id);
|
||||
}
|
||||
// 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') {
|
||||
@@ -112,6 +128,20 @@ export default function AdminPromocodeCreate() {
|
||||
},
|
||||
});
|
||||
|
||||
// Тип промокода выводится из режима и выбранных чекбоксов. «Только
|
||||
// промогруппа» — легаси-тип promo_group; группа в комбинации с балансом/днями
|
||||
// едет через promo_group_id при любом типе (бэкенд применяет её независимо).
|
||||
const derivedType: PromoCodeType =
|
||||
mode === 'bonus_set'
|
||||
? includeBalance && includeDays
|
||||
? 'balance_and_days'
|
||||
: includeBalance
|
||||
? 'balance'
|
||||
: includeDays
|
||||
? 'subscription_days'
|
||||
: 'promo_group'
|
||||
: mode;
|
||||
|
||||
const handleSubmit = () => {
|
||||
// For discount: balance_bonus_kopeks = percent (integer), subscription_days = hours
|
||||
// For balance: balance_bonus_kopeks = rubles * 100
|
||||
@@ -121,12 +151,19 @@ export default function AdminPromocodeCreate() {
|
||||
|
||||
const data: PromoCodeCreateRequest | PromoCodeUpdateRequest = {
|
||||
code: code.trim().toUpperCase(),
|
||||
type,
|
||||
type: derivedType,
|
||||
balance_bonus_kopeks:
|
||||
type === 'discount'
|
||||
mode === 'discount'
|
||||
? Math.round(balanceValue) // percent as integer
|
||||
: Math.round(balanceValue * 100), // rubles to kopeks
|
||||
subscription_days: daysValue,
|
||||
: mode === 'bonus_set' && includeBalance
|
||||
? Math.round(balanceValue * 100) // rubles to kopeks
|
||||
: 0,
|
||||
subscription_days:
|
||||
mode === 'discount' ||
|
||||
mode === 'trial_subscription' ||
|
||||
(mode === 'bonus_set' && includeDays)
|
||||
? daysValue
|
||||
: 0,
|
||||
max_uses: maxUsesValue,
|
||||
is_active: isActive,
|
||||
first_purchase_only: firstPurchaseOnly,
|
||||
@@ -136,8 +173,8 @@ export default function AdminPromocodeCreate() {
|
||||
// (the START of the day) — for a GMT+3 admin that made a code picked for
|
||||
// "today" already expired by 3am, surfacing as a bogus "expired" error.
|
||||
valid_until: validUntil ? new Date(`${validUntil}T23:59:59`).toISOString() : null,
|
||||
promo_group_id: type === 'promo_group' ? promoGroupId : null,
|
||||
...(type === 'trial_subscription' && tariffId ? { tariff_id: tariffId } : {}),
|
||||
promo_group_id: mode === 'bonus_set' && includeGroup ? promoGroupId : null,
|
||||
...(mode === 'trial_subscription' && tariffId ? { tariff_id: tariffId } : {}),
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
@@ -154,41 +191,41 @@ export default function AdminPromocodeCreate() {
|
||||
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;
|
||||
// For discount, validity hours of 0 = "until first purchase" (perpetual) — allowed.
|
||||
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) {
|
||||
if (mode === 'bonus_set') {
|
||||
if (!includeBalance && !includeDays && !includeGroup) {
|
||||
validationErrors.push('bonusSetEmpty');
|
||||
}
|
||||
if (includeBalance && balanceValue <= 0) {
|
||||
validationErrors.push('balanceRequired');
|
||||
}
|
||||
if ((type === 'subscription_days' || type === 'trial_subscription') && daysValue <= 0) {
|
||||
if (includeDays && daysValue <= 0) {
|
||||
validationErrors.push('daysRequired');
|
||||
}
|
||||
if (type === 'promo_group' && !promoGroupId) {
|
||||
if (includeGroup && !promoGroupId) {
|
||||
validationErrors.push('groupRequired');
|
||||
}
|
||||
if (type === 'discount') {
|
||||
}
|
||||
if (mode === 'trial_subscription' && daysValue <= 0) {
|
||||
validationErrors.push('daysRequired');
|
||||
}
|
||||
if (mode === 'discount') {
|
||||
if (balanceValue <= 0 || balanceValue > 100) {
|
||||
validationErrors.push('discountPercentInvalid');
|
||||
}
|
||||
if (daysValue <= 0) {
|
||||
// 0 часов = «бессрочно до первой покупки» — разрешено (как в isValid до
|
||||
// унификации; раньше isValid и список ошибок противоречили друг другу)
|
||||
if (daysValue < 0) {
|
||||
validationErrors.push('discountHoursRequired');
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = (): boolean => validationErrors.length === 0;
|
||||
|
||||
// Loading state
|
||||
if (isEdit && isLoadingPromocode) {
|
||||
return (
|
||||
@@ -247,24 +284,59 @@ export default function AdminPromocodeCreate() {
|
||||
</label>
|
||||
<select
|
||||
id="pc-type"
|
||||
value={type}
|
||||
onChange={(e) => setType(e.target.value as PromoCodeType)}
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value as typeof mode)}
|
||||
className="input"
|
||||
>
|
||||
<option value="balance">{t('admin.promocodes.form.typeBalance')}</option>
|
||||
<option value="subscription_days">
|
||||
{t('admin.promocodes.form.typeSubscriptionDays')}
|
||||
</option>
|
||||
<option value="bonus_set">{t('admin.promocodes.form.typeBonusSet')}</option>
|
||||
<option value="trial_subscription">
|
||||
{t('admin.promocodes.form.typeTrialSubscription')}
|
||||
</option>
|
||||
<option value="promo_group">{t('admin.promocodes.form.typePromoGroup')}</option>
|
||||
<option value="discount">{t('admin.promocodes.form.typeDiscount')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Состав набора бонусов — любая комбинация чекбоксами */}
|
||||
{mode === 'bonus_set' && (
|
||||
<div>
|
||||
<div className="mb-2 block text-sm font-medium text-dark-300">
|
||||
{t('admin.promocodes.form.bonusComposition')}
|
||||
<span className="text-error-400">*</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{(
|
||||
[
|
||||
['includeBalance', includeBalance, setIncludeBalance] as const,
|
||||
['includeDays', includeDays, setIncludeDays] as const,
|
||||
['includePromoGroup', includeGroup, setIncludeGroup] as const,
|
||||
] as const
|
||||
).map(([key, checked, setChecked]) => (
|
||||
<label key={key} className="flex cursor-pointer items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChecked(!checked)}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-label={t(`admin.promocodes.form.${key}`)}
|
||||
className={`relative h-6 w-10 rounded-full transition-colors ${
|
||||
checked ? 'bg-accent-500' : 'bg-dark-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
|
||||
checked ? 'left-5' : 'left-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-dark-200">{t(`admin.promocodes.form.${key}`)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Type-specific fields */}
|
||||
{type === 'balance' && (
|
||||
{mode === 'bonus_set' && includeBalance && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pc-balance-bonus"
|
||||
@@ -289,7 +361,7 @@ export default function AdminPromocodeCreate() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(type === 'subscription_days' || type === 'trial_subscription') && (
|
||||
{(mode === 'trial_subscription' || (mode === 'bonus_set' && includeDays)) && (
|
||||
<div>
|
||||
<label htmlFor="pc-sub-days" className="mb-2 block text-sm font-medium text-dark-300">
|
||||
{t('admin.promocodes.form.daysCount')}
|
||||
@@ -310,7 +382,7 @@ export default function AdminPromocodeCreate() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'trial_subscription' && (
|
||||
{mode === 'trial_subscription' && (
|
||||
<div>
|
||||
<label htmlFor="pc-tariff" className="mb-2 block text-sm font-medium text-dark-300">
|
||||
{t('admin.promocodes.form.tariff', 'Тариф')}
|
||||
@@ -345,7 +417,7 @@ export default function AdminPromocodeCreate() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'promo_group' && (
|
||||
{mode === 'bonus_set' && includeGroup && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pc-promo-group"
|
||||
@@ -370,7 +442,7 @@ export default function AdminPromocodeCreate() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{type === 'discount' && (
|
||||
{mode === 'discount' && (
|
||||
<>
|
||||
<div>
|
||||
<label
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useParams, useNavigate } from 'react-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
import { promocodesApi, PromoCodeType } from '../api/promocodes';
|
||||
import { promocodesApi, type PromoCodeType } from '../api/promocodes';
|
||||
import { AdminBackButton } from '../components/admin';
|
||||
import { StatCard } from '../components/stats';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ const getTypeLabel = (type: PromoCodeType): string => {
|
||||
trial_subscription: i18n.t('admin.promocodes.type.trialSubscription'),
|
||||
promo_group: i18n.t('admin.promocodes.type.promoGroup'),
|
||||
discount: i18n.t('admin.promocodes.type.discount'),
|
||||
balance_and_days: i18n.t('admin.promocodes.type.balanceAndDays'),
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
@@ -33,6 +34,7 @@ const getTypeColor = (type: PromoCodeType): string => {
|
||||
trial_subscription: 'bg-accent-500/20 text-accent-400',
|
||||
promo_group: 'bg-warning-500/20 text-warning-400',
|
||||
discount: 'bg-pink-500/20 text-pink-400',
|
||||
balance_and_days: 'bg-success-500/20 text-success-400',
|
||||
};
|
||||
return colors[type] || 'bg-dark-600 text-dark-300';
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import i18n from '../i18n';
|
||||
import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes';
|
||||
import { promocodesApi, type PromoCode, type PromoCodeType } from '../api/promocodes';
|
||||
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
import {
|
||||
@@ -29,6 +29,7 @@ const getTypeLabel = (type: PromoCodeType): string => {
|
||||
trial_subscription: i18n.t('admin.promocodes.type.trialSubscription'),
|
||||
promo_group: i18n.t('admin.promocodes.type.promoGroup'),
|
||||
discount: i18n.t('admin.promocodes.type.discount'),
|
||||
balance_and_days: i18n.t('admin.promocodes.type.balanceAndDays'),
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
@@ -40,6 +41,7 @@ const getTypeColor = (type: PromoCodeType): string => {
|
||||
trial_subscription: 'bg-accent-500/20 text-accent-400',
|
||||
promo_group: 'bg-warning-500/20 text-warning-400',
|
||||
discount: 'bg-pink-500/20 text-pink-400',
|
||||
balance_and_days: 'bg-success-500/20 text-success-400',
|
||||
};
|
||||
return colors[type] || 'bg-dark-600 text-dark-300';
|
||||
};
|
||||
@@ -194,13 +196,14 @@ export default function AdminPromocodes() {
|
||||
</div>
|
||||
{/* Info line */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
|
||||
{promo.type === 'balance' && (
|
||||
{(promo.type === 'balance' || promo.type === 'balance_and_days') && (
|
||||
<span className="text-success-400">
|
||||
+{promo.balance_bonus_rubles} {t('admin.promocodes.form.rub')}
|
||||
</span>
|
||||
)}
|
||||
{(promo.type === 'subscription_days' ||
|
||||
promo.type === 'trial_subscription') && (
|
||||
promo.type === 'trial_subscription' ||
|
||||
promo.type === 'balance_and_days') && (
|
||||
<span className="text-accent-400">
|
||||
+{promo.subscription_days} {t('admin.promocodes.form.days')}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user