feat(users): кнопка удаления подписки в карточке пользователя

В мультитарифном режиме у пользователя несколько подписок, и убрать
лишнюю — например, отработавший триал — можно было только через экран
«Массовые действия». Во вкладке «Подписка» карточки подписки видны и
открываются, но удалить выбранную нечем.

Кнопка живёт в детальном виде подписки и требует подтверждения тем же
inline-механизмом, что и отмена автоплатежа; ключ подтверждения включает
id подписки, чтобы взведённое согласие не пережило переключение на
соседнюю. Активная платная подписка на сервере защищена от случайного
удаления, поэтому для неё запрос идёт с явным force — намерение админ
уже подтвердил.
This commit is contained in:
Case211
2026-08-04 23:38:20 +05:00
parent b866bebeeb
commit eafc563128
7 changed files with 89 additions and 4 deletions

View File

@@ -131,6 +131,7 @@ export interface SubscriptionTabProps {
onRemoveTraffic: (purchaseId: number) => Promise<void>;
onResetDevices: () => Promise<void>;
onCancelSbpRecurring: () => Promise<void>;
onDeleteSubscription: () => Promise<void>;
onDeleteDevice: (hwid: string) => Promise<void>;
onRenameDevice: (hwid: string) => Promise<void>;
onLoadDevices: () => Promise<void>;
@@ -195,6 +196,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
onRemoveTraffic,
onResetDevices,
onCancelSbpRecurring,
onDeleteSubscription,
onDeleteDevice,
onRenameDevice,
onLoadDevices,
@@ -429,6 +431,41 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</div>
)}
{/* Delete this subscription — in multi-tariff mode spent trials
pile up in the card, and removing one used to be possible
only through the bulk-actions screen. */}
{hasPermission('users:subscription') && (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-sm font-medium text-dark-200">
{t('admin.users.detail.subscription.deleteTitle')}
</div>
<div className="mt-0.5 text-xs text-dark-400">
{t('admin.users.detail.subscription.deleteHint')}
</div>
</div>
<button
// Per-subscription confirm key: an armed confirm must not
// survive switching to another subscription in the picker.
onClick={() =>
onInlineConfirm(`deleteSubscription_${selectedSub.id}`, onDeleteSubscription)
}
disabled={actionLoading}
className={`shrink-0 rounded-lg px-3 py-2 text-sm font-medium transition-all disabled:opacity-50 ${
confirmingAction === `deleteSubscription_${selectedSub.id}`
? 'bg-error-500 text-white'
: 'bg-error-500/15 text-error-400 hover:bg-error-500/25'
}`}
>
{confirmingAction === `deleteSubscription_${selectedSub.id}`
? t('admin.users.detail.actions.areYouSure')
: t('admin.users.detail.subscription.deleteButton')}
</button>
</div>
</div>
)}
{/* Traffic Packages */}
{selectedSub.traffic_purchases && selectedSub.traffic_purchases.length > 0 && (
<div className="rounded-xl bg-dark-800/50 p-4">