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

View File

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

View File

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

View File

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

View File

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

View File

@@ -386,12 +386,12 @@ export default function AdminUserDetail() {
const loadSyncStatus = useCallback(async () => { const loadSyncStatus = useCallback(async () => {
if (!userId) return; if (!userId) return;
try { try {
const data = await adminUsersApi.getSyncStatus(userId); const data = await adminUsersApi.getSyncStatus(userId, activeSubscriptionId ?? undefined);
setSyncStatus(data); setSyncStatus(data);
} catch (error) { } catch (error) {
console.error('Failed to load sync status:', error); console.error('Failed to load sync status:', error);
} }
}, [userId]); }, [userId, activeSubscriptionId]);
const loadTariffs = useCallback(async () => { const loadTariffs = useCallback(async () => {
if (!userId) return; if (!userId) return;
@@ -656,10 +656,14 @@ export default function AdminUserDetail() {
if (!userId) return; if (!userId) return;
setActionLoading(true); setActionLoading(true);
try { try {
await adminUsersApi.syncFromPanel(userId, { await adminUsersApi.syncFromPanel(
update_subscription: true, userId,
update_traffic: true, {
}); update_subscription: true,
update_traffic: true,
},
activeSubscriptionId ?? undefined,
);
await loadUser(); await loadUser();
await loadSyncStatus(); await loadSyncStatus();
} catch (error) { } catch (error) {
@@ -673,7 +677,11 @@ export default function AdminUserDetail() {
if (!userId) return; if (!userId) return;
setActionLoading(true); setActionLoading(true);
try { try {
await adminUsersApi.syncToPanel(userId, { create_if_missing: true }); await adminUsersApi.syncToPanel(
userId,
{ create_if_missing: true },
activeSubscriptionId ?? undefined,
);
await loadUser(); await loadUser();
await loadSyncStatus(); await loadSyncStatus();
} catch (error) { } catch (error) {
@@ -2309,6 +2317,26 @@ export default function AdminUserDetail() {
{/* Sync Tab */} {/* Sync Tab */}
{activeTab === 'sync' && ( {activeTab === 'sync' && (
<div className="space-y-4"> <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 */} {/* Sync status */}
{syncStatus && ( {syncStatus && (
<div <div
@@ -2431,6 +2459,11 @@ export default function AdminUserDetail() {
{/* UUID info */} {/* UUID info */}
<div className="rounded-xl bg-dark-800/50 p-4"> <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="mb-1 text-sm text-dark-400">Remnawave UUID</div>
<div className="break-all font-mono text-sm text-dark-100"> <div className="break-all font-mono text-sm text-dark-100">
{syncStatus?.remnawave_uuid || {syncStatus?.remnawave_uuid ||