mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-13 16:23:08 +00:00
Merge pull request #543 from Case211/feat/promocode-traffic-bonus
This commit is contained in:
@@ -17,6 +17,8 @@ export interface PromoCode {
|
|||||||
balance_bonus_kopeks: number;
|
balance_bonus_kopeks: number;
|
||||||
balance_bonus_rubles: number;
|
balance_bonus_rubles: number;
|
||||||
subscription_days: number;
|
subscription_days: number;
|
||||||
|
/** Гигабайты к подписке — третья составляющая набора бонусов. */
|
||||||
|
traffic_gb: number;
|
||||||
max_uses: number;
|
max_uses: number;
|
||||||
current_uses: number;
|
current_uses: number;
|
||||||
uses_left: number;
|
uses_left: number;
|
||||||
@@ -60,6 +62,7 @@ export interface PromoCodeCreateRequest {
|
|||||||
type: PromoCodeType;
|
type: PromoCodeType;
|
||||||
balance_bonus_kopeks?: number;
|
balance_bonus_kopeks?: number;
|
||||||
subscription_days?: number;
|
subscription_days?: number;
|
||||||
|
traffic_gb?: number;
|
||||||
max_uses?: number;
|
max_uses?: number;
|
||||||
valid_from?: string;
|
valid_from?: string;
|
||||||
valid_until?: string | null;
|
valid_until?: string | null;
|
||||||
@@ -74,6 +77,7 @@ export interface PromoCodeUpdateRequest {
|
|||||||
type?: PromoCodeType;
|
type?: PromoCodeType;
|
||||||
balance_bonus_kopeks?: number;
|
balance_bonus_kopeks?: number;
|
||||||
subscription_days?: number;
|
subscription_days?: number;
|
||||||
|
traffic_gb?: number;
|
||||||
max_uses?: number;
|
max_uses?: number;
|
||||||
valid_from?: string;
|
valid_from?: string;
|
||||||
valid_until?: string | null;
|
valid_until?: string | null;
|
||||||
|
|||||||
@@ -3303,7 +3303,10 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"defaultTrialTariff": "Default trial tariff",
|
"defaultTrialTariff": "Default trial tariff",
|
||||||
"selectTariff": "Select tariff",
|
"selectTariff": "Select tariff",
|
||||||
"tariff": "Tariff"
|
"tariff": "Tariff",
|
||||||
|
"includeTraffic": "Traffic",
|
||||||
|
"trafficAmount": "Traffic amount",
|
||||||
|
"gb": "GB"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Promo code statistics",
|
"title": "Promo code statistics",
|
||||||
@@ -3377,7 +3380,8 @@
|
|||||||
"daysRequired": "Number of days must be greater than 0",
|
"daysRequired": "Number of days must be greater than 0",
|
||||||
"groupRequired": "Select a discount group",
|
"groupRequired": "Select a discount group",
|
||||||
"discountPercentInvalid": "Discount percent must be between 1 and 100",
|
"discountPercentInvalid": "Discount percent must be between 1 and 100",
|
||||||
"discountHoursRequired": "Specify the discount validity period in hours"
|
"discountHoursRequired": "Specify the discount validity period in hours",
|
||||||
|
"trafficRequired": "Traffic amount must be greater than 0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"promoGroups": {
|
"promoGroups": {
|
||||||
|
|||||||
@@ -2813,7 +2813,10 @@
|
|||||||
"save": "ذخیره",
|
"save": "ذخیره",
|
||||||
"defaultTrialTariff": "تعرفه پیشفرض آزمایشی",
|
"defaultTrialTariff": "تعرفه پیشفرض آزمایشی",
|
||||||
"selectTariff": "تعرفه را انتخاب کنید",
|
"selectTariff": "تعرفه را انتخاب کنید",
|
||||||
"tariff": "تعرفه"
|
"tariff": "تعرفه",
|
||||||
|
"includeTraffic": "ترافیک",
|
||||||
|
"trafficAmount": "حجم ترافیک",
|
||||||
|
"gb": "گیگابایت"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "آمار کد تخفیف",
|
"title": "آمار کد تخفیف",
|
||||||
@@ -2885,7 +2888,8 @@
|
|||||||
"daysRequired": "تعداد روزها باید بیشتر از 0 باشد",
|
"daysRequired": "تعداد روزها باید بیشتر از 0 باشد",
|
||||||
"groupRequired": "یک گروه تخفیف انتخاب کنید",
|
"groupRequired": "یک گروه تخفیف انتخاب کنید",
|
||||||
"discountPercentInvalid": "درصد تخفیف باید بین 1 تا 100 باشد",
|
"discountPercentInvalid": "درصد تخفیف باید بین 1 تا 100 باشد",
|
||||||
"discountHoursRequired": "مدت اعتبار تخفیف را به ساعت مشخص کنید"
|
"discountHoursRequired": "مدت اعتبار تخفیف را به ساعت مشخص کنید",
|
||||||
|
"trafficRequired": "حجم ترافیک باید بیشتر از ۰ باشد"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"promoGroups": {
|
"promoGroups": {
|
||||||
|
|||||||
@@ -3697,7 +3697,10 @@
|
|||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"defaultTrialTariff": "Тариф триала по умолчанию",
|
"defaultTrialTariff": "Тариф триала по умолчанию",
|
||||||
"selectTariff": "Выберите тариф",
|
"selectTariff": "Выберите тариф",
|
||||||
"tariff": "Тариф"
|
"tariff": "Тариф",
|
||||||
|
"includeTraffic": "Трафик",
|
||||||
|
"trafficAmount": "Объём трафика",
|
||||||
|
"gb": "ГБ"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Статистика промокода",
|
"title": "Статистика промокода",
|
||||||
@@ -3773,7 +3776,8 @@
|
|||||||
"daysRequired": "Количество дней должно быть больше 0",
|
"daysRequired": "Количество дней должно быть больше 0",
|
||||||
"groupRequired": "Выберите группу скидок",
|
"groupRequired": "Выберите группу скидок",
|
||||||
"discountPercentInvalid": "Процент скидки должен быть от 1 до 100",
|
"discountPercentInvalid": "Процент скидки должен быть от 1 до 100",
|
||||||
"discountHoursRequired": "Укажите время действия скидки в часах"
|
"discountHoursRequired": "Укажите время действия скидки в часах",
|
||||||
|
"trafficRequired": "Объём трафика должен быть больше 0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"promoGroups": {
|
"promoGroups": {
|
||||||
|
|||||||
@@ -2812,7 +2812,10 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"defaultTrialTariff": "默认试用套餐",
|
"defaultTrialTariff": "默认试用套餐",
|
||||||
"selectTariff": "选择套餐",
|
"selectTariff": "选择套餐",
|
||||||
"tariff": "套餐"
|
"tariff": "套餐",
|
||||||
|
"includeTraffic": "流量",
|
||||||
|
"trafficAmount": "流量额度",
|
||||||
|
"gb": "GB"
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "促销码统计",
|
"title": "促销码统计",
|
||||||
@@ -2884,7 +2887,8 @@
|
|||||||
"daysRequired": "天数必须大于0",
|
"daysRequired": "天数必须大于0",
|
||||||
"groupRequired": "请选择折扣组",
|
"groupRequired": "请选择折扣组",
|
||||||
"discountPercentInvalid": "折扣百分比必须在1到100之间",
|
"discountPercentInvalid": "折扣百分比必须在1到100之间",
|
||||||
"discountHoursRequired": "请指定折扣有效期(小时)"
|
"discountHoursRequired": "请指定折扣有效期(小时)",
|
||||||
|
"trafficRequired": "流量额度必须大于 0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"promoGroups": {
|
"promoGroups": {
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ export default function AdminPromocodeCreate() {
|
|||||||
const [includeBalance, setIncludeBalance] = useState(true);
|
const [includeBalance, setIncludeBalance] = useState(true);
|
||||||
const [includeDays, setIncludeDays] = useState(false);
|
const [includeDays, setIncludeDays] = useState(false);
|
||||||
const [includeGroup, setIncludeGroup] = useState(false);
|
const [includeGroup, setIncludeGroup] = useState(false);
|
||||||
|
const [includeTraffic, setIncludeTraffic] = useState(false);
|
||||||
|
const [trafficGb, setTrafficGb] = useState<number | ''>(0);
|
||||||
const [balanceBonusRubles, setBalanceBonusRubles] = useState<number | ''>(0);
|
const [balanceBonusRubles, setBalanceBonusRubles] = useState<number | ''>(0);
|
||||||
const [subscriptionDays, setSubscriptionDays] = useState<number | ''>(0);
|
const [subscriptionDays, setSubscriptionDays] = useState<number | ''>(0);
|
||||||
const [maxUses, setMaxUses] = useState<number | ''>(1);
|
const [maxUses, setMaxUses] = useState<number | ''>(1);
|
||||||
@@ -85,8 +87,15 @@ export default function AdminPromocodeCreate() {
|
|||||||
setMode(data.type);
|
setMode(data.type);
|
||||||
} else {
|
} else {
|
||||||
setMode('bonus_set');
|
setMode('bonus_set');
|
||||||
setIncludeBalance(data.type === 'balance' || data.type === 'balance_and_days');
|
// Галочки поднимаем по ЗНАЧЕНИЯМ, а не по типу. С появлением трафика
|
||||||
setIncludeDays(data.type === 'subscription_days' || data.type === 'balance_and_days');
|
// balance_and_days перестал означать «есть и баланс, и дни»: этот тип
|
||||||
|
// теперь у любого набора с трафиком, в том числе с нулевым балансом и
|
||||||
|
// нулём дней. По типу такой код открывался бы с двумя чужими галочками
|
||||||
|
// на нулях, валидация требовала бы «больше 0», и Сохранить не работало
|
||||||
|
// бы, пока админ их не снимет, — а подсказка толкает вместо этого
|
||||||
|
// вписать сумму и молча добавить коду бонус, которого в нём не было.
|
||||||
|
setIncludeBalance(data.type === 'balance' || (data.balance_bonus_rubles || 0) > 0);
|
||||||
|
setIncludeDays(data.type === 'subscription_days' || (data.subscription_days || 0) > 0);
|
||||||
// Промогруппа комбинируется с любым составом (bэкенд назначает её
|
// Промогруппа комбинируется с любым составом (bэкенд назначает её
|
||||||
// независимо от типа), поэтому чекбокс — по факту наличия группы
|
// независимо от типа), поэтому чекбокс — по факту наличия группы
|
||||||
setIncludeGroup(data.type === 'promo_group' || !!data.promo_group_id);
|
setIncludeGroup(data.type === 'promo_group' || !!data.promo_group_id);
|
||||||
@@ -99,6 +108,8 @@ export default function AdminPromocodeCreate() {
|
|||||||
setBalanceBonusRubles(data.balance_bonus_rubles || 0);
|
setBalanceBonusRubles(data.balance_bonus_rubles || 0);
|
||||||
}
|
}
|
||||||
setSubscriptionDays(data.subscription_days || 0);
|
setSubscriptionDays(data.subscription_days || 0);
|
||||||
|
setTrafficGb(data.traffic_gb || 0);
|
||||||
|
setIncludeTraffic((data.traffic_gb || 0) > 0);
|
||||||
setMaxUses(data.max_uses || 1);
|
setMaxUses(data.max_uses || 1);
|
||||||
setIsActive(data.is_active ?? true);
|
setIsActive(data.is_active ?? true);
|
||||||
setFirstPurchaseOnly(data.first_purchase_only || false);
|
setFirstPurchaseOnly(data.first_purchase_only || false);
|
||||||
@@ -133,7 +144,7 @@ export default function AdminPromocodeCreate() {
|
|||||||
// едет через promo_group_id при любом типе (бэкенд применяет её независимо).
|
// едет через promo_group_id при любом типе (бэкенд применяет её независимо).
|
||||||
const derivedType: PromoCodeType =
|
const derivedType: PromoCodeType =
|
||||||
mode === 'bonus_set'
|
mode === 'bonus_set'
|
||||||
? includeBalance && includeDays
|
? includeTraffic || (includeBalance && includeDays)
|
||||||
? 'balance_and_days'
|
? 'balance_and_days'
|
||||||
: includeBalance
|
: includeBalance
|
||||||
? 'balance'
|
? 'balance'
|
||||||
@@ -148,6 +159,7 @@ export default function AdminPromocodeCreate() {
|
|||||||
const balanceValue = balanceBonusRubles === '' ? 0 : balanceBonusRubles;
|
const balanceValue = balanceBonusRubles === '' ? 0 : balanceBonusRubles;
|
||||||
const daysValue = subscriptionDays === '' ? 0 : subscriptionDays;
|
const daysValue = subscriptionDays === '' ? 0 : subscriptionDays;
|
||||||
const maxUsesValue = maxUses === '' ? 0 : maxUses;
|
const maxUsesValue = maxUses === '' ? 0 : maxUses;
|
||||||
|
const trafficValue = trafficGb === '' ? 0 : trafficGb;
|
||||||
|
|
||||||
const data: PromoCodeCreateRequest | PromoCodeUpdateRequest = {
|
const data: PromoCodeCreateRequest | PromoCodeUpdateRequest = {
|
||||||
code: code.trim().toUpperCase(),
|
code: code.trim().toUpperCase(),
|
||||||
@@ -164,6 +176,7 @@ export default function AdminPromocodeCreate() {
|
|||||||
(mode === 'bonus_set' && includeDays)
|
(mode === 'bonus_set' && includeDays)
|
||||||
? daysValue
|
? daysValue
|
||||||
: 0,
|
: 0,
|
||||||
|
traffic_gb: mode === 'bonus_set' && includeTraffic ? trafficValue : 0,
|
||||||
max_uses: maxUsesValue,
|
max_uses: maxUsesValue,
|
||||||
is_active: isActive,
|
is_active: isActive,
|
||||||
first_purchase_only: firstPurchaseOnly,
|
first_purchase_only: firstPurchaseOnly,
|
||||||
@@ -197,7 +210,7 @@ export default function AdminPromocodeCreate() {
|
|||||||
validationErrors.push('codeRequired');
|
validationErrors.push('codeRequired');
|
||||||
}
|
}
|
||||||
if (mode === 'bonus_set') {
|
if (mode === 'bonus_set') {
|
||||||
if (!includeBalance && !includeDays && !includeGroup) {
|
if (!includeBalance && !includeDays && !includeGroup && !includeTraffic) {
|
||||||
validationErrors.push('bonusSetEmpty');
|
validationErrors.push('bonusSetEmpty');
|
||||||
}
|
}
|
||||||
if (includeBalance && balanceValue <= 0) {
|
if (includeBalance && balanceValue <= 0) {
|
||||||
@@ -209,6 +222,9 @@ export default function AdminPromocodeCreate() {
|
|||||||
if (includeGroup && !promoGroupId) {
|
if (includeGroup && !promoGroupId) {
|
||||||
validationErrors.push('groupRequired');
|
validationErrors.push('groupRequired');
|
||||||
}
|
}
|
||||||
|
if (includeTraffic && (trafficGb === '' || trafficGb <= 0)) {
|
||||||
|
validationErrors.push('trafficRequired');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mode === 'trial_subscription' && daysValue <= 0) {
|
if (mode === 'trial_subscription' && daysValue <= 0) {
|
||||||
validationErrors.push('daysRequired');
|
validationErrors.push('daysRequired');
|
||||||
@@ -308,6 +324,7 @@ export default function AdminPromocodeCreate() {
|
|||||||
[
|
[
|
||||||
['includeBalance', includeBalance, setIncludeBalance] as const,
|
['includeBalance', includeBalance, setIncludeBalance] as const,
|
||||||
['includeDays', includeDays, setIncludeDays] as const,
|
['includeDays', includeDays, setIncludeDays] as const,
|
||||||
|
['includeTraffic', includeTraffic, setIncludeTraffic] as const,
|
||||||
['includePromoGroup', includeGroup, setIncludeGroup] as const,
|
['includePromoGroup', includeGroup, setIncludeGroup] as const,
|
||||||
] as const
|
] as const
|
||||||
).map(([key, checked, setChecked]) => (
|
).map(([key, checked, setChecked]) => (
|
||||||
@@ -361,6 +378,28 @@ export default function AdminPromocodeCreate() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{mode === 'bonus_set' && includeTraffic && (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="pc-traffic-gb" className="mb-2 block text-sm font-medium text-dark-300">
|
||||||
|
{t('admin.promocodes.form.trafficAmount')}
|
||||||
|
<span className="text-error-400">*</span>
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id="pc-traffic-gb"
|
||||||
|
type="number"
|
||||||
|
value={trafficGb}
|
||||||
|
onChange={createNumberInputHandler(setTrafficGb, 0)}
|
||||||
|
className="input w-32"
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
<span className="text-dark-400">{t('admin.promocodes.form.gb')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(mode === 'trial_subscription' || (mode === 'bonus_set' && includeDays)) && (
|
{(mode === 'trial_subscription' || (mode === 'bonus_set' && includeDays)) && (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="pc-sub-days" className="mb-2 block text-sm font-medium text-dark-300">
|
<label htmlFor="pc-sub-days" className="mb-2 block text-sm font-medium text-dark-300">
|
||||||
|
|||||||
@@ -196,18 +196,29 @@ export default function AdminPromocodes() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Info line */}
|
{/* Info line */}
|
||||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-dark-400">
|
||||||
{(promo.type === 'balance' || promo.type === 'balance_and_days') && (
|
{/* Составляющие показываем по значению, а не по типу:
|
||||||
|
balance_and_days теперь стоит и у набора, где баланса
|
||||||
|
или дней нет вовсе, — иначе в списке висело бы «+0 ₽
|
||||||
|
+0 дн.» у кода, который на самом деле раздаёт трафик. */}
|
||||||
|
{(promo.type === 'balance' || promo.type === 'balance_and_days') &&
|
||||||
|
promo.balance_bonus_rubles > 0 && (
|
||||||
<span className="text-success-400">
|
<span className="text-success-400">
|
||||||
+{promo.balance_bonus_rubles} {t('admin.promocodes.form.rub')}
|
+{promo.balance_bonus_rubles} {t('admin.promocodes.form.rub')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{(promo.type === 'subscription_days' ||
|
{(promo.type === 'subscription_days' ||
|
||||||
promo.type === 'trial_subscription' ||
|
promo.type === 'trial_subscription' ||
|
||||||
promo.type === 'balance_and_days') && (
|
promo.type === 'balance_and_days') &&
|
||||||
|
promo.subscription_days > 0 && (
|
||||||
<span className="text-accent-400">
|
<span className="text-accent-400">
|
||||||
+{promo.subscription_days} {t('admin.promocodes.form.days')}
|
+{promo.subscription_days} {t('admin.promocodes.form.days')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{promo.type === 'balance_and_days' && (promo.traffic_gb || 0) > 0 && (
|
||||||
|
<span className="text-accent-400">
|
||||||
|
+{promo.traffic_gb} {t('admin.promocodes.form.gb')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{promo.type === 'discount' && (
|
{promo.type === 'discount' && (
|
||||||
<span className="text-pink-400">
|
<span className="text-pink-400">
|
||||||
{t('admin.promocodes.discountForHours', {
|
{t('admin.promocodes.discountForHours', {
|
||||||
|
|||||||
Reference in New Issue
Block a user