mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-09-15 01:03: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;
|
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">
|
||||||
|
|||||||
@@ -3650,7 +3650,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",
|
||||||
|
|||||||
@@ -3109,7 +3109,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": "موجودی فعلی",
|
||||||
|
|||||||
@@ -4047,7 +4047,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": "Текущий баланс",
|
||||||
|
|||||||
@@ -3108,7 +3108,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": "当前余额",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ActivityTab } from '../components/admin/userDetail/ActivityTab';
|
|||||||
import { TicketsTab } from '../components/admin/userDetail/TicketsTab';
|
import { TicketsTab } from '../components/admin/userDetail/TicketsTab';
|
||||||
import { InfoTab } from '../components/admin/userDetail/InfoTab';
|
import { InfoTab } from '../components/admin/userDetail/InfoTab';
|
||||||
import { SubscriptionTab } from '../components/admin/userDetail/SubscriptionTab';
|
import { SubscriptionTab } from '../components/admin/userDetail/SubscriptionTab';
|
||||||
|
import { getApiErrorMessage } from '../utils/api-error';
|
||||||
import { toNumber } from '../utils/inputHelpers';
|
import { toNumber } from '../utils/inputHelpers';
|
||||||
import { usePermissionStore } from '../store/permissions';
|
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 () => {
|
const handleDisableUser = async () => {
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
@@ -874,6 +897,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