mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
@@ -32,6 +32,7 @@ export interface TariffListItem {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_trial_available: boolean;
|
is_trial_available: boolean;
|
||||||
|
show_in_gift: boolean;
|
||||||
is_daily: boolean;
|
is_daily: boolean;
|
||||||
daily_price_kopeks: number;
|
daily_price_kopeks: number;
|
||||||
traffic_limit_gb: number;
|
traffic_limit_gb: number;
|
||||||
@@ -54,6 +55,7 @@ export interface TariffDetail {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
is_trial_available: boolean;
|
is_trial_available: boolean;
|
||||||
|
show_in_gift: boolean;
|
||||||
traffic_limit_gb: number;
|
traffic_limit_gb: number;
|
||||||
device_limit: number;
|
device_limit: number;
|
||||||
device_price_kopeks: number | null;
|
device_price_kopeks: number | null;
|
||||||
@@ -95,6 +97,7 @@ export interface TariffCreateRequest {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
|
show_in_gift?: boolean;
|
||||||
traffic_limit_gb?: number;
|
traffic_limit_gb?: number;
|
||||||
device_limit?: number;
|
device_limit?: number;
|
||||||
device_price_kopeks?: number;
|
device_price_kopeks?: number;
|
||||||
@@ -137,6 +140,7 @@ export interface TariffUpdateRequest {
|
|||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
|
show_in_gift?: boolean;
|
||||||
traffic_limit_gb?: number;
|
traffic_limit_gb?: number;
|
||||||
device_limit?: number;
|
device_limit?: number;
|
||||||
device_price_kopeks?: number;
|
device_price_kopeks?: number;
|
||||||
|
|||||||
@@ -2044,6 +2044,9 @@
|
|||||||
"statusTitle": "Tariff Status",
|
"statusTitle": "Tariff Status",
|
||||||
"isActiveLabel": "Active",
|
"isActiveLabel": "Active",
|
||||||
"isActiveHint": "Inactive tariffs are not shown to users",
|
"isActiveHint": "Inactive tariffs are not shown to users",
|
||||||
|
"showInGiftLabel": "Show in gifts",
|
||||||
|
"showInGiftHint": "Tariff will be available for purchase in the gifts section",
|
||||||
|
"giftBadge": "Gift",
|
||||||
"isTrialLabel": "Trial tariff",
|
"isTrialLabel": "Trial tariff",
|
||||||
"isTrialHint": "Available for free trial period",
|
"isTrialHint": "Available for free trial period",
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -1705,6 +1705,9 @@
|
|||||||
"statusTitle": "وضعیت تعرفه",
|
"statusTitle": "وضعیت تعرفه",
|
||||||
"isActiveLabel": "فعال",
|
"isActiveLabel": "فعال",
|
||||||
"isActiveHint": "تعرفههای غیرفعال به کاربران نمایش داده نمیشوند",
|
"isActiveHint": "تعرفههای غیرفعال به کاربران نمایش داده نمیشوند",
|
||||||
|
"showInGiftLabel": "نمایش در هدایا",
|
||||||
|
"showInGiftHint": "تعرفه برای خرید در بخش هدایا در دسترس خواهد بود",
|
||||||
|
"giftBadge": "هدیه",
|
||||||
"isTrialLabel": "تعرفه آزمایشی",
|
"isTrialLabel": "تعرفه آزمایشی",
|
||||||
"isTrialHint": "برای دوره آزمایشی رایگان در دسترس است",
|
"isTrialHint": "برای دوره آزمایشی رایگان در دسترس است",
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -2558,6 +2558,9 @@
|
|||||||
"statusTitle": "Статус тарифа",
|
"statusTitle": "Статус тарифа",
|
||||||
"isActiveLabel": "Активен",
|
"isActiveLabel": "Активен",
|
||||||
"isActiveHint": "Неактивные тарифы не отображаются пользователям",
|
"isActiveHint": "Неактивные тарифы не отображаются пользователям",
|
||||||
|
"showInGiftLabel": "Видимость в подарках",
|
||||||
|
"showInGiftHint": "Тариф будет доступен для покупки в разделе подарков",
|
||||||
|
"giftBadge": "Подарок",
|
||||||
"isTrialLabel": "Триальный тариф",
|
"isTrialLabel": "Триальный тариф",
|
||||||
"isTrialHint": "Доступен для бесплатного пробного периода",
|
"isTrialHint": "Доступен для бесплатного пробного периода",
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -1742,6 +1742,9 @@
|
|||||||
"statusTitle": "套餐状态",
|
"statusTitle": "套餐状态",
|
||||||
"isActiveLabel": "已激活",
|
"isActiveLabel": "已激活",
|
||||||
"isActiveHint": "未激活的套餐不会显示给用户",
|
"isActiveHint": "未激活的套餐不会显示给用户",
|
||||||
|
"showInGiftLabel": "在礼物中显示",
|
||||||
|
"showInGiftHint": "套餐将在礼物区域中可供购买",
|
||||||
|
"giftBadge": "礼物",
|
||||||
"isTrialLabel": "试用套餐",
|
"isTrialLabel": "试用套餐",
|
||||||
"isTrialHint": "可用于免费试用期",
|
"isTrialHint": "可用于免费试用期",
|
||||||
"validation": {
|
"validation": {
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export default function AdminTariffCreate() {
|
|||||||
// Traffic reset mode
|
// Traffic reset mode
|
||||||
const [trafficResetMode, setTrafficResetMode] = useState<string | null>(null);
|
const [trafficResetMode, setTrafficResetMode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Gift visibility
|
||||||
|
const [showInGift, setShowInGift] = useState(true);
|
||||||
|
|
||||||
// New period for adding
|
// New period for adding
|
||||||
const [newPeriodDays, setNewPeriodDays] = useState<number | ''>(30);
|
const [newPeriodDays, setNewPeriodDays] = useState<number | ''>(30);
|
||||||
const [newPeriodPrice, setNewPeriodPrice] = useState<number | ''>(300);
|
const [newPeriodPrice, setNewPeriodPrice] = useState<number | ''>(300);
|
||||||
@@ -182,6 +185,7 @@ export default function AdminTariffCreate() {
|
|||||||
setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0);
|
setMaxTopupTrafficGb(data.max_topup_traffic_gb || 0);
|
||||||
setTrafficTopupPackages(data.traffic_topup_packages || {});
|
setTrafficTopupPackages(data.traffic_topup_packages || {});
|
||||||
setTrafficResetMode(data.traffic_reset_mode || null);
|
setTrafficResetMode(data.traffic_reset_mode || null);
|
||||||
|
setShowInGift(data.show_in_gift ?? true);
|
||||||
return data;
|
return data;
|
||||||
}, []),
|
}, []),
|
||||||
});
|
});
|
||||||
@@ -212,6 +216,7 @@ export default function AdminTariffCreate() {
|
|||||||
name,
|
name,
|
||||||
description: description || undefined,
|
description: description || undefined,
|
||||||
is_active: isActive,
|
is_active: isActive,
|
||||||
|
show_in_gift: showInGift,
|
||||||
traffic_limit_gb: toNumber(trafficLimitGb, 100),
|
traffic_limit_gb: toNumber(trafficLimitGb, 100),
|
||||||
device_limit: toNumber(deviceLimit, 1),
|
device_limit: toNumber(deviceLimit, 1),
|
||||||
device_price_kopeks:
|
device_price_kopeks:
|
||||||
@@ -1113,6 +1118,28 @@ export default function AdminTariffCreate() {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Show in gift toggle */}
|
||||||
|
<div className="flex items-center justify-between rounded-lg bg-dark-800 p-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium text-dark-200">
|
||||||
|
{t('admin.tariffs.showInGiftLabel')}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-dark-500">{t('admin.tariffs.showInGiftHint')}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowInGift(!showInGift)}
|
||||||
|
className={`relative h-6 w-11 rounded-full transition-colors ${
|
||||||
|
showInGift ? 'bg-accent-500' : 'bg-dark-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`absolute top-1 h-4 w-4 rounded-full bg-white transition-transform ${
|
||||||
|
showInGift ? 'left-6' : 'left-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -173,6 +173,24 @@ function SortableTariffCard({
|
|||||||
{t('admin.tariffs.trial')}
|
{t('admin.tariffs.trial')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{tariff.show_in_gift && (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded bg-purple-500/20 px-2 py-0.5 text-xs text-purple-400">
|
||||||
|
<svg
|
||||||
|
className="h-3 w-3"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('admin.tariffs.giftBadge')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!tariff.is_active && (
|
{!tariff.is_active && (
|
||||||
<span className="rounded bg-dark-600 px-2 py-0.5 text-xs text-dark-400">
|
<span className="rounded bg-dark-600 px-2 py-0.5 text-xs text-dark-400">
|
||||||
{t('admin.tariffs.inactive')}
|
{t('admin.tariffs.inactive')}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { cn } from '../lib/utils';
|
|||||||
import { copyToClipboard } from '../utils/clipboard';
|
import { copyToClipboard } from '../utils/clipboard';
|
||||||
import { getApiErrorMessage } from '../utils/api-error';
|
import { getApiErrorMessage } from '../utils/api-error';
|
||||||
import { formatPrice } from '../utils/format';
|
import { formatPrice } from '../utils/format';
|
||||||
|
import { usePlatform, useHaptic } from '@/platform';
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SVG Icons
|
// SVG Icons
|
||||||
@@ -524,6 +525,8 @@ function BuyTabContent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { openInvoice, capabilities } = usePlatform();
|
||||||
|
const haptic = useHaptic();
|
||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
const [selectedTariffId, setSelectedTariffId] = useState<number | null>(null);
|
const [selectedTariffId, setSelectedTariffId] = useState<number | null>(null);
|
||||||
@@ -610,8 +613,29 @@ function BuyTabContent({
|
|||||||
// Purchase mutation
|
// Purchase mutation
|
||||||
const purchaseMutation = useMutation({
|
const purchaseMutation = useMutation({
|
||||||
mutationFn: (data: GiftPurchaseRequest) => giftApi.createPurchase(data),
|
mutationFn: (data: GiftPurchaseRequest) => giftApi.createPurchase(data),
|
||||||
onSuccess: (result) => {
|
onSuccess: async (result) => {
|
||||||
if (result.payment_url) {
|
if (result.payment_url) {
|
||||||
|
// Telegram Stars: open invoice natively instead of redirect
|
||||||
|
const isStars = selectedMethod === 'telegram_stars';
|
||||||
|
if (isStars && capabilities.hasInvoice) {
|
||||||
|
try {
|
||||||
|
const status = await openInvoice(result.payment_url);
|
||||||
|
if (status === 'paid') {
|
||||||
|
haptic.notification('success');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['gift-config'] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['gift-sent'] });
|
||||||
|
onPurchaseComplete();
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
haptic.notification('error');
|
||||||
|
setSubmitError(t('gift.failedDesc'));
|
||||||
|
}
|
||||||
|
// 'cancelled' — user closed the invoice, do nothing
|
||||||
|
} catch (e) {
|
||||||
|
setSubmitError(t('gift.failedDesc'));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
window.location.href = result.payment_url;
|
window.location.href = result.payment_url;
|
||||||
} else {
|
} else {
|
||||||
// Balance purchase: switch to MyGifts tab so the new code is visible
|
// Balance purchase: switch to MyGifts tab so the new code is visible
|
||||||
|
|||||||
Reference in New Issue
Block a user