feat: add subscription selector to admin sync tab for multi-tariff

- Add subscription dropdown to sync tab when user has 2+ subscriptions
- Pass subscription_id to getSyncStatus, syncFromPanel, syncToPanel
- loadSyncStatus re-fetches when selected subscription changes
- Show tariff name in UUID info section from API response
- Add selectSubscription i18n keys to all 4 locales
This commit is contained in:
Fringg
2026-03-28 19:50:10 +03:00
parent 8ba5ceae10
commit aa989a6ade
6 changed files with 62 additions and 11 deletions

View File

@@ -251,6 +251,8 @@ export interface PanelSyncStatusResponse {
user_id: number;
telegram_id: number;
remnawave_uuid: string | null;
subscription_id: number | null;
subscription_tariff_name: string | null;
last_sync: string | null;
bot_subscription_status: string | null;
bot_subscription_end_date: string | null;
@@ -561,8 +563,12 @@ export const adminUsersApi = {
},
// Sync status
getSyncStatus: async (userId: number): Promise<PanelSyncStatusResponse> => {
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`);
getSyncStatus: async (
userId: number,
subscriptionId?: number,
): Promise<PanelSyncStatusResponse> => {
const params = subscriptionId != null ? { subscription_id: subscriptionId } : undefined;
const response = await apiClient.get(`/cabinet/admin/users/${userId}/sync/status`, { params });
return response.data;
},
@@ -570,8 +576,12 @@ export const adminUsersApi = {
syncFromPanel: async (
userId: number,
data: SyncFromPanelRequest = {},
subscriptionId?: number,
): Promise<SyncFromPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data);
const params = subscriptionId != null ? { subscription_id: subscriptionId } : undefined;
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/from-panel`, data, {
params,
});
return response.data;
},
@@ -579,8 +589,12 @@ export const adminUsersApi = {
syncToPanel: async (
userId: number,
data: SyncToPanelRequest = {},
subscriptionId?: number,
): Promise<SyncToPanelResponse> => {
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data);
const params = subscriptionId != null ? { subscription_id: subscriptionId } : undefined;
const response = await apiClient.post(`/cabinet/admin/users/${userId}/sync/to-panel`, data, {
params,
});
return response.data;
},

View File

@@ -2950,6 +2950,7 @@
"recentTransactions": "Recent transactions"
},
"sync": {
"selectSubscription": "Select subscription",
"hasDifferences": "Differences found",
"synced": "Synchronized",
"bot": "Bot",

View File

@@ -2578,6 +2578,7 @@
"recentTransactions": "تراکنش‌های اخیر"
},
"sync": {
"selectSubscription": "انتخاب اشتراک",
"hasDifferences": "اختلاف وجود دارد",
"synced": "همگام‌سازی شده",
"bot": "ربات",

View File

@@ -3471,6 +3471,7 @@
"recentTransactions": "Последние транзакции"
},
"sync": {
"selectSubscription": "Выберите подписку",
"hasDifferences": "Есть расхождения",
"synced": "Синхронизировано",
"bot": "Бот",

View File

@@ -2577,6 +2577,7 @@
"recentTransactions": "最近交易"
},
"sync": {
"selectSubscription": "选择订阅",
"hasDifferences": "存在差异",
"synced": "已同步",
"bot": "机器人",

View File

@@ -386,12 +386,12 @@ export default function AdminUserDetail() {
const loadSyncStatus = useCallback(async () => {
if (!userId) return;
try {
const data = await adminUsersApi.getSyncStatus(userId);
const data = await adminUsersApi.getSyncStatus(userId, activeSubscriptionId ?? undefined);
setSyncStatus(data);
} catch (error) {
console.error('Failed to load sync status:', error);
}
}, [userId]);
}, [userId, activeSubscriptionId]);
const loadTariffs = useCallback(async () => {
if (!userId) return;
@@ -656,10 +656,14 @@ export default function AdminUserDetail() {
if (!userId) return;
setActionLoading(true);
try {
await adminUsersApi.syncFromPanel(userId, {
await adminUsersApi.syncFromPanel(
userId,
{
update_subscription: true,
update_traffic: true,
});
},
activeSubscriptionId ?? undefined,
);
await loadUser();
await loadSyncStatus();
} catch (error) {
@@ -673,7 +677,11 @@ export default function AdminUserDetail() {
if (!userId) return;
setActionLoading(true);
try {
await adminUsersApi.syncToPanel(userId, { create_if_missing: true });
await adminUsersApi.syncToPanel(
userId,
{ create_if_missing: true },
activeSubscriptionId ?? undefined,
);
await loadUser();
await loadSyncStatus();
} catch (error) {
@@ -2309,6 +2317,26 @@ export default function AdminUserDetail() {
{/* Sync Tab */}
{activeTab === 'sync' && (
<div className="space-y-4">
{/* Subscription selector for multi-tariff */}
{userSubscriptions.length > 1 && (
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="mb-2 text-sm text-dark-400">
{t('admin.users.detail.sync.selectSubscription')}
</div>
<select
value={activeSubscriptionId || ''}
onChange={(e) => setActiveSubscriptionId(Number(e.target.value) || null)}
className="w-full rounded-lg border border-dark-600 bg-dark-700 px-3 py-2 text-sm text-dark-100"
>
{userSubscriptions.map((sub) => (
<option key={sub.id} value={sub.id}>
{sub.tariff_name || `#${sub.id}`} {sub.status}
</option>
))}
</select>
</div>
)}
{/* Sync status */}
{syncStatus && (
<div
@@ -2431,6 +2459,11 @@ export default function AdminUserDetail() {
{/* UUID info */}
<div className="rounded-xl bg-dark-800/50 p-4">
{syncStatus?.subscription_tariff_name && (
<div className="mb-2 text-xs text-dark-500">
{syncStatus.subscription_tariff_name}
</div>
)}
<div className="mb-1 text-sm text-dark-400">Remnawave UUID</div>
<div className="break-all font-mono text-sm text-dark-100">
{syncStatus?.remnawave_uuid ||