mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 01:23:47 +00:00
feat(admin): статус и отмена СБП-автопродления в карточке юзера
This commit is contained in:
@@ -130,6 +130,7 @@ export interface SubscriptionTabProps {
|
||||
onAddTraffic: (gb: number) => Promise<void>;
|
||||
onRemoveTraffic: (purchaseId: number) => Promise<void>;
|
||||
onResetDevices: () => Promise<void>;
|
||||
onCancelSbpRecurring: () => Promise<void>;
|
||||
onDeleteDevice: (hwid: string) => Promise<void>;
|
||||
onRenameDevice: (hwid: string) => Promise<void>;
|
||||
onLoadDevices: () => Promise<void>;
|
||||
@@ -193,6 +194,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
onAddTraffic,
|
||||
onRemoveTraffic,
|
||||
onResetDevices,
|
||||
onCancelSbpRecurring,
|
||||
onDeleteDevice,
|
||||
onRenameDevice,
|
||||
onLoadDevices,
|
||||
@@ -226,6 +228,14 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
{sub.tariff_name || `#${sub.id}`}
|
||||
</span>
|
||||
<StatusBadge status={sub.status} />
|
||||
{sub.sbp_recurring_status && (
|
||||
<span
|
||||
title={t('admin.users.detail.subscription.sbpTitle')}
|
||||
className="rounded-full bg-accent-500/15 px-2 py-0.5 text-[10px] font-medium text-accent-400"
|
||||
>
|
||||
SBP
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRightIcon className="h-4 w-4 text-dark-500" />
|
||||
</div>
|
||||
@@ -380,6 +390,41 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SBP (Platega) recurring auto-payment — status comes from the admin
|
||||
detail response; cancel is idempotent on the backend. */}
|
||||
{selectedSub.sbp_recurring_status && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-dark-200">
|
||||
{t('admin.users.detail.subscription.sbpTitle')}
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-dark-400">
|
||||
{t(
|
||||
`admin.users.detail.subscription.sbpStatus_${selectedSub.sbp_recurring_status}`,
|
||||
selectedSub.sbp_recurring_status,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasPermission('users:subscription') && (
|
||||
<button
|
||||
onClick={() => onInlineConfirm('cancelSbpRecurring', onCancelSbpRecurring)}
|
||||
disabled={actionLoading}
|
||||
className={`rounded-lg px-3 py-2 text-sm font-medium transition-all disabled:opacity-50 ${
|
||||
confirmingAction === 'cancelSbpRecurring'
|
||||
? 'bg-warning-500 text-white'
|
||||
: 'bg-warning-500/15 text-warning-400 hover:bg-warning-500/25'
|
||||
}`}
|
||||
>
|
||||
{confirmingAction === 'cancelSbpRecurring'
|
||||
? t('admin.users.detail.actions.areYouSure')
|
||||
: t('admin.users.detail.subscription.sbpCancel')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Traffic Packages */}
|
||||
{selectedSub.traffic_purchases && selectedSub.traffic_purchases.length > 0 && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
|
||||
@@ -648,6 +648,20 @@ export default function AdminUserDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelSbpRecurring = async () => {
|
||||
if (!userId || !selectedSub?.sbp_recurring_id) return;
|
||||
setActionLoading(true);
|
||||
try {
|
||||
await adminUsersApi.cancelSbpRecurring(userId, selectedSub.id);
|
||||
notify.success(t('admin.users.detail.subscription.sbpCancelled'), t('common.success'));
|
||||
await loadUser();
|
||||
} catch {
|
||||
notify.error(t('admin.users.userActions.error'), t('common.error'));
|
||||
} finally {
|
||||
setActionLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDisableUser = async () => {
|
||||
if (!userId) return;
|
||||
setActionLoading(true);
|
||||
@@ -713,7 +727,7 @@ export default function AdminUserDetail() {
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
@@ -859,6 +873,7 @@ export default function AdminUserDetail() {
|
||||
<SubscriptionTab
|
||||
userSubscriptions={userSubscriptions}
|
||||
selectedSub={selectedSub}
|
||||
onCancelSbpRecurring={handleCancelSbpRecurring}
|
||||
activeSubscriptionId={activeSubscriptionId}
|
||||
onActiveSubscriptionChange={setActiveSubscriptionId}
|
||||
subscriptionDetailView={subscriptionDetailView}
|
||||
|
||||
Reference in New Issue
Block a user