mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Merge pull request #175 from BEDOLAGA-DEV/feat/node-usage-local-period
feat: local period calculation and refresh button for node usage
This commit is contained in:
@@ -134,10 +134,12 @@ export interface UserNodeUsageItem {
|
|||||||
node_name: string;
|
node_name: string;
|
||||||
country_code: string;
|
country_code: string;
|
||||||
total_bytes: number;
|
total_bytes: number;
|
||||||
|
daily_bytes: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserNodeUsageResponse {
|
export interface UserNodeUsageResponse {
|
||||||
items: UserNodeUsageItem[];
|
items: UserNodeUsageItem[];
|
||||||
|
categories: string[];
|
||||||
period_days: number;
|
period_days: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,11 +547,9 @@ export const adminUsersApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Get node usage
|
// Get node usage (always 30 days with daily breakdown)
|
||||||
getNodeUsage: async (userId: number, days = 7): Promise<UserNodeUsageResponse> => {
|
getNodeUsage: async (userId: number): Promise<UserNodeUsageResponse> => {
|
||||||
const response = await apiClient.get(`/cabinet/admin/users/${userId}/node-usage`, {
|
const response = await apiClient.get(`/cabinet/admin/users/${userId}/node-usage`);
|
||||||
params: { days },
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -268,18 +268,19 @@ export default function AdminUserDetail() {
|
|||||||
}
|
}
|
||||||
}, [userId]);
|
}, [userId]);
|
||||||
|
|
||||||
const loadNodeUsage = useCallback(
|
const loadNodeUsage = useCallback(async () => {
|
||||||
async (days = 7) => {
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
try {
|
try {
|
||||||
const data = await adminUsersApi.getNodeUsage(userId, days);
|
const data = await adminUsersApi.getNodeUsage(userId);
|
||||||
setNodeUsage(data);
|
setNodeUsage(data);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
},
|
}, [userId]);
|
||||||
[userId],
|
|
||||||
);
|
const loadSubscriptionData = useCallback(async () => {
|
||||||
|
await Promise.all([loadPanelInfo(), loadNodeUsage()]);
|
||||||
|
}, [loadPanelInfo, loadNodeUsage]);
|
||||||
|
|
||||||
const handleTicketReply = async () => {
|
const handleTicketReply = async () => {
|
||||||
if (!selectedTicketId || !replyText.trim()) return;
|
if (!selectedTicketId || !replyText.trim()) return;
|
||||||
@@ -335,16 +336,10 @@ export default function AdminUserDetail() {
|
|||||||
if (activeTab === 'sync') loadSyncStatus();
|
if (activeTab === 'sync') loadSyncStatus();
|
||||||
if (activeTab === 'subscription') {
|
if (activeTab === 'subscription') {
|
||||||
loadTariffs();
|
loadTariffs();
|
||||||
loadPanelInfo();
|
loadSubscriptionData();
|
||||||
}
|
}
|
||||||
if (activeTab === 'tickets') loadTickets();
|
if (activeTab === 'tickets') loadTickets();
|
||||||
}, [activeTab, loadSyncStatus, loadTariffs, loadTickets, loadReferrals, loadPanelInfo]);
|
}, [activeTab, loadSyncStatus, loadTariffs, loadTickets, loadReferrals, loadSubscriptionData]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTab === 'subscription') {
|
|
||||||
loadNodeUsage(nodeUsageDays);
|
|
||||||
}
|
|
||||||
}, [activeTab, loadNodeUsage, nodeUsageDays]);
|
|
||||||
|
|
||||||
const handleUpdateBalance = async (isAdd: boolean) => {
|
const handleUpdateBalance = async (isAdd: boolean) => {
|
||||||
if (balanceAmount === '' || !userId) return;
|
if (balanceAmount === '' || !userId) return;
|
||||||
@@ -547,6 +542,19 @@ export default function AdminUserDetail() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Compute node usage for selected period from cached 30-day data
|
||||||
|
const nodeUsageForPeriod = (() => {
|
||||||
|
if (!nodeUsage || nodeUsage.items.length === 0) return [];
|
||||||
|
return nodeUsage.items
|
||||||
|
.map((item) => {
|
||||||
|
const daily = item.daily_bytes || [];
|
||||||
|
const sliced = daily.slice(-nodeUsageDays);
|
||||||
|
const total = sliced.reduce((sum, v) => sum + v, 0);
|
||||||
|
return { ...item, total_bytes: total };
|
||||||
|
})
|
||||||
|
.sort((a, b) => b.total_bytes - a.total_bytes);
|
||||||
|
})();
|
||||||
|
|
||||||
const formatBytes = (bytes: number) => {
|
const formatBytes = (bytes: number) => {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
if (bytes < 1024) return `${bytes} B`;
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
@@ -1198,6 +1206,7 @@ export default function AdminUserDetail() {
|
|||||||
<span className="text-sm font-medium text-dark-200">
|
<span className="text-sm font-medium text-dark-200">
|
||||||
{t('admin.users.detail.nodeUsage')}
|
{t('admin.users.detail.nodeUsage')}
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{[7, 14, 30].map((d) => (
|
{[7, 14, 30].map((d) => (
|
||||||
<button
|
<button
|
||||||
@@ -1213,11 +1222,19 @@ export default function AdminUserDetail() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => loadSubscriptionData()}
|
||||||
|
className="rounded-lg p-1 text-dark-500 transition-colors hover:text-dark-300"
|
||||||
|
title={t('common.refresh')}
|
||||||
|
>
|
||||||
|
<RefreshIcon className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{nodeUsage && nodeUsage.items.length > 0 ? (
|
</div>
|
||||||
|
{nodeUsageForPeriod.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{nodeUsage.items.map((item) => {
|
{nodeUsageForPeriod.map((item) => {
|
||||||
const maxBytes = nodeUsage.items[0].total_bytes;
|
const maxBytes = nodeUsageForPeriod[0].total_bytes;
|
||||||
const pct = maxBytes > 0 ? (item.total_bytes / maxBytes) * 100 : 0;
|
const pct = maxBytes > 0 ? (item.total_bytes / maxBytes) * 100 : 0;
|
||||||
return (
|
return (
|
||||||
<div key={item.node_uuid}>
|
<div key={item.node_uuid}>
|
||||||
|
|||||||
Reference in New Issue
Block a user