mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-14 16:53:08 +00:00
feat(users): кнопка удаления подписки в карточке пользователя
В мультитарифном режиме у пользователя несколько подписок, и убрать лишнюю — например, отработавший триал — можно было только через экран «Массовые действия». Во вкладке «Подписка» карточки подписки видны и открываются, но удалить выбранную нечем. Кнопка живёт в детальном виде подписки и требует подтверждения тем же inline-механизмом, что и отмена автоплатежа; ключ подтверждения включает id подписки, чтобы взведённое согласие не пережило переключение на соседнюю. Активная платная подписка на сервере защищена от случайного удаления, поэтому для неё запрос идёт с явным force — намерение админ уже подтвердил.
This commit is contained in:
@@ -518,6 +518,19 @@ export const adminUsersApi = {
|
|||||||
return response.data;
|
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
|
// Update status
|
||||||
updateStatus: async (
|
updateStatus: async (
|
||||||
userId: number,
|
userId: number,
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export interface SubscriptionTabProps {
|
|||||||
onRemoveTraffic: (purchaseId: number) => Promise<void>;
|
onRemoveTraffic: (purchaseId: number) => Promise<void>;
|
||||||
onResetDevices: () => Promise<void>;
|
onResetDevices: () => Promise<void>;
|
||||||
onCancelSbpRecurring: () => Promise<void>;
|
onCancelSbpRecurring: () => Promise<void>;
|
||||||
|
onDeleteSubscription: () => Promise<void>;
|
||||||
onDeleteDevice: (hwid: string) => Promise<void>;
|
onDeleteDevice: (hwid: string) => Promise<void>;
|
||||||
onRenameDevice: (hwid: string) => Promise<void>;
|
onRenameDevice: (hwid: string) => Promise<void>;
|
||||||
onLoadDevices: () => Promise<void>;
|
onLoadDevices: () => Promise<void>;
|
||||||
@@ -195,6 +196,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
|||||||
onRemoveTraffic,
|
onRemoveTraffic,
|
||||||
onResetDevices,
|
onResetDevices,
|
||||||
onCancelSbpRecurring,
|
onCancelSbpRecurring,
|
||||||
|
onDeleteSubscription,
|
||||||
onDeleteDevice,
|
onDeleteDevice,
|
||||||
onRenameDevice,
|
onRenameDevice,
|
||||||
onLoadDevices,
|
onLoadDevices,
|
||||||
@@ -429,6 +431,41 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
|||||||
</div>
|
</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 */}
|
{/* Traffic Packages */}
|
||||||
{selectedSub.traffic_purchases && selectedSub.traffic_purchases.length > 0 && (
|
{selectedSub.traffic_purchases && selectedSub.traffic_purchases.length > 0 && (
|
||||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||||
|
|||||||
@@ -3646,7 +3646,11 @@
|
|||||||
"sbpCancelled": "SBP auto-payment disabled",
|
"sbpCancelled": "SBP auto-payment disabled",
|
||||||
"sbpStatus_PENDING": "Pending",
|
"sbpStatus_PENDING": "Pending",
|
||||||
"sbpStatus_ACTIVE": "Active",
|
"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": {
|
"balance": {
|
||||||
"current": "Current balance",
|
"current": "Current balance",
|
||||||
|
|||||||
@@ -3105,7 +3105,11 @@
|
|||||||
"sbpCancelled": "پرداخت خودکار SBP غیرفعال شد",
|
"sbpCancelled": "پرداخت خودکار SBP غیرفعال شد",
|
||||||
"sbpStatus_PENDING": "در انتظار",
|
"sbpStatus_PENDING": "در انتظار",
|
||||||
"sbpStatus_ACTIVE": "فعال",
|
"sbpStatus_ACTIVE": "فعال",
|
||||||
"sbpStatus_PAST_DUE": "پرداخت ناموفق"
|
"sbpStatus_PAST_DUE": "پرداخت ناموفق",
|
||||||
|
"deleteTitle": "حذف اشتراک",
|
||||||
|
"deleteHint": "اشتراک و دستگاههای آن برای همیشه حذف میشوند",
|
||||||
|
"deleteButton": "حذف اشتراک",
|
||||||
|
"deleted": "اشتراک حذف شد"
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"current": "موجودی فعلی",
|
"current": "موجودی فعلی",
|
||||||
|
|||||||
@@ -4043,7 +4043,11 @@
|
|||||||
"sbpCancelled": "Автоплатёж по СБП отключён",
|
"sbpCancelled": "Автоплатёж по СБП отключён",
|
||||||
"sbpStatus_PENDING": "Ожидает подтверждения",
|
"sbpStatus_PENDING": "Ожидает подтверждения",
|
||||||
"sbpStatus_ACTIVE": "Активен",
|
"sbpStatus_ACTIVE": "Активен",
|
||||||
"sbpStatus_PAST_DUE": "Платёж не прошёл"
|
"sbpStatus_PAST_DUE": "Платёж не прошёл",
|
||||||
|
"deleteTitle": "Удаление подписки",
|
||||||
|
"deleteHint": "Подписка и её устройства будут удалены безвозвратно",
|
||||||
|
"deleteButton": "Удалить подписку",
|
||||||
|
"deleted": "Подписка удалена"
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"current": "Текущий баланс",
|
"current": "Текущий баланс",
|
||||||
|
|||||||
@@ -3104,7 +3104,11 @@
|
|||||||
"sbpCancelled": "SBP 自动扣款已关闭",
|
"sbpCancelled": "SBP 自动扣款已关闭",
|
||||||
"sbpStatus_PENDING": "待确认",
|
"sbpStatus_PENDING": "待确认",
|
||||||
"sbpStatus_ACTIVE": "已启用",
|
"sbpStatus_ACTIVE": "已启用",
|
||||||
"sbpStatus_PAST_DUE": "扣款失败"
|
"sbpStatus_PAST_DUE": "扣款失败",
|
||||||
|
"deleteTitle": "删除订阅",
|
||||||
|
"deleteHint": "订阅及其设备将被永久删除",
|
||||||
|
"deleteButton": "删除订阅",
|
||||||
|
"deleted": "订阅已删除"
|
||||||
},
|
},
|
||||||
"balance": {
|
"balance": {
|
||||||
"current": "当前余额",
|
"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 () => {
|
const handleDisableUser = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
@@ -874,6 +892,7 @@ export default function AdminUserDetail() {
|
|||||||
userSubscriptions={userSubscriptions}
|
userSubscriptions={userSubscriptions}
|
||||||
selectedSub={selectedSub}
|
selectedSub={selectedSub}
|
||||||
onCancelSbpRecurring={handleCancelSbpRecurring}
|
onCancelSbpRecurring={handleCancelSbpRecurring}
|
||||||
|
onDeleteSubscription={handleDeleteSubscription}
|
||||||
activeSubscriptionId={activeSubscriptionId}
|
activeSubscriptionId={activeSubscriptionId}
|
||||||
onActiveSubscriptionChange={setActiveSubscriptionId}
|
onActiveSubscriptionChange={setActiveSubscriptionId}
|
||||||
subscriptionDetailView={subscriptionDetailView}
|
subscriptionDetailView={subscriptionDetailView}
|
||||||
|
|||||||
Reference in New Issue
Block a user