diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index aeb4a96..6b811a3 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -2,6 +2,15 @@ import apiClient from './client'; // ============ Types ============ +export interface TrafficPurchaseInfo { + id: number; + traffic_gb: number; + expires_at: string; + created_at: string; + days_remaining: number; + is_expired: boolean; +} + export interface UserSubscriptionInfo { id: number; status: string; @@ -16,6 +25,8 @@ export interface UserSubscriptionInfo { autopay_enabled: boolean; is_active: boolean; days_remaining: number; + purchased_traffic_gb: number; + traffic_purchases: TrafficPurchaseInfo[]; } export interface UserPromoGroupInfo { @@ -187,6 +198,11 @@ export interface UserAvailableTariff { price_per_day_kopeks: number; min_days: number; max_days: number; + device_price_kopeks: number | null; + max_device_limit: number | null; + traffic_topup_enabled: boolean; + traffic_topup_packages: Record; + max_topup_traffic_gb: number; is_available: boolean; requires_promo_group: boolean; } @@ -277,7 +293,10 @@ export interface UpdateSubscriptionRequest { | 'toggle_autopay' | 'cancel' | 'activate' - | 'create'; + | 'create' + | 'add_traffic' + | 'remove_traffic' + | 'set_device_limit'; days?: number; end_date?: string; tariff_id?: number; @@ -286,6 +305,8 @@ export interface UpdateSubscriptionRequest { autopay_enabled?: boolean; is_trial?: boolean; device_limit?: number; + traffic_gb?: number; + traffic_purchase_id?: number; } export interface UpdateSubscriptionResponse { diff --git a/src/locales/en.json b/src/locales/en.json index b8b8761..7ccfa11 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1967,7 +1967,17 @@ "unavailable": "(unavailable)", "noActive": "No active subscription", "create": "Create subscription", - "creating": "Creating..." + "creating": "Creating...", + "trafficPackages": "Traffic packages", + "addTraffic": "Add traffic", + "selectPackage": "Select package", + "addButton": "Add", + "addTrafficNote": "Admin: free, valid 30 days", + "trafficRemoved": "Traffic package removed", + "trafficAdded": "Traffic package added", + "expired": "expired", + "daysLeft": "d left", + "deviceLimitUpdated": "Device limit updated" }, "balance": { "current": "Current balance", diff --git a/src/locales/fa.json b/src/locales/fa.json index 0004a65..0e56e32 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1666,7 +1666,17 @@ "unavailable": "(غیرقابل دسترس)", "noActive": "اشتراک فعالی وجود ندارد", "create": "ایجاد اشتراک", - "creating": "در حال ایجاد..." + "creating": "در حال ایجاد...", + "trafficPackages": "بسته‌های ترافیک", + "addTraffic": "افزودن ترافیک", + "selectPackage": "انتخاب بسته", + "addButton": "افزودن", + "addTrafficNote": "مدیر: رایگان، ۳۰ روز اعتبار", + "trafficRemoved": "بسته ترافیک حذف شد", + "trafficAdded": "بسته ترافیک اضافه شد", + "expired": "منقضی شده", + "daysLeft": "روز باقیمانده", + "deviceLimitUpdated": "محدودیت دستگاه به‌روز شد" }, "balance": { "current": "موجودی فعلی", diff --git a/src/locales/ru.json b/src/locales/ru.json index 7962936..f69ed6e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2493,7 +2493,17 @@ "unavailable": "(недоступен)", "noActive": "Нет активной подписки", "create": "Создать подписку", - "creating": "Создание..." + "creating": "Создание...", + "trafficPackages": "Пакеты трафика", + "addTraffic": "Добавить трафик", + "selectPackage": "Выберите пакет", + "addButton": "Добавить", + "addTrafficNote": "Админ: бесплатно, действует 30 дней", + "trafficRemoved": "Пакет трафика удалён", + "trafficAdded": "Пакет трафика добавлен", + "expired": "истёк", + "daysLeft": "дн. осталось", + "deviceLimitUpdated": "Лимит устройств обновлён" }, "balance": { "current": "Текущий баланс", diff --git a/src/locales/zh.json b/src/locales/zh.json index 5f0fe56..a05d235 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1665,7 +1665,17 @@ "unavailable": "(不可用)", "noActive": "无活跃订阅", "create": "创建订阅", - "creating": "创建中..." + "creating": "创建中...", + "trafficPackages": "流量包", + "addTraffic": "添加流量", + "selectPackage": "选择套餐", + "addButton": "添加", + "addTrafficNote": "管理员:免费,有效期30天", + "trafficRemoved": "流量包已删除", + "trafficAdded": "流量包已添加", + "expired": "已过期", + "daysLeft": "天剩余", + "deviceLimitUpdated": "设备限制已更新" }, "balance": { "current": "当前余额", diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index 94afc93..0d690ba 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -195,6 +195,9 @@ export default function AdminUserDetail() { const [offerValidHours, setOfferValidHours] = useState(24); const [offerSending, setOfferSending] = useState(false); + // Traffic packages + const [selectedTrafficGb, setSelectedTrafficGb] = useState(''); + // Devices const [devices, setDevices] = useState< { hwid: string; platform: string; device_model: string; created_at: string | null }[] @@ -544,6 +547,57 @@ export default function AdminUserDetail() { } }; + const handleAddTraffic = async (gb: number) => { + if (!userId) return; + setActionLoading(true); + try { + await adminUsersApi.updateSubscription(userId, { action: 'add_traffic', traffic_gb: gb }); + notify.success(t('admin.users.detail.subscription.trafficAdded')); + setSelectedTrafficGb(''); + await loadUser(); + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleRemoveTraffic = async (purchaseId: number) => { + if (!userId) return; + setActionLoading(true); + try { + await adminUsersApi.updateSubscription(userId, { + action: 'remove_traffic', + traffic_purchase_id: purchaseId, + }); + notify.success(t('admin.users.detail.subscription.trafficRemoved')); + await loadUser(); + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleSetDeviceLimit = async (newLimit: number) => { + if (!userId) return; + setActionLoading(true); + try { + await adminUsersApi.updateSubscription(userId, { + action: 'set_device_limit', + device_limit: newLimit, + }); + notify.success(t('admin.users.detail.subscription.deviceLimitUpdated')); + await loadUser(); + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const currentTariff = tariffs.find((t) => t.id === user?.subscription?.tariff_id) || null; + const handleChangePromoGroup = async (groupId: number | null) => { if (!userId) return; setActionLoading(true); @@ -1166,11 +1220,136 @@ export default function AdminUserDetail() {
{t('admin.users.detail.subscription.devices')}
-
{user.subscription.device_limit}
+
+ + + {user.subscription.device_limit} + + +
+ {/* Traffic Packages */} + {user.subscription.traffic_purchases && + user.subscription.traffic_purchases.length > 0 && ( +
+
+ + {t('admin.users.detail.subscription.trafficPackages')} + {user.subscription.purchased_traffic_gb > 0 && ( + + ({user.subscription.purchased_traffic_gb} {t('common.units.gb')}) + + )} + +
+
+ {user.subscription.traffic_purchases.map((tp) => ( +
+
+
+ + {tp.traffic_gb} {t('common.units.gb')} + + {tp.is_expired ? ( + + {t('admin.users.detail.subscription.expired')} + + ) : ( + + {tp.days_remaining}{' '} + {t('admin.users.detail.subscription.daysLeft')} + + )} +
+
+ {!tp.is_expired && ( + + )} +
+ ))} +
+
+ )} + + {/* Add Traffic */} + {currentTariff && + currentTariff.traffic_topup_enabled && + Object.keys(currentTariff.traffic_topup_packages).length > 0 && ( +
+
+ {t('admin.users.detail.subscription.addTraffic')} +
+
+ + +
+
+ {t('admin.users.detail.subscription.addTrafficNote')} +
+
+ )} + {/* Actions */}