mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-13 16:23:08 +00:00
Merge pull request #542 from Case211/feat/admin-delete-subscription
This commit is contained in:
@@ -519,6 +519,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">
|
||||
|
||||
@@ -3650,7 +3650,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",
|
||||
|
||||
@@ -3109,7 +3109,11 @@
|
||||
"sbpCancelled": "پرداخت خودکار SBP غیرفعال شد",
|
||||
"sbpStatus_PENDING": "در انتظار",
|
||||
"sbpStatus_ACTIVE": "فعال",
|
||||
"sbpStatus_PAST_DUE": "پرداخت ناموفق"
|
||||
"sbpStatus_PAST_DUE": "پرداخت ناموفق",
|
||||
"deleteTitle": "حذف اشتراک",
|
||||
"deleteHint": "اشتراک و دستگاههای آن برای همیشه حذف میشوند",
|
||||
"deleteButton": "حذف اشتراک",
|
||||
"deleted": "اشتراک حذف شد"
|
||||
},
|
||||
"balance": {
|
||||
"current": "موجودی فعلی",
|
||||
|
||||
@@ -4047,7 +4047,11 @@
|
||||
"sbpCancelled": "Автоплатёж по СБП отключён",
|
||||
"sbpStatus_PENDING": "Ожидает подтверждения",
|
||||
"sbpStatus_ACTIVE": "Активен",
|
||||
"sbpStatus_PAST_DUE": "Платёж не прошёл"
|
||||
"sbpStatus_PAST_DUE": "Платёж не прошёл",
|
||||
"deleteTitle": "Удаление подписки",
|
||||
"deleteHint": "Подписка и её устройства будут удалены безвозвратно",
|
||||
"deleteButton": "Удалить подписку",
|
||||
"deleted": "Подписка удалена"
|
||||
},
|
||||
"balance": {
|
||||
"current": "Текущий баланс",
|
||||
|
||||
@@ -3108,7 +3108,11 @@
|
||||
"sbpCancelled": "SBP 自动扣款已关闭",
|
||||
"sbpStatus_PENDING": "待确认",
|
||||
"sbpStatus_ACTIVE": "已启用",
|
||||
"sbpStatus_PAST_DUE": "扣款失败"
|
||||
"sbpStatus_PAST_DUE": "扣款失败",
|
||||
"deleteTitle": "删除订阅",
|
||||
"deleteHint": "订阅及其设备将被永久删除",
|
||||
"deleteButton": "删除订阅",
|
||||
"deleted": "订阅已删除"
|
||||
},
|
||||
"balance": {
|
||||
"current": "当前余额",
|
||||
|
||||
@@ -28,6 +28,7 @@ import { ActivityTab } from '../components/admin/userDetail/ActivityTab';
|
||||
import { TicketsTab } from '../components/admin/userDetail/TicketsTab';
|
||||
import { InfoTab } from '../components/admin/userDetail/InfoTab';
|
||||
import { SubscriptionTab } from '../components/admin/userDetail/SubscriptionTab';
|
||||
import { getApiErrorMessage } from '../utils/api-error';
|
||||
import { toNumber } from '../utils/inputHelpers';
|
||||
import { usePermissionStore } from '../store/permissions';
|
||||
|
||||
@@ -662,6 +663,28 @@ 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 (err) {
|
||||
// Отказы тут осмысленные и действенные: открытый временный доступ
|
||||
// (409, «сначала заверши или восстанови grace»), активная платная без
|
||||
// force (409), подписки нет (404). Общее «Ошибка» оставило бы админа
|
||||
// гадать, почему кнопка не сработала, — показываем текст сервера.
|
||||
notify.error(getApiErrorMessage(err, t('admin.users.userActions.error')), t('common.error'));
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisableUser = async () => {
|
||||
if (!userId) return;
|
||||
setActionLoading(true);
|
||||
@@ -874,6 +897,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