mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
redesign: admin subscription tab — two-level navigation for multi-tariff
Level 1 (subscription list): - Cards with tariff name, status badge, traffic, date, devices - Click to drill into subscription detail - "Создать подписку" with filtered purchased tariffs Level 2 (subscription detail): - Back button to return to list - All actions scoped to selected subscription - No "Сменить тариф" in multi-subscription mode - Panel info, devices, traffic stats per-subscription Single subscription: keeps current behavior (direct detail view)
This commit is contained in:
@@ -336,6 +336,7 @@ export default function AdminUserDetail() {
|
||||
const [subDays, setSubDays] = useState<number | ''>(30);
|
||||
const [selectedTariffId, setSelectedTariffId] = useState<number | null>(null);
|
||||
const [activeSubscriptionId, setActiveSubscriptionId] = useState<number | null>(null);
|
||||
const [subscriptionDetailView, setSubscriptionDetailView] = useState(false);
|
||||
|
||||
// Promo group
|
||||
const [promoGroups, setPromoGroups] = useState<PromoGroup[]>([]);
|
||||
@@ -1379,40 +1380,133 @@ export default function AdminUserDetail() {
|
||||
{/* Subscription Tab */}
|
||||
{activeTab === 'subscription' && (
|
||||
<div className="space-y-4">
|
||||
{/* Subscription selector (when multiple) */}
|
||||
{userSubscriptions.length > 1 && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
<div className="mb-2 text-xs font-medium text-dark-500">
|
||||
{t('admin.users.detail.subscription.selectSubscription', 'Выберите подписку')} (
|
||||
{userSubscriptions.length})
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* Multi-subscription: Level 1 — subscription list */}
|
||||
{userSubscriptions.length > 1 && !subscriptionDetailView && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{userSubscriptions.map((sub) => (
|
||||
<button
|
||||
key={sub.id}
|
||||
onClick={() => setActiveSubscriptionId(sub.id)}
|
||||
className={`rounded-lg px-3 py-1.5 text-sm transition-all ${
|
||||
activeSubscriptionId === sub.id
|
||||
? 'bg-accent-500 text-white'
|
||||
: 'bg-dark-700/50 text-dark-300 hover:bg-dark-700'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setActiveSubscriptionId(sub.id);
|
||||
setSubscriptionDetailView(true);
|
||||
}}
|
||||
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 p-4 text-left transition-all hover:border-dark-600"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${
|
||||
sub.is_active ? 'bg-emerald-400' : 'bg-red-400'
|
||||
}`}
|
||||
/>
|
||||
{sub.tariff_name || `#${sub.id}`}
|
||||
</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-semibold text-dark-100">
|
||||
{sub.tariff_name || `#${sub.id}`}
|
||||
</span>
|
||||
<StatusBadge status={sub.status} />
|
||||
</div>
|
||||
<svg
|
||||
className="h-4 w-4 text-dark-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-4 text-xs text-dark-400">
|
||||
<span>
|
||||
{sub.traffic_used_gb.toFixed(1)} / {sub.traffic_limit_gb}{' '}
|
||||
{t('common.units.gb')}
|
||||
</span>
|
||||
<span>{formatDate(sub.end_date)}</span>
|
||||
<span>
|
||||
{sub.device_limit}{' '}
|
||||
{t('admin.users.detail.subscription.devices', 'устройств')}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create new subscription — at list level */}
|
||||
{hasPermission('users:subscription') && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
<div className="mb-3 text-sm font-medium text-dark-200">
|
||||
{t('admin.users.detail.subscription.createNew', 'Создать подписку')}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
value={selectedTariffId || ''}
|
||||
onChange={(e) =>
|
||||
setSelectedTariffId(e.target.value ? parseInt(e.target.value) : null)
|
||||
}
|
||||
className="input"
|
||||
>
|
||||
<option value="">
|
||||
{t('admin.users.detail.subscription.selectTariff')}
|
||||
</option>
|
||||
{tariffs
|
||||
.filter((tariffItem) => {
|
||||
const purchasedIds = new Set(
|
||||
userSubscriptions
|
||||
.filter((s) => s.is_active || s.status === 'trial')
|
||||
.map((s) => s.tariff_id),
|
||||
);
|
||||
return !purchasedIds.has(tariffItem.id);
|
||||
})
|
||||
.map((tariffItem) => (
|
||||
<option key={tariffItem.id} value={tariffItem.id}>
|
||||
{tariffItem.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
value={subDays}
|
||||
onChange={createNumberInputHandler(setSubDays, 1)}
|
||||
placeholder={t('admin.users.detail.subscription.days')}
|
||||
className="input"
|
||||
min={1}
|
||||
max={3650}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleUpdateSubscription('create')}
|
||||
disabled={actionLoading}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
{actionLoading
|
||||
? t('admin.users.detail.subscription.creating')
|
||||
: t('admin.users.detail.subscription.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedSub ? (
|
||||
{/* Level 2 — subscription detail (or single subscription) */}
|
||||
{(subscriptionDetailView || userSubscriptions.length <= 1) && selectedSub ? (
|
||||
<>
|
||||
{/* Back to list (multi-subscription) */}
|
||||
{subscriptionDetailView && userSubscriptions.length > 1 && (
|
||||
<button
|
||||
onClick={() => setSubscriptionDetailView(false)}
|
||||
className="flex items-center gap-1.5 text-sm text-dark-400 transition-colors hover:text-dark-200"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
{t('admin.users.detail.subscription.backToList', 'Все подписки')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Current subscription */}
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
@@ -1602,9 +1696,11 @@ export default function AdminUserDetail() {
|
||||
<option value="shorten">
|
||||
{t('admin.users.detail.subscription.shorten')}
|
||||
</option>
|
||||
<option value="change_tariff">
|
||||
{t('admin.users.detail.subscription.changeTariff')}
|
||||
</option>
|
||||
{userSubscriptions.length <= 1 && (
|
||||
<option value="change_tariff">
|
||||
{t('admin.users.detail.subscription.changeTariff')}
|
||||
</option>
|
||||
)}
|
||||
<option value="cancel">
|
||||
{t('admin.users.detail.subscription.cancel')}
|
||||
</option>
|
||||
@@ -1661,66 +1757,67 @@ export default function AdminUserDetail() {
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Create new subscription (shown when no subs, or always in multi-tariff) */}
|
||||
{hasPermission('users:subscription') && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
{userSubscriptions.length === 0 && (
|
||||
<div className="mb-4 text-center text-dark-400">
|
||||
{t('admin.users.detail.subscription.noActive')}
|
||||
{/* Create new subscription (shown when no subs, or single-tariff mode) */}
|
||||
{hasPermission('users:subscription') &&
|
||||
!(subscriptionDetailView && userSubscriptions.length > 1) && (
|
||||
<div className="rounded-xl bg-dark-800/50 p-4">
|
||||
{userSubscriptions.length === 0 && (
|
||||
<div className="mb-4 text-center text-dark-400">
|
||||
{t('admin.users.detail.subscription.noActive')}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm font-medium text-dark-200">
|
||||
{t('admin.users.detail.subscription.createNew', 'Создать подписку')}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
value={selectedTariffId || ''}
|
||||
onChange={(e) =>
|
||||
setSelectedTariffId(e.target.value ? parseInt(e.target.value) : null)
|
||||
}
|
||||
className="input"
|
||||
>
|
||||
<option value="">{t('admin.users.detail.subscription.selectTariff')}</option>
|
||||
{tariffs
|
||||
.filter((tariffItem) => {
|
||||
// In multi-tariff: hide tariffs user already has
|
||||
if (userSubscriptions.length > 0) {
|
||||
const purchasedIds = new Set(
|
||||
userSubscriptions
|
||||
.filter((s) => s.is_active || s.status === 'trial')
|
||||
.map((s) => s.tariff_id),
|
||||
);
|
||||
return !purchasedIds.has(tariffItem.id);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((tariffItem) => (
|
||||
<option key={tariffItem.id} value={tariffItem.id}>
|
||||
{tariffItem.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
value={subDays}
|
||||
onChange={createNumberInputHandler(setSubDays, 1)}
|
||||
placeholder={t('admin.users.detail.subscription.days')}
|
||||
className="input"
|
||||
min={1}
|
||||
max={3650}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleUpdateSubscription('create')}
|
||||
disabled={actionLoading}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
{actionLoading
|
||||
? t('admin.users.detail.subscription.creating')
|
||||
: t('admin.users.detail.subscription.create')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3 text-sm font-medium text-dark-200">
|
||||
{t('admin.users.detail.subscription.createNew', 'Создать подписку')}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<select
|
||||
value={selectedTariffId || ''}
|
||||
onChange={(e) =>
|
||||
setSelectedTariffId(e.target.value ? parseInt(e.target.value) : null)
|
||||
}
|
||||
className="input"
|
||||
>
|
||||
<option value="">{t('admin.users.detail.subscription.selectTariff')}</option>
|
||||
{tariffs
|
||||
.filter((tariffItem) => {
|
||||
// In multi-tariff: hide tariffs user already has
|
||||
if (userSubscriptions.length > 0) {
|
||||
const purchasedIds = new Set(
|
||||
userSubscriptions
|
||||
.filter((s) => s.is_active || s.status === 'trial')
|
||||
.map((s) => s.tariff_id),
|
||||
);
|
||||
return !purchasedIds.has(tariffItem.id);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((tariffItem) => (
|
||||
<option key={tariffItem.id} value={tariffItem.id}>
|
||||
{tariffItem.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
value={subDays}
|
||||
onChange={createNumberInputHandler(setSubDays, 1)}
|
||||
placeholder={t('admin.users.detail.subscription.days')}
|
||||
className="input"
|
||||
min={1}
|
||||
max={3650}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleUpdateSubscription('create')}
|
||||
disabled={actionLoading}
|
||||
className="btn-primary w-full"
|
||||
>
|
||||
{actionLoading
|
||||
? t('admin.users.detail.subscription.creating')
|
||||
: t('admin.users.detail.subscription.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Panel Info */}
|
||||
{panelInfoLoading ? (
|
||||
|
||||
Reference in New Issue
Block a user