mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
fix: complete is_limited status handling across all views
- SubscriptionListCard: show traffic progress bar for limited subs in multi-tariff list (was hidden because isActive excluded limited) - SubscriptionListCard: amber border/background for limited status cards (was using default neutral style, inconsistent with rest of UI) - Subscription page: hide delete button for limited subs in multi-tariff mode (limited subs still have valid days remaining) - SubscriptionPurchase: allow tariff switch for limited subs (was blocked by is_active-only gate in canSwitch formula) - AdminUserDetail: include limited status in "already purchased" tariff filter to prevent duplicate subscription creation
This commit is contained in:
@@ -82,7 +82,10 @@ export default function SubscriptionListCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isTrial = subscription.is_trial;
|
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 isExpired = subscription.status === 'expired' || subscription.status === 'disabled';
|
||||||
const trafficLimit = subscription.traffic_limit_gb;
|
const trafficLimit = subscription.traffic_limit_gb;
|
||||||
const trafficUsed = subscription.traffic_used_gb;
|
const trafficUsed = subscription.traffic_used_gb;
|
||||||
@@ -95,21 +98,25 @@ export default function SubscriptionListCard({
|
|||||||
const trafficColor =
|
const trafficColor =
|
||||||
trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400';
|
trafficPercent >= 90 ? 'bg-red-400' : trafficPercent >= 70 ? 'bg-amber-400' : 'bg-emerald-400';
|
||||||
|
|
||||||
const borderColor = isTrial
|
const isLimitedStatus = subscription.status === 'limited';
|
||||||
? 'rgba(251,191,36,0.2)'
|
|
||||||
: isExpired
|
|
||||||
? 'rgba(255,59,92,0.15)'
|
|
||||||
: g.cardBorder;
|
|
||||||
|
|
||||||
const bgColor = isTrial
|
const borderColor =
|
||||||
? isDark
|
isTrial || isLimitedStatus
|
||||||
? 'rgba(251,191,36,0.04)'
|
? 'rgba(251,191,36,0.2)'
|
||||||
: 'rgba(251,191,36,0.03)'
|
: isExpired
|
||||||
: isExpired
|
? 'rgba(255,59,92,0.15)'
|
||||||
|
: g.cardBorder;
|
||||||
|
|
||||||
|
const bgColor =
|
||||||
|
isTrial || isLimitedStatus
|
||||||
? isDark
|
? isDark
|
||||||
? 'rgba(255,59,92,0.04)'
|
? 'rgba(251,191,36,0.04)'
|
||||||
: 'rgba(255,59,92,0.03)'
|
: 'rgba(251,191,36,0.03)'
|
||||||
: g.cardBg;
|
: isExpired
|
||||||
|
? isDark
|
||||||
|
? 'rgba(255,59,92,0.04)'
|
||||||
|
: 'rgba(255,59,92,0.03)'
|
||||||
|
: g.cardBg;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1658,7 +1658,10 @@ export default function AdminUserDetail() {
|
|||||||
.filter((tariffItem) => {
|
.filter((tariffItem) => {
|
||||||
const purchasedIds = new Set(
|
const purchasedIds = new Set(
|
||||||
userSubscriptions
|
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),
|
.map((s) => s.tariff_id),
|
||||||
);
|
);
|
||||||
return !purchasedIds.has(tariffItem.id);
|
return !purchasedIds.has(tariffItem.id);
|
||||||
|
|||||||
@@ -1316,68 +1316,25 @@ export default function Subscription() {
|
|||||||
<PurchaseCTAButton subscription={subscription} isMultiTariff={isMultiTariff} />
|
<PurchaseCTAButton subscription={subscription} isMultiTariff={isMultiTariff} />
|
||||||
|
|
||||||
{/* Delete expired subscription */}
|
{/* Delete expired subscription */}
|
||||||
{isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && (
|
{isMultiTariff &&
|
||||||
<div className="space-y-3">
|
subscription &&
|
||||||
{!showDeleteSheet ? (
|
!subscription.is_active &&
|
||||||
<button
|
!subscription.is_trial &&
|
||||||
onClick={async () => {
|
!subscription.is_limited && (
|
||||||
if (platform === 'telegram') {
|
<div className="space-y-3">
|
||||||
const confirmed = await destructiveConfirm(
|
{!showDeleteSheet ? (
|
||||||
t(
|
<button
|
||||||
'subscription.deleteWarning',
|
onClick={async () => {
|
||||||
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
if (platform === 'telegram') {
|
||||||
),
|
const confirmed = await destructiveConfirm(
|
||||||
t('subscription.confirmDelete', 'Да, удалить'),
|
t(
|
||||||
t('subscription.deleteTitle', 'Удалить подписку?'),
|
'subscription.deleteWarning',
|
||||||
);
|
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
|
||||||
if (!confirmed) return;
|
),
|
||||||
setDeleteLoading(true);
|
t('subscription.confirmDelete', 'Да, удалить'),
|
||||||
try {
|
t('subscription.deleteTitle', 'Удалить подписку?'),
|
||||||
await subscriptionApi.deleteSubscription(subscription.id);
|
);
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
if (!confirmed) return;
|
||||||
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 () => {
|
|
||||||
setDeleteLoading(true);
|
setDeleteLoading(true);
|
||||||
try {
|
try {
|
||||||
await subscriptionApi.deleteSubscription(subscription.id);
|
await subscriptionApi.deleteSubscription(subscription.id);
|
||||||
@@ -1385,28 +1342,75 @@ export default function Subscription() {
|
|||||||
navigate('/subscriptions', { replace: true });
|
navigate('/subscriptions', { replace: true });
|
||||||
} catch {
|
} catch {
|
||||||
setDeleteLoading(false);
|
setDeleteLoading(false);
|
||||||
setShowDeleteSheet(false);
|
|
||||||
}
|
}
|
||||||
}}
|
} else {
|
||||||
disabled={deleteLoading}
|
setShowDeleteSheet(true);
|
||||||
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"
|
}
|
||||||
|
}}
|
||||||
|
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
|
<path
|
||||||
? t('common.processing', 'Удаление...')
|
strokeLinecap="round"
|
||||||
: t('subscription.confirmDelete', 'Да, удалить')}
|
strokeLinejoin="round"
|
||||||
</button>
|
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"
|
||||||
<button
|
/>
|
||||||
onClick={() => setShowDeleteSheet(false)}
|
</svg>
|
||||||
className="flex-1 rounded-xl border border-dark-700 py-2.5 text-sm font-medium transition-colors hover:bg-dark-700"
|
{t('subscription.delete', 'Удалить подписку')}
|
||||||
style={{ color: g.textSecondary }}
|
</button>
|
||||||
>
|
) : (
|
||||||
{t('common.cancel', 'Отмена')}
|
<div
|
||||||
</button>
|
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>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Additional Options (Buy Devices) */}
|
{/* Additional Options (Buy Devices) */}
|
||||||
{subscription &&
|
{subscription &&
|
||||||
|
|||||||
@@ -781,7 +781,7 @@ export default function SubscriptionPurchase() {
|
|||||||
!isCurrentTariff &&
|
!isCurrentTariff &&
|
||||||
!subscription.is_trial &&
|
!subscription.is_trial &&
|
||||||
!isSubscriptionExpired &&
|
!isSubscriptionExpired &&
|
||||||
subscription.is_active;
|
(subscription.is_active || subscription.is_limited);
|
||||||
const isLegacySubscription =
|
const isLegacySubscription =
|
||||||
subscription && !subscription.is_trial && !subscription.tariff_id;
|
subscription && !subscription.is_trial && !subscription.tariff_id;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user