From 6f31fbe6b5638e400db2ea16af65ab69979dca97 Mon Sep 17 00:00:00 2001 From: Fringg Date: Sun, 8 Feb 2026 20:49:07 +0300 Subject: [PATCH] feat: add device management UI in admin user card Show connected devices in subscription tab with ability to: - View device platform, model, HWID and connection date - Delete individual devices - Reset all devices at once --- src/api/adminUsers.ts | 29 ++++++++ src/locales/en.json | 7 ++ src/locales/fa.json | 7 ++ src/locales/ru.json | 7 ++ src/locales/zh.json | 7 ++ src/pages/AdminUserDetail.tsx | 136 +++++++++++++++++++++++++++++++++- 6 files changed, 191 insertions(+), 2 deletions(-) diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index 60f4cfe..aeb4a96 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -563,4 +563,33 @@ export const adminUsersApi = { const response = await apiClient.get(`/cabinet/admin/users/${userId}/node-usage`); return response.data; }, + + // Get user devices + getUserDevices: async ( + userId: number, + ): Promise<{ + devices: { hwid: string; platform: string; device_model: string; created_at: string | null }[]; + total: number; + device_limit: number; + }> => { + const response = await apiClient.get(`/cabinet/admin/users/${userId}/devices`); + return response.data; + }, + + // Delete single device + deleteUserDevice: async ( + userId: number, + hwid: string, + ): Promise<{ success: boolean; message: string; deleted_hwid: string | null }> => { + const response = await apiClient.delete(`/cabinet/admin/users/${userId}/devices/${hwid}`); + return response.data; + }, + + // Reset all devices + resetUserDevices: async ( + userId: number, + ): Promise<{ success: boolean; message: string; deleted_count: number }> => { + const response = await apiClient.delete(`/cabinet/admin/users/${userId}/devices`); + return response.data; + }, }; diff --git a/src/locales/en.json b/src/locales/en.json index cb4d7dd..b8b8761 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1909,6 +1909,13 @@ "nodeUsage": "Node usage", "copied": "Copied", "panelNotFound": "User not found in panel", + "devices": { + "title": "Devices", + "none": "No connected devices", + "resetAll": "Reset all", + "deleted": "Device removed", + "allDeleted": "All devices reset" + }, "referral": { "title": "Referral program", "referrals": "Referrals", diff --git a/src/locales/fa.json b/src/locales/fa.json index a6cfb9f..0004a65 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1612,6 +1612,13 @@ "nodeUsage": "مصرف نودها", "copied": "کپی شد", "panelNotFound": "کاربر در پنل یافت نشد", + "devices": { + "title": "دستگاه‌ها", + "none": "دستگاه متصلی وجود ندارد", + "resetAll": "بازنشانی همه", + "deleted": "دستگاه حذف شد", + "allDeleted": "همه دستگاه‌ها بازنشانی شدند" + }, "referral": { "title": "برنامه ارجاع", "referrals": "ارجاعات", diff --git a/src/locales/ru.json b/src/locales/ru.json index edc6d87..7962936 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2435,6 +2435,13 @@ "nodeUsage": "Расход по нодам", "copied": "Скопировано", "panelNotFound": "Пользователь не найден в панели", + "devices": { + "title": "Устройства", + "none": "Нет подключённых устройств", + "resetAll": "Сбросить все", + "deleted": "Устройство удалено", + "allDeleted": "Все устройства сброшены" + }, "referral": { "title": "Реферальная программа", "referrals": "Рефералов", diff --git a/src/locales/zh.json b/src/locales/zh.json index 38e20ce..5f0fe56 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1611,6 +1611,13 @@ "nodeUsage": "节点用量", "copied": "已复制", "panelNotFound": "面板中未找到用户", + "devices": { + "title": "设备", + "none": "没有已连接的设备", + "resetAll": "重置全部", + "deleted": "设备已删除", + "allDeleted": "所有设备已重置" + }, "referral": { "title": "推荐计划", "referrals": "推荐人数", diff --git a/src/pages/AdminUserDetail.tsx b/src/pages/AdminUserDetail.tsx index a93611d..94afc93 100644 --- a/src/pages/AdminUserDetail.tsx +++ b/src/pages/AdminUserDetail.tsx @@ -195,6 +195,14 @@ export default function AdminUserDetail() { const [offerValidHours, setOfferValidHours] = useState(24); const [offerSending, setOfferSending] = useState(false); + // Devices + const [devices, setDevices] = useState< + { hwid: string; platform: string; device_model: string; created_at: string | null }[] + >([]); + const [devicesTotal, setDevicesTotal] = useState(0); + const [deviceLimit, setDeviceLimit] = useState(0); + const [devicesLoading, setDevicesLoading] = useState(false); + const userId = id ? parseInt(id, 10) : null; const loadUser = useCallback(async () => { @@ -293,9 +301,24 @@ export default function AdminUserDetail() { } }, [userId]); + const loadDevices = useCallback(async () => { + if (!userId) return; + try { + setDevicesLoading(true); + const data = await adminUsersApi.getUserDevices(userId); + setDevices(data.devices); + setDevicesTotal(data.total); + setDeviceLimit(data.device_limit); + } catch { + // ignore + } finally { + setDevicesLoading(false); + } + }, [userId]); + const loadSubscriptionData = useCallback(async () => { - await Promise.all([loadPanelInfo(), loadNodeUsage()]); - }, [loadPanelInfo, loadNodeUsage]); + await Promise.all([loadPanelInfo(), loadNodeUsage(), loadDevices()]); + }, [loadPanelInfo, loadNodeUsage, loadDevices]); const loadPromoGroups = useCallback(async () => { try { @@ -493,6 +516,34 @@ export default function AdminUserDetail() { } }; + const handleDeleteDevice = async (hwid: string) => { + if (!userId) return; + setActionLoading(true); + try { + await adminUsersApi.deleteUserDevice(userId, hwid); + notify.success(t('admin.users.detail.devices.deleted')); + await loadDevices(); + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + + const handleResetDevices = async () => { + if (!userId) return; + setActionLoading(true); + try { + await adminUsersApi.resetUserDevices(userId); + notify.success(t('admin.users.detail.devices.allDeleted')); + await loadDevices(); + } catch { + notify.error(t('admin.users.userActions.error'), t('common.error')); + } finally { + setActionLoading(false); + } + }; + const handleChangePromoGroup = async (groupId: number | null) => { if (!userId) return; setActionLoading(true); @@ -1453,6 +1504,87 @@ export default function AdminUserDetail() { ) : null} + + {/* Devices */} +
+
+ + {t('admin.users.detail.devices.title')} ({devicesTotal}/{deviceLimit}) + +
+ + {devices.length > 0 && ( + + )} +
+
+ {devicesLoading ? ( +
+
+
+ ) : devices.length > 0 ? ( +
+ {devices.map((device) => ( +
+
+
+ {device.platform || device.device_model || device.hwid.slice(0, 12)} +
+
+ {device.device_model && device.platform && ( + {device.device_model} + )} + {device.hwid.slice(0, 8)}... + {device.created_at && ( + {new Date(device.created_at).toLocaleDateString(locale)} + )} +
+
+ +
+ ))} +
+ ) : ( +
+ {t('admin.users.detail.devices.none')} +
+ )} +
)}