mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-13 16:23:08 +00:00
feat(users): кнопка удаления подписки в карточке пользователя
В мультитарифном режиме у пользователя несколько подписок, и убрать лишнюю — например, отработавший триал — можно было только через экран «Массовые действия». Во вкладке «Подписка» карточки подписки видны и открываются, но удалить выбранную нечем. Кнопка живёт в детальном виде подписки и требует подтверждения тем же inline-механизмом, что и отмена автоплатежа; ключ подтверждения включает id подписки, чтобы взведённое согласие не пережило переключение на соседнюю. Активная платная подписка на сервере защищена от случайного удаления, поэтому для неё запрос идёт с явным force — намерение админ уже подтвердил.
This commit is contained in:
@@ -518,6 +518,19 @@ export const adminUsersApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Delete one of the user's subscriptions (multi-tariff: trials pile up)
|
||||
deleteSubscription: async (
|
||||
userId: number,
|
||||
subId: number,
|
||||
force = false,
|
||||
): Promise<{ status: string }> => {
|
||||
const response = await apiClient.delete(
|
||||
`/cabinet/admin/users/${userId}/subscriptions/${subId}`,
|
||||
{ params: force ? { force: true } : undefined },
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update status
|
||||
updateStatus: async (
|
||||
userId: number,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -3646,7 +3646,11 @@
|
||||
"sbpCancelled": "SBP auto-payment disabled",
|
||||
"sbpStatus_PENDING": "Pending",
|
||||
"sbpStatus_ACTIVE": "Active",
|
||||
"sbpStatus_PAST_DUE": "Payment failed"
|
||||
"sbpStatus_PAST_DUE": "Payment failed",
|
||||
"deleteTitle": "Delete subscription",
|
||||
"deleteHint": "The subscription and its devices will be removed permanently",
|
||||
"deleteButton": "Delete subscription",
|
||||
"deleted": "Subscription deleted"
|
||||
},
|
||||
"balance": {
|
||||
"current": "Current balance",
|
||||
|
||||
@@ -3105,7 +3105,11 @@
|
||||
"sbpCancelled": "پرداخت خودکار SBP غیرفعال شد",
|
||||
"sbpStatus_PENDING": "در انتظار",
|
||||
"sbpStatus_ACTIVE": "فعال",
|
||||
"sbpStatus_PAST_DUE": "پرداخت ناموفق"
|
||||
"sbpStatus_PAST_DUE": "پرداخت ناموفق",
|
||||
"deleteTitle": "حذف اشتراک",
|
||||
"deleteHint": "اشتراک و دستگاههای آن برای همیشه حذف میشوند",
|
||||
"deleteButton": "حذف اشتراک",
|
||||
"deleted": "اشتراک حذف شد"
|
||||
},
|
||||
"balance": {
|
||||
"current": "موجودی فعلی",
|
||||
|
||||
@@ -4043,7 +4043,11 @@
|
||||
"sbpCancelled": "Автоплатёж по СБП отключён",
|
||||
"sbpStatus_PENDING": "Ожидает подтверждения",
|
||||
"sbpStatus_ACTIVE": "Активен",
|
||||
"sbpStatus_PAST_DUE": "Платёж не прошёл"
|
||||
"sbpStatus_PAST_DUE": "Платёж не прошёл",
|
||||
"deleteTitle": "Удаление подписки",
|
||||
"deleteHint": "Подписка и её устройства будут удалены безвозвратно",
|
||||
"deleteButton": "Удалить подписку",
|
||||
"deleted": "Подписка удалена"
|
||||
},
|
||||
"balance": {
|
||||
"current": "Текущий баланс",
|
||||
|
||||
@@ -3104,7 +3104,11 @@
|
||||
"sbpCancelled": "SBP 自动扣款已关闭",
|
||||
"sbpStatus_PENDING": "待确认",
|
||||
"sbpStatus_ACTIVE": "已启用",
|
||||
"sbpStatus_PAST_DUE": "扣款失败"
|
||||
"sbpStatus_PAST_DUE": "扣款失败",
|
||||
"deleteTitle": "删除订阅",
|
||||
"deleteHint": "订阅及其设备将被永久删除",
|
||||
"deleteButton": "删除订阅",
|
||||
"deleted": "订阅已删除"
|
||||
},
|
||||
"balance": {
|
||||
"current": "当前余额",
|
||||
|
||||
@@ -662,6 +662,24 @@ export default function AdminUserDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteSubscription = async () => {
|
||||
if (!userId || !selectedSub) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
// Активную платную подписку сервер по умолчанию бережёт — админ уже
|
||||
// подтвердил намерение кнопкой, поэтому просим удалить именно её.
|
||||
const force = Boolean(selectedSub.is_active) && !selectedSub.is_trial;
|
||||
await adminUsersApi.deleteSubscription(userId, selectedSub.id, force);
|
||||
notify.success(t('admin.users.detail.subscription.deleted'), t('common.success'));
|
||||
setSubscriptionDetailView(false);
|
||||
await loadUser();
|
||||
} catch {
|
||||
notify.error(t('admin.users.userActions.error'), t('common.error'));
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisableUser = async () => {
|
||||
if (!userId) return;
|
||||
setActionLoading(true);
|
||||
@@ -874,6 +892,7 @@ export default function AdminUserDetail() {
|
||||
userSubscriptions={userSubscriptions}
|
||||
selectedSub={selectedSub}
|
||||
onCancelSbpRecurring={handleCancelSbpRecurring}
|
||||
onDeleteSubscription={handleDeleteSubscription}
|
||||
activeSubscriptionId={activeSubscriptionId}
|
||||
onActiveSubscriptionChange={setActiveSubscriptionId}
|
||||
subscriptionDetailView={subscriptionDetailView}
|
||||
|
||||
Reference in New Issue
Block a user