mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -228,7 +228,7 @@ export default function SubscriptionCardExpired({
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="mb-0.5 font-mono text-[10px] font-medium uppercase tracking-wider text-dark-50/30">
|
||||
{t('dashboard.expired.expiredDate')}
|
||||
{isLimited ? t('dashboard.expired.activeUntil') : t('dashboard.expired.expiredDate')}
|
||||
</div>
|
||||
<div className="ml-3 text-base font-bold tracking-tight text-dark-50/50">
|
||||
{formattedDate}
|
||||
@@ -280,7 +280,7 @@ export default function SubscriptionCardExpired({
|
||||
>
|
||||
<path d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
{t('subscription.trafficLimited')}
|
||||
{t('subscription.buyTraffic')}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -15,7 +15,9 @@ export default function PurchaseCTAButton({
|
||||
}: PurchaseCTAButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isExpired = !subscription || (!subscription.is_active && !subscription.is_trial);
|
||||
const isExpired =
|
||||
!subscription ||
|
||||
(!subscription.is_active && !subscription.is_trial && !subscription.is_limited);
|
||||
const isTrial = subscription?.is_trial;
|
||||
const isDaily = subscription?.is_daily;
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ export default function SubscriptionListCard({
|
||||
};
|
||||
|
||||
const isTrial = subscription.is_trial;
|
||||
const isActive = subscription.status === 'active' || subscription.status === 'trial';
|
||||
const isActive =
|
||||
subscription.status === 'active' ||
|
||||
subscription.status === 'trial' ||
|
||||
subscription.status === 'limited';
|
||||
const isExpired = subscription.status === 'expired' || subscription.status === 'disabled';
|
||||
const trafficLimit = subscription.traffic_limit_gb;
|
||||
const trafficUsed = subscription.traffic_used_gb;
|
||||
@@ -95,21 +98,25 @@ export default function SubscriptionListCard({
|
||||
const trafficColor =
|
||||
trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400';
|
||||
|
||||
const borderColor = isTrial
|
||||
? 'rgba(251,191,36,0.2)'
|
||||
: isExpired
|
||||
? 'rgba(255,59,92,0.15)'
|
||||
: g.cardBorder;
|
||||
const isLimitedStatus = subscription.status === 'limited';
|
||||
|
||||
const bgColor = isTrial
|
||||
? isDark
|
||||
? 'rgba(251,191,36,0.04)'
|
||||
: 'rgba(251,191,36,0.03)'
|
||||
: isExpired
|
||||
const borderColor =
|
||||
isTrial || isLimitedStatus
|
||||
? 'rgba(251,191,36,0.2)'
|
||||
: isExpired
|
||||
? 'rgba(255,59,92,0.15)'
|
||||
: g.cardBorder;
|
||||
|
||||
const bgColor =
|
||||
isTrial || isLimitedStatus
|
||||
? isDark
|
||||
? 'rgba(255,59,92,0.04)'
|
||||
: 'rgba(255,59,92,0.03)'
|
||||
: g.cardBg;
|
||||
? 'rgba(251,191,36,0.04)'
|
||||
: 'rgba(251,191,36,0.03)'
|
||||
: isExpired
|
||||
? isDark
|
||||
? 'rgba(255,59,92,0.04)'
|
||||
: 'rgba(255,59,92,0.03)'
|
||||
: g.cardBg;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -272,7 +272,8 @@
|
||||
"tariffs": "Tariffs",
|
||||
"traffic": "Traffic",
|
||||
"devices": "Devices",
|
||||
"expiredDate": "Expired"
|
||||
"expiredDate": "Expired",
|
||||
"activeUntil": "Active until"
|
||||
},
|
||||
"suspended": {
|
||||
"title": "Subscription Suspended",
|
||||
@@ -554,8 +555,8 @@
|
||||
"unlimited": "Unlimited",
|
||||
"buyTrafficGb": "Buy {{gb}} GB",
|
||||
"buyUnlimited": "Buy Unlimited",
|
||||
"manageServers": "Server management",
|
||||
"manageServersTitle": "Server management"
|
||||
"manageServers": "Squad management",
|
||||
"manageServersTitle": "Squad management"
|
||||
},
|
||||
"serverManagement": {
|
||||
"statusLegend": "✅ — connected • ➕ — will be added (paid) • ➖ — will be disconnected",
|
||||
@@ -1099,7 +1100,7 @@
|
||||
"apps": "Apps",
|
||||
"wheel": "Wheel",
|
||||
"tariffs": "Tariffs",
|
||||
"servers": "Servers",
|
||||
"servers": "Squads",
|
||||
"banSystem": "Ban Monitoring",
|
||||
"broadcasts": "Broadcasts",
|
||||
"users": "Users",
|
||||
@@ -2350,8 +2351,8 @@
|
||||
}
|
||||
},
|
||||
"servers": {
|
||||
"title": "Server Management",
|
||||
"subtitle": "Configure VPN servers",
|
||||
"title": "Squad Management",
|
||||
"subtitle": "Configure squads",
|
||||
"sync": "Sync",
|
||||
"syncing": "Syncing...",
|
||||
"syncNow": "Sync now",
|
||||
|
||||
@@ -386,8 +386,8 @@
|
||||
"unlimited": "نامحدود",
|
||||
"buyTrafficGb": "خرید {{gb}} GB",
|
||||
"buyUnlimited": "خرید نامحدود",
|
||||
"manageServers": "مدیریت سرورها",
|
||||
"manageServersTitle": "مدیریت سرورها"
|
||||
"manageServers": "مدیریت اسکوادها",
|
||||
"manageServersTitle": "مدیریت اسکوادها"
|
||||
},
|
||||
"serverManagement": {
|
||||
"statusLegend": "✅ — متصل • ➕ — اضافه خواهد شد (پولی) • ➖ — قطع خواهد شد",
|
||||
@@ -924,7 +924,7 @@
|
||||
"apps": "برنامهها",
|
||||
"wheel": "چرخ",
|
||||
"tariffs": "تعرفهها",
|
||||
"servers": "سرورها",
|
||||
"servers": "اسکوادها",
|
||||
"broadcasts": "پیامرسانی",
|
||||
"emailTemplates": "قالبهای ایمیل",
|
||||
"paymentMethods": "روشهای پرداخت",
|
||||
@@ -1986,8 +1986,8 @@
|
||||
}
|
||||
},
|
||||
"servers": {
|
||||
"title": "مدیریت سرورها",
|
||||
"subtitle": "پیکربندی سرورهای VPN",
|
||||
"title": "مدیریت اسکوادها",
|
||||
"subtitle": "پیکربندی اسکوادها",
|
||||
"sync": "همگامسازی",
|
||||
"syncing": "در حال همگامسازی...",
|
||||
"syncNow": "همگامسازی الان",
|
||||
|
||||
@@ -284,7 +284,8 @@
|
||||
"tariffs": "Тарифы",
|
||||
"traffic": "Трафик",
|
||||
"devices": "Устройства",
|
||||
"expiredDate": "Истекла"
|
||||
"expiredDate": "Истекла",
|
||||
"activeUntil": "Активна до"
|
||||
},
|
||||
"suspended": {
|
||||
"title": "Подписка приостановлена",
|
||||
@@ -582,8 +583,8 @@
|
||||
"unlimited": "Безлимит",
|
||||
"buyTrafficGb": "Купить {{gb}} ГБ",
|
||||
"buyUnlimited": "Купить Безлимит",
|
||||
"manageServers": "Управление серверами",
|
||||
"manageServersTitle": "Управление серверами"
|
||||
"manageServers": "Управление сквадами",
|
||||
"manageServersTitle": "Управление сквадами"
|
||||
},
|
||||
"serverManagement": {
|
||||
"statusLegend": "✅ — подключено • ➕ — будет добавлено (платно) • ➖ — будет отключено",
|
||||
@@ -1120,7 +1121,7 @@
|
||||
"apps": "Приложения",
|
||||
"wheel": "Колесо",
|
||||
"tariffs": "Тарифы",
|
||||
"servers": "Серверы",
|
||||
"servers": "Сквады",
|
||||
"banSystem": "Мониторинг банов",
|
||||
"broadcasts": "Рассылки",
|
||||
"users": "Пользователи",
|
||||
@@ -2856,8 +2857,8 @@
|
||||
}
|
||||
},
|
||||
"servers": {
|
||||
"title": "Управление серверами",
|
||||
"subtitle": "Настройка серверов VPN",
|
||||
"title": "Управление сквадами",
|
||||
"subtitle": "Настройка сквадов",
|
||||
"sync": "Синхронизировать",
|
||||
"syncing": "Синхронизация...",
|
||||
"syncNow": "Синхронизировать сейчас",
|
||||
|
||||
@@ -386,8 +386,8 @@
|
||||
"unlimited": "无限制",
|
||||
"buyTrafficGb": "购买 {{gb}} GB",
|
||||
"buyUnlimited": "购买无限流量",
|
||||
"manageServers": "服务器管理",
|
||||
"manageServersTitle": "服务器管理"
|
||||
"manageServers": "小队管理",
|
||||
"manageServersTitle": "小队管理"
|
||||
},
|
||||
"serverManagement": {
|
||||
"statusLegend": "✅ — 已连接 • ➕ — 将添加(付费)• ➖ — 将断开",
|
||||
@@ -924,7 +924,7 @@
|
||||
"apps": "应用",
|
||||
"wheel": "转盘",
|
||||
"tariffs": "套餐",
|
||||
"servers": "服务器",
|
||||
"servers": "小队",
|
||||
"broadcasts": "群发",
|
||||
"emailTemplates": "邮件模板",
|
||||
"paymentMethods": "支付方式",
|
||||
|
||||
@@ -186,14 +186,14 @@ export default function AdminServers() {
|
||||
onClick={() => toggleMutation.mutate(server.id)}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
server.is_available
|
||||
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
? 'bg-error-500/20 text-error-400 hover:bg-error-500/30'
|
||||
: 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
|
||||
}`}
|
||||
title={
|
||||
server.is_available ? t('admin.servers.disable') : t('admin.servers.enable')
|
||||
}
|
||||
>
|
||||
{server.is_available ? <CheckIcon /> : <XIcon />}
|
||||
{server.is_available ? <XIcon /> : <CheckIcon />}
|
||||
</button>
|
||||
|
||||
{/* Toggle Trial */}
|
||||
@@ -201,7 +201,7 @@ export default function AdminServers() {
|
||||
onClick={() => toggleTrialMutation.mutate(server.id)}
|
||||
className={`rounded-lg p-2 transition-colors ${
|
||||
server.is_trial_eligible
|
||||
? 'bg-accent-500/20 text-accent-400 hover:bg-accent-500/30'
|
||||
? 'bg-warning-500/20 text-warning-400 hover:bg-warning-500/30'
|
||||
: 'bg-dark-700 text-dark-400 hover:bg-dark-600'
|
||||
}`}
|
||||
title={t('admin.servers.toggleTrial')}
|
||||
|
||||
@@ -1658,7 +1658,10 @@ export default function AdminUserDetail() {
|
||||
.filter((tariffItem) => {
|
||||
const purchasedIds = new Set(
|
||||
userSubscriptions
|
||||
.filter((s) => s.is_active || s.status === 'trial')
|
||||
.filter(
|
||||
(s) =>
|
||||
s.is_active || s.status === 'trial' || s.status === 'limited',
|
||||
)
|
||||
.map((s) => s.tariff_id),
|
||||
);
|
||||
return !purchasedIds.has(tariffItem.id);
|
||||
|
||||
@@ -516,35 +516,14 @@ function BuyTabContent({
|
||||
const [selectedSubOption, setSelectedSubOption] = useState<string | null>(null);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
// Collect ALL unique periods across ALL tariffs
|
||||
const allPeriods = useMemo(() => {
|
||||
const periodMap = new Map<number, GiftTariffPeriod>();
|
||||
for (const tariff of config.tariffs) {
|
||||
for (const period of tariff.periods) {
|
||||
if (!periodMap.has(period.days)) {
|
||||
periodMap.set(period.days, period);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(periodMap.values()).sort((a, b) => a.days - b.days);
|
||||
}, [config]);
|
||||
|
||||
// Filter tariffs to only those that have the selected period
|
||||
const visibleTariffs = useMemo(() => {
|
||||
if (!selectedPeriodDays) return config.tariffs;
|
||||
return config.tariffs.filter((tariff) =>
|
||||
tariff.periods.some((p) => p.days === selectedPeriodDays),
|
||||
);
|
||||
}, [config, selectedPeriodDays]);
|
||||
|
||||
// Auto-select first tariff, period, method on config load
|
||||
useEffect(() => {
|
||||
if (allPeriods.length > 0 && selectedPeriodDays === null) {
|
||||
setSelectedPeriodDays(allPeriods[0].days);
|
||||
}
|
||||
|
||||
if (visibleTariffs.length > 0 && selectedTariffId === null) {
|
||||
setSelectedTariffId(visibleTariffs[0].id);
|
||||
if (config.tariffs.length > 0 && selectedTariffId === null) {
|
||||
const firstTariff = config.tariffs[0];
|
||||
setSelectedTariffId(firstTariff.id);
|
||||
if (firstTariff.periods.length > 0 && selectedPeriodDays === null) {
|
||||
setSelectedPeriodDays(firstTariff.periods[0].days);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.payment_methods.length > 0 && selectedMethod === null) {
|
||||
@@ -556,16 +535,19 @@ function BuyTabContent({
|
||||
setSelectedSubOption(null);
|
||||
}
|
||||
}
|
||||
}, [config, allPeriods, visibleTariffs, selectedTariffId, selectedPeriodDays, selectedMethod]);
|
||||
}, [config, selectedTariffId, selectedPeriodDays, selectedMethod]);
|
||||
|
||||
// When period changes, auto-select first visible tariff if current is hidden
|
||||
// When tariff changes, auto-select its first period
|
||||
useEffect(() => {
|
||||
if (!visibleTariffs.length) return;
|
||||
const currentVisible = visibleTariffs.find((tariff) => tariff.id === selectedTariffId);
|
||||
if (!currentVisible) {
|
||||
setSelectedTariffId(visibleTariffs[0].id);
|
||||
if (!selectedTariffId) return;
|
||||
const tariff = config.tariffs.find((t) => t.id === selectedTariffId);
|
||||
if (tariff && tariff.periods.length > 0) {
|
||||
const hasCurrent = tariff.periods.some((p) => p.days === selectedPeriodDays);
|
||||
if (!hasCurrent) {
|
||||
setSelectedPeriodDays(tariff.periods[0].days);
|
||||
}
|
||||
}
|
||||
}, [visibleTariffs, selectedTariffId]);
|
||||
}, [selectedTariffId, config.tariffs, selectedPeriodDays]);
|
||||
|
||||
// Derived data
|
||||
const selectedTariff = useMemo(
|
||||
@@ -661,15 +643,15 @@ function BuyTabContent({
|
||||
return `${t('gift.fromBalance')} (${formatPrice(config.balance_kopeks)})`;
|
||||
}, [config, t]);
|
||||
|
||||
const showTariffCards = visibleTariffs.length > 1;
|
||||
const showTariffCards = config.tariffs.length > 1;
|
||||
|
||||
// Periods for the selected tariff (for period cards)
|
||||
const periodsForDisplay = useMemo(() => {
|
||||
if (selectedTariff) {
|
||||
return [...selectedTariff.periods].sort((a, b) => a.days - b.days);
|
||||
}
|
||||
return allPeriods;
|
||||
}, [selectedTariff, allPeriods]);
|
||||
return [];
|
||||
}, [selectedTariff]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -680,7 +662,7 @@ function BuyTabContent({
|
||||
{t('gift.selectTariff')}
|
||||
</h2>
|
||||
<div role="radiogroup" aria-label={t('gift.chooseTariff')} className="space-y-2">
|
||||
{visibleTariffs.map((tariff) => (
|
||||
{config.tariffs.map((tariff) => (
|
||||
<TariffCard
|
||||
key={tariff.id}
|
||||
tariff={tariff}
|
||||
|
||||
@@ -957,7 +957,7 @@ export default function Subscription() {
|
||||
<div className="mb-5">
|
||||
<CountdownTimer
|
||||
endDate={subscription.end_date}
|
||||
isActive={subscription.is_active}
|
||||
isActive={subscription.is_active || subscription.is_limited}
|
||||
glassColors={g}
|
||||
/>
|
||||
</div>
|
||||
@@ -1316,68 +1316,25 @@ export default function Subscription() {
|
||||
<PurchaseCTAButton subscription={subscription} isMultiTariff={isMultiTariff} />
|
||||
|
||||
{/* Delete expired subscription */}
|
||||
{isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && (
|
||||
<div className="space-y-3">
|
||||
{!showDeleteSheet ? (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (platform === 'telegram') {
|
||||
const confirmed = await destructiveConfirm(
|
||||
t(
|
||||
'subscription.deleteWarning',
|
||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
||||
),
|
||||
t('subscription.confirmDelete', 'Да, удалить'),
|
||||
t('subscription.deleteTitle', 'Удалить подписку?'),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
await subscriptionApi.deleteSubscription(subscription.id);
|
||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||||
navigate('/subscriptions', { replace: true });
|
||||
} catch {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
} else {
|
||||
setShowDeleteSheet(true);
|
||||
}
|
||||
}}
|
||||
disabled={deleteLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10 disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{t('subscription.delete', 'Удалить подписку')}
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-2xl border border-red-400/20 p-4"
|
||||
style={{ background: 'rgba(255,59,92,0.04)' }}
|
||||
>
|
||||
<div className="mb-3 text-sm font-semibold text-red-400">
|
||||
{t('subscription.deleteTitle', 'Удалить подписку?')}
|
||||
</div>
|
||||
<div className="mb-4 text-xs" style={{ color: g.textSecondary }}>
|
||||
{t(
|
||||
'subscription.deleteWarning',
|
||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
{isMultiTariff &&
|
||||
subscription &&
|
||||
!subscription.is_active &&
|
||||
!subscription.is_trial &&
|
||||
!subscription.is_limited && (
|
||||
<div className="space-y-3">
|
||||
{!showDeleteSheet ? (
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (platform === 'telegram') {
|
||||
const confirmed = await destructiveConfirm(
|
||||
t(
|
||||
'subscription.deleteWarning',
|
||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
||||
),
|
||||
t('subscription.confirmDelete', 'Да, удалить'),
|
||||
t('subscription.deleteTitle', 'Удалить подписку?'),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
await subscriptionApi.deleteSubscription(subscription.id);
|
||||
@@ -1385,28 +1342,75 @@ export default function Subscription() {
|
||||
navigate('/subscriptions', { replace: true });
|
||||
} catch {
|
||||
setDeleteLoading(false);
|
||||
setShowDeleteSheet(false);
|
||||
}
|
||||
}}
|
||||
disabled={deleteLoading}
|
||||
className="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50"
|
||||
} else {
|
||||
setShowDeleteSheet(true);
|
||||
}
|
||||
}}
|
||||
disabled={deleteLoading}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10 disabled:opacity-50"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
{deleteLoading
|
||||
? t('common.processing', 'Удаление...')
|
||||
: t('subscription.confirmDelete', 'Да, удалить')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteSheet(false)}
|
||||
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
|
||||
style={{ color: g.textSecondary }}
|
||||
>
|
||||
{t('common.cancel', 'Отмена')}
|
||||
</button>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
{t('subscription.delete', 'Удалить подписку')}
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
className="rounded-2xl border border-red-400/20 p-4"
|
||||
style={{ background: 'rgba(255,59,92,0.04)' }}
|
||||
>
|
||||
<div className="mb-3 text-sm font-semibold text-red-400">
|
||||
{t('subscription.deleteTitle', 'Удалить подписку?')}
|
||||
</div>
|
||||
<div className="mb-4 text-xs" style={{ color: g.textSecondary }}>
|
||||
{t(
|
||||
'subscription.deleteWarning',
|
||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={async () => {
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
await subscriptionApi.deleteSubscription(subscription.id);
|
||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||||
navigate('/subscriptions', { replace: true });
|
||||
} catch {
|
||||
setDeleteLoading(false);
|
||||
setShowDeleteSheet(false);
|
||||
}
|
||||
}}
|
||||
disabled={deleteLoading}
|
||||
className="flex-1 rounded-xl bg-red-500 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50"
|
||||
>
|
||||
{deleteLoading
|
||||
? t('common.processing', 'Удаление...')
|
||||
: t('subscription.confirmDelete', 'Да, удалить')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowDeleteSheet(false)}
|
||||
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
|
||||
style={{ color: g.textSecondary }}
|
||||
>
|
||||
{t('common.cancel', 'Отмена')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional Options (Buy Devices) */}
|
||||
{subscription &&
|
||||
|
||||
@@ -781,7 +781,7 @@ export default function SubscriptionPurchase() {
|
||||
!isCurrentTariff &&
|
||||
!subscription.is_trial &&
|
||||
!isSubscriptionExpired &&
|
||||
subscription.is_active;
|
||||
(subscription.is_active || subscription.is_limited);
|
||||
const isLegacySubscription =
|
||||
subscription && !subscription.is_trial && !subscription.tariff_id;
|
||||
|
||||
|
||||
@@ -7,11 +7,28 @@ export function formatUptime(seconds: number): string {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
export function formatPrice(kopeks: number): string {
|
||||
import i18next from 'i18next';
|
||||
|
||||
const LANG_CURRENCY_MAP: Record<string, { currency: string; locale: string; symbol: string }> = {
|
||||
ru: { currency: 'RUB', locale: 'ru-RU', symbol: '₽' },
|
||||
en: { currency: 'USD', locale: 'en-US', symbol: '$' },
|
||||
zh: { currency: 'CNY', locale: 'zh-CN', symbol: '¥' },
|
||||
fa: { currency: 'IRR', locale: 'fa-IR', symbol: '﷼' },
|
||||
};
|
||||
|
||||
const DEFAULT_CURRENCY = { currency: 'RUB', locale: 'ru-RU', symbol: '₽' };
|
||||
|
||||
export function formatPrice(kopeks: number, lang?: string): string {
|
||||
const resolvedLang = lang || i18next.language || 'ru';
|
||||
const config = LANG_CURRENCY_MAP[resolvedLang] || DEFAULT_CURRENCY;
|
||||
const rubles = kopeks / 100;
|
||||
return new Intl.NumberFormat('ru-RU', {
|
||||
style: 'currency',
|
||||
currency: 'RUB',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(rubles);
|
||||
try {
|
||||
return new Intl.NumberFormat(config.locale, {
|
||||
style: 'currency',
|
||||
currency: config.currency,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(rubles);
|
||||
} catch {
|
||||
return `${Math.round(rubles)} ${config.symbol}`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user