feat(coupons): раздел оптовых купонов + публичная страница активации

UI для купонов из BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3042
(API: PR #3044 бота):

- /admin/coupons — список партий с погашениями и offset-пагинацией;
- /admin/coupons/create — мастер партии (тариф, дни, количество,
  учётная оптовая цена, срок), после создания — экран со ссылками
  (копирование всех, скачивание .txt);
- /admin/coupons/:id — карточка со статистикой, экспорт актуальных
  ссылок, отзыв непогашенных за PermissionGate coupons:edit
  с подтверждением и тостом;
- /coupon/:token — публичная страница купона: тариф/срок, кнопка
  активации в боте, для залогиненных — активация прямо в кабинете
  с маппингом структурированных кодов ошибок {code, message};
- пункт меню в разделе тарифов (гейт coupons:read), лениво
  загружаемые роуты, локали ru/en (zh/fa падают на ru-фолбэк).
This commit is contained in:
kewldan
2026-07-11 23:52:13 +03:00
parent 20565b8f59
commit 799e986d84
9 changed files with 1324 additions and 0 deletions

View File

@@ -1263,6 +1263,7 @@
"campaigns": "Кампании",
"promoOffers": "Промопредложения",
"promocodes": "Промокоды",
"coupons": "Купоны",
"promoGroups": "Группы скидок",
"trafficUsage": "Расход трафика",
"updates": "Обновления",
@@ -3433,6 +3434,84 @@
"description": "Это лишит партнёра привилегий. Партнёр больше не будет получать комиссию от рефералов."
}
},
"coupons": {
"title": "Купоны",
"subtitle": "Оптовые партии одноразовых ссылок на подписку",
"addBatch": "Создать партию",
"noBatches": "Партий пока нет",
"days": "дн.",
"stats": {
"batches": "Партий",
"active": "Активных",
"redeemed": "Погашено",
"revoked": "Отозвано"
},
"list": {
"revoked": "Отозвана",
"until": "До",
"price": "Опт"
},
"pagination": {
"prev": "Назад",
"next": "Вперёд",
"page": "Стр. {{current}} из {{total}}"
},
"form": {
"title": "Новая партия купонов",
"subtitle": "Каждый купон — одноразовая ссылка на подписку",
"name": "Название партии",
"namePlaceholder": "Например, имя партнёра",
"tariff": "Тариф",
"selectTariff": "Выберите тариф",
"periodDays": "Дней подписки на купон",
"couponsCount": "Количество купонов (1500)",
"price": "Оптовая цена за купон, ₽",
"priceHint": "Только для учёта, 0 — не указывать",
"validDays": "Срок действия купонов, дней",
"validDaysHint": "0 или пусто — бессрочно",
"create": "Создать партию",
"creating": "Создание…",
"cancel": "Отмена"
},
"validation": {
"nameRequired": "Укажите название партии",
"tariffRequired": "Выберите тариф",
"periodInvalid": "Дней подписки: от 1 до 3650",
"countInvalid": "Количество купонов: от 1 до 500"
},
"created": {
"title": "Партия создана",
"linksLabel": "Ссылки ({{count}} шт.)",
"copyAll": "Скопировать все",
"copied": "Скопировано",
"download": "Скачать .txt",
"toList": "К списку партий",
"toBatch": "К партии"
},
"detail": {
"title": "Партия #{{id}}",
"total": "Всего купонов",
"price": "Оптовая цена",
"priceTotal": "итого",
"validUntil": "Действует до",
"perpetual": "Бессрочно",
"createdAt": "Создана",
"linksTitle": "Активные ссылки ({{count}} шт.)"
},
"revoke": {
"button": "Отозвать непогашенные ({{count}})",
"confirmTitle": "Отозвать партию?",
"confirmText": "Будет отозвано {{count}} непогашенных купонов. Их ссылки перестанут работать. Действие необратимо.",
"confirm": "Отозвать",
"cancel": "Отмена",
"success": "Отозвано купонов: {{count}}"
},
"errors": {
"loadFailed": "Партия не найдена",
"createFailed": "Не удалось создать партию",
"revokeFailed": "Не удалось отозвать партию"
}
},
"promocodes": {
"title": "Промокоды",
"subtitle": "Управление промокодами и группами скидок",
@@ -5643,6 +5722,34 @@
"nMonths": "{{count}} мес"
}
},
"coupon": {
"title": "Купон на подписку",
"subtitle": "Одноразовый купон — активируйте, чтобы получить или продлить подписку",
"tariff": "Тариф",
"period": "Срок",
"daysSuffix": "дн.",
"validUntil": "Активировать до",
"openBot": "Активировать в боте",
"redeemCabinet": "Активировать в кабинете",
"redeeming": "Активация…",
"needAuth": "Чтобы активировать купон в кабинете, войдите в аккаунт — или откройте ссылку в боте",
"notFound": {
"title": "Купон не найден",
"text": "Купон не существует, уже использован или истёк"
},
"success": {
"activated": "Подписка активирована!",
"renewed": "Подписка продлена!",
"until": "Действует до"
},
"errors": {
"invalid": "Купон не найден или уже использован",
"expired": "Срок действия купона истёк",
"already_redeemed_by_you": "Вы уже активировали этот купон",
"internal": "Ошибка сервера, попробуйте позже",
"generic": "Не удалось активировать купон"
}
},
"gift": {
"title": "Подарить подписку",
"subtitle": "Отправьте VPN-подписку в подарок",