feat(admin): статус и отмена СБП-автопродления в карточке юзера

This commit is contained in:
Fringg
2026-07-24 06:45:00 +03:00
parent e8409a9007
commit 332849393c
2 changed files with 61 additions and 1 deletions

View File

@@ -130,6 +130,7 @@ export interface SubscriptionTabProps {
onAddTraffic: (gb: number) => Promise<void>; onAddTraffic: (gb: number) => Promise<void>;
onRemoveTraffic: (purchaseId: number) => Promise<void>; onRemoveTraffic: (purchaseId: number) => Promise<void>;
onResetDevices: () => Promise<void>; onResetDevices: () => Promise<void>;
onCancelSbpRecurring: () => 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>;
@@ -193,6 +194,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
onAddTraffic, onAddTraffic,
onRemoveTraffic, onRemoveTraffic,
onResetDevices, onResetDevices,
onCancelSbpRecurring,
onDeleteDevice, onDeleteDevice,
onRenameDevice, onRenameDevice,
onLoadDevices, onLoadDevices,
@@ -226,6 +228,14 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
{sub.tariff_name || `#${sub.id}`} {sub.tariff_name || `#${sub.id}`}
</span> </span>
<StatusBadge status={sub.status} /> <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> </div>
<ChevronRightIcon className="h-4 w-4 text-dark-500" /> <ChevronRightIcon className="h-4 w-4 text-dark-500" />
</div> </div>
@@ -380,6 +390,41 @@ export function SubscriptionTab(props: SubscriptionTabProps) {
</div> </div>
</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 */} {/* 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">

View File

@@ -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 () => { const handleDisableUser = async () => {
if (!userId) return; if (!userId) return;
setActionLoading(true); setActionLoading(true);
@@ -713,7 +727,7 @@ export default function AdminUserDetail() {
const k = 1024; const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); 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) => { const copyToClipboard = async (text: string) => {
@@ -859,6 +873,7 @@ export default function AdminUserDetail() {
<SubscriptionTab <SubscriptionTab
userSubscriptions={userSubscriptions} userSubscriptions={userSubscriptions}
selectedSub={selectedSub} selectedSub={selectedSub}
onCancelSbpRecurring={handleCancelSbpRecurring}
activeSubscriptionId={activeSubscriptionId} activeSubscriptionId={activeSubscriptionId}
onActiveSubscriptionChange={setActiveSubscriptionId} onActiveSubscriptionChange={setActiveSubscriptionId}
subscriptionDetailView={subscriptionDetailView} subscriptionDetailView={subscriptionDetailView}