mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: subscription selector in VPN connection block for multi-tariff
- Show VPN connection block even when panelInfo not yet loaded - Add subscription dropdown when user has multiple subscriptions - Show spinner while loading, "no data" when panel returns not found - Changing subscription in VPN block reloads panel info for that sub
This commit is contained in:
@@ -3081,6 +3081,7 @@
|
|||||||
"lastConnection": "Last connection",
|
"lastConnection": "Last connection",
|
||||||
"firstConnection": "First connection",
|
"firstConnection": "First connection",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
|
"noVpnData": "No connection data available",
|
||||||
"requestHistory": "Subscription request history",
|
"requestHistory": "Subscription request history",
|
||||||
"requestHistoryTotal": "Total requests",
|
"requestHistoryTotal": "Total requests",
|
||||||
"requestAt": "Date",
|
"requestAt": "Date",
|
||||||
|
|||||||
@@ -3488,6 +3488,7 @@
|
|||||||
"lastConnection": "Последнее подключение",
|
"lastConnection": "Последнее подключение",
|
||||||
"firstConnection": "Первое подключение",
|
"firstConnection": "Первое подключение",
|
||||||
"online": "Онлайн",
|
"online": "Онлайн",
|
||||||
|
"noVpnData": "Нет данных о подключении",
|
||||||
"requestHistory": "История запросов подписки",
|
"requestHistory": "История запросов подписки",
|
||||||
"requestHistoryTotal": "Всего запросов",
|
"requestHistoryTotal": "Всего запросов",
|
||||||
"requestAt": "Дата",
|
"requestAt": "Дата",
|
||||||
|
|||||||
@@ -1376,83 +1376,108 @@ export default function AdminUserDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* VPN Connection Info */}
|
{/* VPN Connection Info */}
|
||||||
{panelInfo && panelInfo.found && (
|
{(panelInfo || userSubscriptions.length > 0) && (
|
||||||
<div className="rounded-xl border border-dark-700/30 bg-dark-800/50 p-4">
|
<div className="rounded-xl border border-dark-700/30 bg-dark-800/50 p-4">
|
||||||
<div className="mb-3 text-sm font-medium text-dark-200">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
{t('admin.users.detail.vpnConnection')}
|
<span className="text-sm font-medium text-dark-200">
|
||||||
</div>
|
{t('admin.users.detail.vpnConnection')}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
</span>
|
||||||
<div className="rounded-lg bg-dark-700/30 p-3">
|
{userSubscriptions.length > 1 && (
|
||||||
<div className="mb-1 text-xs text-dark-500">
|
<select
|
||||||
{t('admin.users.detail.lastConnection')}
|
value={activeSubscriptionId ?? ''}
|
||||||
</div>
|
onChange={(e) => {
|
||||||
<div className="flex items-center gap-2">
|
const subId = Number(e.target.value);
|
||||||
{panelInfo.online_at &&
|
setActiveSubscriptionId(subId);
|
||||||
(() => {
|
}}
|
||||||
const onlineDate = new Date(panelInfo.online_at);
|
className="rounded-lg border border-dark-600 bg-dark-700 px-3 py-1.5 text-xs text-dark-200"
|
||||||
const isRecent = Date.now() - onlineDate.getTime() < 5 * 60 * 1000;
|
>
|
||||||
return (
|
{userSubscriptions.map((s) => (
|
||||||
<>
|
<option key={s.id} value={s.id}>
|
||||||
<span
|
{s.tariff_name || `#${s.id}`} — {s.status}
|
||||||
className={`inline-block h-2 w-2 shrink-0 rounded-full ${isRecent ? 'bg-success-400 shadow-[0_0_6px_rgba(74,222,128,0.5)]' : 'bg-dark-500'}`}
|
</option>
|
||||||
title={isRecent ? t('admin.users.detail.online') : ''}
|
))}
|
||||||
/>
|
</select>
|
||||||
<span
|
|
||||||
className={`text-sm ${isRecent ? 'font-medium text-success-400' : 'text-dark-100'}`}
|
|
||||||
>
|
|
||||||
{isRecent
|
|
||||||
? t('admin.users.detail.online')
|
|
||||||
: formatDate(panelInfo.online_at)}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
{!panelInfo.online_at && <span className="text-sm text-dark-100">-</span>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg bg-dark-700/30 p-3">
|
|
||||||
<div className="mb-1 text-xs text-dark-500">
|
|
||||||
{t('admin.users.detail.firstConnection')}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-dark-100">
|
|
||||||
{panelInfo.first_connected_at
|
|
||||||
? new Date(panelInfo.first_connected_at).toLocaleDateString(locale, {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
})
|
|
||||||
: '-'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{panelInfo.last_connected_node_name && (
|
|
||||||
<div className="col-span-2 rounded-lg bg-dark-700/30 p-3">
|
|
||||||
<div className="mb-1 text-xs text-dark-500">
|
|
||||||
{t('admin.users.detail.lastNode')}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-dark-100">
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 shrink-0 text-dark-400"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
{panelInfo.last_connected_node_name}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{panelInfoLoading && !panelInfo?.found && (
|
||||||
)}
|
<div className="flex items-center justify-center py-4">
|
||||||
{panelInfoLoading && !panelInfo && (
|
<div className="h-5 w-5 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||||
<div className="flex justify-center rounded-xl bg-dark-800/50 py-4">
|
</div>
|
||||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
)}
|
||||||
|
{panelInfo?.found && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="rounded-lg bg-dark-700/30 p-3">
|
||||||
|
<div className="mb-1 text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.lastConnection')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{panelInfo.online_at &&
|
||||||
|
(() => {
|
||||||
|
const onlineDate = new Date(panelInfo.online_at);
|
||||||
|
const isRecent = Date.now() - onlineDate.getTime() < 5 * 60 * 1000;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-2 w-2 shrink-0 rounded-full ${isRecent ? 'bg-success-400 shadow-[0_0_6px_rgba(74,222,128,0.5)]' : 'bg-dark-500'}`}
|
||||||
|
title={isRecent ? t('admin.users.detail.online') : ''}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${isRecent ? 'font-medium text-success-400' : 'text-dark-100'}`}
|
||||||
|
>
|
||||||
|
{isRecent
|
||||||
|
? t('admin.users.detail.online')
|
||||||
|
: formatDate(panelInfo.online_at)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{!panelInfo.online_at && <span className="text-sm text-dark-100">-</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg bg-dark-700/30 p-3">
|
||||||
|
<div className="mb-1 text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.firstConnection')}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-dark-100">
|
||||||
|
{panelInfo.first_connected_at
|
||||||
|
? new Date(panelInfo.first_connected_at).toLocaleDateString(locale, {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{panelInfo.last_connected_node_name && (
|
||||||
|
<div className="col-span-2 rounded-lg bg-dark-700/30 p-3">
|
||||||
|
<div className="mb-1 text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.lastNode')}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-dark-100">
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 shrink-0 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{panelInfo.last_connected_node_name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!panelInfoLoading && !panelInfo?.found && userSubscriptions.length > 0 && (
|
||||||
|
<div className="py-2 text-center text-xs text-dark-500">
|
||||||
|
{t('admin.users.detail.noVpnData')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user