streamio / Backend /fastapi /templates /subscriptions_manage.html
Karan Adhikari
UI: redesign templates
ef88983 unverified
Raw History Blame Contribute Delete
37.6 kB
{% extends "base.html" %}
{% block title %}Subscription Management - Telegram Stremio{% endblock %}
{% block content %}
<style>
.subs-shell {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.page-hero,
.section-panel,
.table-shell,
.mobile-user-card,
.empty-state,
.modal-glass {
background: color-mix(in srgb, var(--card) 50%, transparent);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.22);
}
.page-hero,
.section-panel,
.table-shell,
.empty-state {
border-radius: 1.5rem;
}
.hero-glow {
position: relative;
overflow: hidden;
isolation: isolate;
}
.hero-glow::before {
content: "";
position: absolute;
inset: -30% auto auto -10%;
width: 16rem;
height: 16rem;
border-radius: 9999px;
background: color-mix(in srgb, var(--primary) 18%, transparent);
filter: blur(50px);
z-index: -1;
pointer-events: none;
}
.hero-glow::after {
content: "";
position: absolute;
inset: auto -5% -35% auto;
width: 14rem;
height: 14rem;
border-radius: 9999px;
background: color-mix(in srgb, var(--accent) 16%, transparent);
filter: blur(50px);
z-index: -1;
pointer-events: none;
}
.section-title {
color: var(--text);
}
.section-muted {
color: var(--text-sec);
}
.glass-btn {
border: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
background: color-mix(in srgb, var(--card) 92%, var(--bg));
color: var(--text);
transition: all 0.25s ease;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
}
.glass-btn:hover {
transform: translateY(-1px);
border-color: color-mix(in srgb, var(--primary) 35%, var(--border));
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.18);
}
.glass-btn-primary {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--primary) 88%, white 8%),
color-mix(in srgb, var(--accent) 82%, var(--primary) 18%)
);
color: white;
border: 1px solid color-mix(in srgb, var(--primary) 50%, transparent);
box-shadow: 0 10px 26px color-mix(in srgb, var(--primary) 22%, transparent);
}
.glass-btn-primary:hover {
filter: brightness(1.05);
box-shadow: 0 14px 34px color-mix(in srgb, var(--primary) 28%, transparent);
}
.glass-btn-danger {
background: linear-gradient(135deg, #ef4444, #dc2626);
color: #ffffff;
border-color: transparent;
}
.glass-btn-warning {
background: linear-gradient(135deg, #f59e0b, #d97706);
color: #ffffff;
border-color: transparent;
}
.glass-btn-success {
background: linear-gradient(135deg, #22c55e, #16a34a);
color: #ffffff;
border-color: transparent;
}
.glass-input,
.glass-select {
width: 100%;
border-radius: 1rem;
border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
background: color-mix(in srgb, var(--bg) 68%, transparent);
color: var(--text);
padding: 0.85rem 1rem;
outline: none;
transition: all 0.2s ease;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
}
.glass-input::placeholder {
color: color-mix(in srgb, var(--text-sec) 85%, transparent);
}
.glass-input:focus,
.glass-select:focus {
border-color: color-mix(in srgb, var(--primary) 65%, var(--border));
box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 12%, transparent);
}
.plan-card {
position: relative;
overflow: hidden;
border-radius: 1.5rem;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--card) 96%, var(--bg)),
color-mix(in srgb, var(--card) 88%, var(--bg))
);
border: 1px solid color-mix(in srgb, var(--border) 90%, transparent);
box-shadow: 0 12px 30px rgba(0,0,0,0.18);
transition: transform 0.28s ease, border-color 0.28s ease, box-shadow 0.28s ease;
}
.plan-card::before {
content: "";
position: absolute;
inset: -25% auto auto -5%;
width: 8rem;
height: 8rem;
border-radius: 9999px;
background: color-mix(in srgb, var(--primary) 18%, transparent);
filter: blur(36px);
pointer-events: none;
}
.plan-card:hover {
transform: translateY(-4px);
border-color: color-mix(in srgb, var(--primary) 30%, var(--border));
box-shadow: 0 18px 42px rgba(0,0,0,0.24);
}
.plan-actions {
opacity: 0;
transform: translateY(-4px);
transition: all 0.22s ease;
}
.plan-card:hover .plan-actions {
opacity: 1;
transform: translateY(0);
}
.metric-badge {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.45rem 0.8rem;
border-radius: 9999px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
border: 1px solid transparent;
}
.badge-active {
background: color-mix(in srgb, #22c55e 16%, transparent);
color: #86efac;
border-color: color-mix(in srgb, #22c55e 28%, transparent);
}
.badge-expired {
background: color-mix(in srgb, #ef4444 14%, transparent);
color: #fca5a5;
border-color: color-mix(in srgb, #ef4444 26%, transparent);
}
.table-shell {
overflow: hidden;
}
.subs-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.subs-table thead th {
background: color-mix(in srgb, var(--bg) 65%, transparent);
color: var(--text-sec);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid color-mix(in srgb, var(--border) 92%, transparent);
}
.subs-table tbody tr {
transition: background 0.22s ease;
}
.subs-table tbody tr:hover {
background: color-mix(in srgb, var(--primary) 6%, transparent);
}
.subs-table td {
border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
color: var(--text);
}
.subs-table tbody tr:last-child td {
border-bottom: none;
}
.action-chip {
border-radius: 9999px;
padding: 0.45rem 0.8rem;
font-size: 0.72rem;
font-weight: 700;
border: 1px solid transparent;
transition: all 0.2s ease;
white-space: nowrap;
}
.action-chip:hover {
transform: translateY(-1px);
}
.mobile-user-card {
border-radius: 1.35rem;
padding: 1rem;
}
.mobile-user-card + .mobile-user-card {
margin-top: 0.9rem;
}
.mobile-grid {
display: none;
}
.pagination-shell {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.pagination-text {
color: var(--text-sec);
}
.pagination-btn {
min-width: 92px;
}
.modal-backdrop {
background: rgba(4, 8, 18, 0.58);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.modal-glass {
border-radius: 1.6rem;
padding: 1.4rem;
border: 1px solid color-mix(in srgb, var(--border) 95%, transparent);
width: min(92vw, 30rem);
}
.modal-field label {
color: var(--text-sec);
font-size: 0.82rem;
font-weight: 700;
margin-bottom: 0.55rem;
display: block;
}
.text-soft {
color: var(--text-sec);
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
@media (max-width: 1024px) {
.subs-table th,
.subs-table td {
padding-left: 1rem;
padding-right: 1rem;
}
}
@media (max-width: 767px) {
.desktop-table-wrap {
display: none;
}
.mobile-grid {
display: block;
}
.page-hero {
padding: 1.25rem;
border-radius: 1.35rem;
}
.section-panel {
padding: 1rem;
border-radius: 1.35rem;
}
.pagination-shell {
flex-direction: column;
align-items: stretch;
}
.pagination-shell .controls {
width: 100%;
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 0.6rem;
align-items: center;
}
.pagination-btn {
width: 100%;
}
.plan-actions {
opacity: 1;
transform: none;
}
}
</style>
<div class="subs-shell py-2 sm:py-4">
<!-- Hero -->
<section class="page-hero hero-glow p-5 sm:p-6 lg:p-8">
<div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
<div>
<div class="inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs font-bold tracking-[0.18em] uppercase mb-3"
style="border-color: color-mix(in srgb, var(--primary) 28%, var(--border)); color: var(--text-sec); background: color-mix(in srgb, var(--primary) 10%, transparent);">
<i class="fas fa-layer-group text-[11px]" style="color: var(--primary);"></i>
Subscription Control Center
</div>
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-extrabold tracking-tight section-title">
Subscription Management
</h1>
<p class="mt-2 text-sm sm:text-base section-muted max-w-2xl">
Create plans, manage users, and monitor active or expired subscriptions.
</p>
</div>
<div class="flex flex-wrap gap-3">
<button onclick="openPlanModal()" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold inline-flex items-center gap-2">
<i class="fas fa-plus"></i>
Add Plan
</button>
<button onclick="fetchSubscribers()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold inline-flex items-center gap-2">
<i class="fas fa-rotate-right"></i>
Refresh Users
</button>
</div>
</div>
</section>
<!-- Plans -->
<section class="section-panel p-4 sm:p-5 lg:p-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-5">
<div>
<h2 class="text-lg sm:text-xl font-bold section-title">Subscription Plans</h2>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 sm:gap-5" id="plans-container">
<div class="empty-state col-span-full text-center px-6 py-10 text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading plans...
</div>
</div>
</section>
<!-- Users -->
<section class="section-panel p-4 sm:p-5 lg:p-6">
<div class="flex flex-col lg:flex-row lg:items-end lg:justify-between gap-4 mb-5">
<div>
<h2 class="text-lg sm:text-xl font-bold section-title">User Subscriptions</h2>
<p class="text-sm section-muted mt-1">Search, filter, paginate, and manage subscription access.</p>
</div>
<div class="w-full lg:w-auto grid grid-cols-1 sm:grid-cols-3 gap-3">
<input
id="sub-search"
type="text"
placeholder="Search by name, username or ID..."
class="glass-input"
oninput="subPage=1; renderSubPage()"
>
<select id="sub-filter" class="glass-select" onchange="subPage=1; renderSubPage()">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="expired">Expired</option>
</select>
<select id="sub-page-size" class="glass-select" onchange="subPage=1; renderSubPage()">
<option value="10">10 / page</option>
<option value="25">25 / page</option>
<option value="50">50 / page</option>
</select>
</div>
</div>
<!-- Desktop table -->
<div class="desktop-table-wrap table-shell">
<div class="overflow-x-auto no-scrollbar">
<table class="subs-table min-w-[780px] text-sm">
<thead>
<tr>
<th class="px-6 py-4 text-left font-semibold">User Details</th>
<th class="px-6 py-4 text-center font-semibold">Status</th>
<th class="px-6 py-4 text-center font-semibold">Expiry</th>
<th class="px-6 py-4 text-right font-semibold">Actions</th>
</tr>
</thead>
<tbody id="subscribers-tbody">
<tr>
<td colspan="4" class="px-6 py-8 text-center text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading...
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Mobile cards -->
<div id="subscribers-mobile" class="mobile-grid mt-1">
<div class="empty-state text-center px-4 py-8 text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading...
</div>
</div>
<!-- Pagination -->
<div class="pagination-shell mt-5" id="sub-pagination">
<div id="sub-page-info" class="pagination-text text-sm"></div>
<div class="controls">
<button id="sub-btn-prev" onclick="subChangePage(-1)"
class="glass-btn pagination-btn rounded-xl px-4 py-2.5 text-sm font-semibold">
‹ Prev
</button>
<span id="sub-page-num" class="text-sm font-bold text-center section-title px-2"></span>
<button id="sub-btn-next" onclick="subChangePage(1)"
class="glass-btn pagination-btn rounded-xl px-4 py-2.5 text-sm font-semibold">
Next ›
</button>
</div>
</div>
</section>
</div>
<!-- Plan Modal -->
<div id="plan-modal" class="fixed inset-0 hidden z-50 modal-backdrop items-center justify-center p-4">
<div class="modal-glass">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h3 class="text-xl font-bold section-title" id="plan-modal-title">Add New Plan</h3>
<p class="text-sm text-soft mt-1">Create or update a subscription plan.</p>
</div>
<button onclick="closePlanModal()" class="glass-btn rounded-full w-10 h-10 flex items-center justify-center">
<i class="fas fa-xmark"></i>
</button>
</div>
<form id="plan-form" onsubmit="submitPlanForm(event)" class="space-y-4">
<input type="hidden" id="plan-id">
<div class="modal-field">
<label for="plan-days">Duration (Days)</label>
<input type="number" id="plan-days" class="glass-input" required min="1">
</div>
<div class="modal-field">
<label for="plan-price">Price (₹)</label>
<input type="number" id="plan-price" class="glass-input" required min="0" step="0.01">
</div>
<div class="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-2">
<button type="button" onclick="closePlanModal()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold">
Cancel
</button>
<button type="submit" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold">
Save Plan
</button>
</div>
</form>
</div>
</div>
<!-- User Action Modal -->
<div id="user-modal" class="fixed inset-0 hidden z-50 modal-backdrop items-center justify-center p-4">
<div class="modal-glass">
<div class="flex items-start justify-between gap-4 mb-5">
<div>
<h3 class="text-xl font-bold section-title" id="user-modal-title">Manage User</h3>
<div class="text-sm text-soft mt-1" id="user-modal-desc"></div>
</div>
<button onclick="closeUserModal()" class="glass-btn rounded-full w-10 h-10 flex items-center justify-center">
<i class="fas fa-xmark"></i>
</button>
</div>
<form id="user-form" onsubmit="submitUserForm(event)" class="space-y-4">
<input type="hidden" id="manage-user-id">
<input type="hidden" id="manage-action">
<div class="modal-field" id="days-input-group">
<label for="manage-days">Days to <span id="days-action-label"></span></label>
<input type="number" id="manage-days" class="glass-input" min="1" value="30">
</div>
<div class="flex flex-col-reverse sm:flex-row justify-end gap-3 pt-2">
<button type="button" onclick="closeUserModal()" class="glass-btn rounded-2xl px-5 py-3 text-sm font-bold">
Cancel
</button>
<button type="submit" class="glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold" id="btn-user-save">
Confirm
</button>
</div>
</form>
</div>
</div>
<script>
let allUsers = [];
let subPage = 1;
function formatExpiry(expiryValue) {
if (!expiryValue) return 'N/A';
const d = new Date(expiryValue);
return d.toLocaleDateString('en-IN', {
timeZone: 'Asia/Kolkata',
day: '2-digit',
month: 'short',
year: 'numeric'
}) + ' ' + d.toLocaleTimeString('en-IN', {
timeZone: 'Asia/Kolkata',
hour: '2-digit',
minute: '2-digit'
});
}
function getUserDisplayName(u) {
let name = u.first_name || '';
if (u.username) name += (name ? ' ' : '') + `(@${u.username})`;
if (!name) name = `User ${u._id}`;
return name;
}
function getStatusBadge(status) {
return status === 'active'
? '<span class="metric-badge badge-active"><i class="fas fa-circle text-[8px]"></i> Active</span>'
: '<span class="metric-badge badge-expired"><i class="fas fa-circle text-[8px]"></i> Expired</span>';
}
async function fetchPlans() {
const container = document.getElementById('plans-container');
container.innerHTML = `
<div class="empty-state col-span-full text-center px-6 py-10 text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading plans...
</div>
`;
try {
const res = await fetch('/api/admin/subscriptions/plans');
if (!res.ok) throw new Error('Failed to fetch plans');
const data = await res.json();
const plans = data.data || data.plans || [];
if (!plans.length) {
container.innerHTML = `
<div class="empty-state col-span-full text-center px-6 py-10 text-soft">
<i class="fas fa-box-open text-2xl mb-3"></i>
<div class="font-semibold mb-1">No plans found</div>
<div class="text-sm">Create your first subscription plan.</div>
</div>
`;
return;
}
container.innerHTML = plans.map(p => `
<div class="plan-card p-5 sm:p-6">
<div class="plan-actions absolute right-4 top-4 flex gap-2">
<button onclick="openPlanModal('${p._id}', ${p.days}, ${p.price})"
class="glass-btn rounded-full w-9 h-9 flex items-center justify-center" title="Edit plan">
<i class="fas fa-pen text-sm"></i>
</button>
<button onclick="deletePlan('${p._id}')"
class="glass-btn glass-btn-danger rounded-full w-9 h-9 flex items-center justify-center" title="Delete plan">
<i class="fas fa-trash text-sm"></i>
</button>
</div>
<div class="flex items-start gap-4">
<div class="w-14 h-14 rounded-2xl flex items-center justify-center shrink-0"
style="background: color-mix(in srgb, var(--primary) 16%, transparent); border: 1px solid color-mix(in srgb, var(--primary) 24%, transparent);">
<i class="fas fa-calendar-alt text-xl" style="color: var(--primary);"></i>
</div>
<div class="min-w-0">
<div class="text-sm font-semibold section-muted">${p.days} Days Plan</div>
<div class="mt-1 text-3xl font-extrabold section-title tracking-tight">₹${p.price}</div>
<div class="mt-3 inline-flex items-center gap-2 text-xs font-bold rounded-full px-3 py-1"
style="background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--text-sec); border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);">
<i class="fas fa-bolt" style="color: var(--accent);"></i>
Premium Access
</div>
</div>
</div>
</div>
`).join('');
} catch (err) {
console.error(err);
container.innerHTML = `
<div class="empty-state col-span-full text-center px-6 py-10 text-soft">
<i class="fas fa-triangle-exclamation mr-2"></i> Failed to load plans.
</div>
`;
}
}
function openPlanModal(id = '', days = '', price = '') {
document.getElementById('plan-id').value = id;
document.getElementById('plan-days').value = days;
document.getElementById('plan-price').value = price;
document.getElementById('plan-modal-title').innerText = id ? 'Edit Plan' : 'Add New Plan';
const modal = document.getElementById('plan-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.body.classList.add('overflow-hidden');
}
function closePlanModal() {
const modal = document.getElementById('plan-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
document.body.classList.remove('overflow-hidden');
}
async function submitPlanForm(e) {
e.preventDefault();
const id = document.getElementById('plan-id').value;
const payload = {
days: parseInt(document.getElementById('plan-days').value),
price: parseFloat(document.getElementById('plan-price').value)
};
try {
const res = await fetch(id ? `/api/admin/subscriptions/plans/${id}` : '/api/admin/subscriptions/plans', {
method: id ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error('Failed to save plan');
closePlanModal();
fetchPlans();
} catch (err) {
console.error(err);
showToast('Failed to save plan.', 'error', 'Save Failed');
}
}
async function deletePlan(id) {
if (!confirm('Delete this plan?')) return;
try {
const res = await fetch(`/api/admin/subscriptions/plans/${id}`, { method: 'DELETE' });
if (!res.ok) throw new Error('Failed');
fetchPlans();
} catch (err) {
console.error(err);
showToast('Failed to delete plan.', 'error', 'Delete Failed');
}
}
function getSubFiltered() {
const q = document.getElementById('sub-search').value.toLowerCase().trim();
const status = document.getElementById('sub-filter').value;
return allUsers.filter(u => {
const name = ((u.first_name || '') + ' ' + (u.username || '') + ' ' + (u._id || '')).toLowerCase();
const matchQ = !q || name.includes(q);
const matchStatus = status === 'all' || u.subscription_status === status;
return matchQ && matchStatus;
});
}
function renderDesktopRows(slice) {
const tbody = document.getElementById('subscribers-tbody');
if (!slice.length) {
tbody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-10 text-center text-soft">
No users match your search.
</td>
</tr>
`;
return;
}
tbody.innerHTML = slice.map(u => {
const expiry = formatExpiry(u.subscription_expiry);
const name = getUserDisplayName(u);
const safeName = name.replace(/'/g, "\\'");
const statusBadge = getStatusBadge(u.subscription_status);
return `
<tr>
<td class="px-6 py-4">
<div class="font-semibold section-title">${name}</div>
<div class="text-xs text-soft mt-1">ID: ${u._id}</div>
</td>
<td class="px-6 py-4 text-center">${statusBadge}</td>
<td class="px-6 py-4 text-center text-sm section-title">${expiry}</td>
<td class="px-6 py-4">
<div class="flex justify-end gap-2 flex-wrap">
<button onclick="openUserModal(${u._id}, '${safeName}', 'extend')"
class="action-chip glass-btn-success">
Extend
</button>
<button onclick="openUserModal(${u._id}, '${safeName}', 'reduce')"
class="action-chip glass-btn-warning">
Reduce
</button>
<button onclick="openUserModal(${u._id}, '${safeName}', 'delete')"
class="action-chip glass-btn-danger">
Revoke
</button>
</div>
</td>
</tr>
`;
}).join('');
}
function renderMobileCards(slice) {
const mobileWrap = document.getElementById('subscribers-mobile');
if (!slice.length) {
mobileWrap.innerHTML = `
<div class="empty-state text-center px-4 py-8 text-soft">
No users match your search.
</div>
`;
return;
}
mobileWrap.innerHTML = slice.map(u => {
const expiry = formatExpiry(u.subscription_expiry);
const name = getUserDisplayName(u);
const safeName = name.replace(/'/g, "\\'");
const statusBadge = getStatusBadge(u.subscription_status);
return `
<div class="mobile-user-card">
<div class="flex items-start justify-between gap-3 mb-3">
<div class="min-w-0">
<div class="font-semibold section-title break-words">${name}</div>
<div class="text-xs text-soft mt-1 break-all">ID: ${u._id}</div>
</div>
<div class="shrink-0">${statusBadge}</div>
</div>
<div class="grid grid-cols-1 gap-3 mb-4">
<div class="rounded-2xl px-4 py-3"
style="background: color-mix(in srgb, var(--bg) 55%, transparent); border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);">
<div class="text-[11px] uppercase tracking-[0.14em] text-soft font-bold mb-1">Expiry</div>
<div class="text-sm font-semibold section-title">${expiry}</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2">
<button onclick="openUserModal(${u._id}, '${safeName}', 'extend')"
class="action-chip glass-btn-success w-full">
Extend
</button>
<button onclick="openUserModal(${u._id}, '${safeName}', 'reduce')"
class="action-chip glass-btn-warning w-full">
Reduce
</button>
<button onclick="openUserModal(${u._id}, '${safeName}', 'delete')"
class="action-chip glass-btn-danger w-full">
Revoke
</button>
</div>
</div>
`;
}).join('');
}
function renderSubPage() {
const filtered = getSubFiltered();
const pageSize = parseInt(document.getElementById('sub-page-size').value);
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
if (subPage > totalPages) subPage = totalPages;
const start = filtered.length ? (subPage - 1) * pageSize : 0;
const slice = filtered.slice(start, start + pageSize);
document.getElementById('sub-page-info').textContent =
filtered.length
? `Showing ${start + 1}–${Math.min(start + pageSize, filtered.length)} of ${filtered.length}`
: 'Showing 0 of 0';
document.getElementById('sub-page-num').textContent = `Page ${subPage} / ${totalPages}`;
const prevBtn = document.getElementById('sub-btn-prev');
const nextBtn = document.getElementById('sub-btn-next');
prevBtn.disabled = subPage <= 1;
nextBtn.disabled = subPage >= totalPages;
prevBtn.style.opacity = subPage <= 1 ? '0.45' : '1';
nextBtn.style.opacity = subPage >= totalPages ? '0.45' : '1';
renderDesktopRows(slice);
renderMobileCards(slice);
}
function subChangePage(dir) {
const filtered = getSubFiltered();
const pageSize = parseInt(document.getElementById('sub-page-size').value);
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
subPage += dir;
if (subPage < 1) subPage = 1;
if (subPage > totalPages) subPage = totalPages;
renderSubPage();
}
async function fetchSubscribers() {
document.getElementById('subscribers-tbody').innerHTML = `
<tr>
<td colspan="4" class="px-6 py-8 text-center text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading...
</td>
</tr>
`;
document.getElementById('subscribers-mobile').innerHTML = `
<div class="empty-state text-center px-4 py-8 text-soft">
<i class="fas fa-spinner fa-spin mr-2"></i> Loading...
</div>
`;
try {
const res = await fetch('/api/admin/subscriptions/users');
if (!res.ok) throw new Error('Failed to fetch users');
const data = await res.json();
allUsers = data.data || [];
subPage = 1;
renderSubPage();
} catch (err) {
console.error(err);
document.getElementById('subscribers-tbody').innerHTML = `
<tr>
<td colspan="4" class="px-6 py-8 text-center text-soft">
<i class="fas fa-triangle-exclamation mr-2"></i> Failed to load users.
</td>
</tr>
`;
document.getElementById('subscribers-mobile').innerHTML = `
<div class="empty-state text-center px-4 py-8 text-soft">
<i class="fas fa-triangle-exclamation mr-2"></i> Failed to load users.
</div>
`;
}
}
function openUserModal(userId, userName, action) {
document.getElementById('manage-user-id').value = userId;
document.getElementById('manage-action').value = action;
let title = '';
let desc = '';
let btnClasses = 'glass-btn glass-btn-primary rounded-2xl px-5 py-3 text-sm font-bold';
let showDays = true;
if (action === 'extend') {
title = 'Extend Subscription';
desc = `Add extra days to <b>${userName}</b>'s subscription.`;
document.getElementById('days-action-label').innerText = 'add';
btnClasses = 'glass-btn glass-btn-success rounded-2xl px-5 py-3 text-sm font-bold';
} else if (action === 'reduce') {
title = 'Reduce Subscription';
desc = `Subtract days from <b>${userName}</b>'s subscription.`;
document.getElementById('days-action-label').innerText = 'subtract';
btnClasses = 'glass-btn glass-btn-warning rounded-2xl px-5 py-3 text-sm font-bold';
} else {
title = 'Revoke Subscription';
desc = `<b>Warning:</b> This will remove <b>${userName}</b>'s subscription access completely.`;
showDays = false;
btnClasses = 'glass-btn glass-btn-danger rounded-2xl px-5 py-3 text-sm font-bold';
}
document.getElementById('user-modal-title').innerText = title;
document.getElementById('user-modal-desc').innerHTML = desc;
document.getElementById('days-input-group').style.display = showDays ? 'block' : 'none';
document.getElementById('btn-user-save').className = btnClasses;
if (!showDays) {
document.getElementById('manage-days').value = 0;
document.getElementById('manage-days').removeAttribute('required');
} else {
document.getElementById('manage-days').setAttribute('required', 'required');
if (!document.getElementById('manage-days').value || document.getElementById('manage-days').value === '0') {
document.getElementById('manage-days').value = 30;
}
}
const modal = document.getElementById('user-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.body.classList.add('overflow-hidden');
}
function closeUserModal() {
const modal = document.getElementById('user-modal');
modal.classList.add('hidden');
modal.classList.remove('flex');
document.body.classList.remove('overflow-hidden');
}
async function submitUserForm(e) {
e.preventDefault();
const userId = document.getElementById('manage-user-id').value;
const action = document.getElementById('manage-action').value;
const days = document.getElementById('manage-days').value;
try {
const res = await fetch(`/api/admin/subscriptions/users/${userId}/manage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, days: parseInt(days) || 0 })
});
if (!res.ok) throw new Error('Failed to update user');
closeUserModal();
showToast('Subscription updated successfully.', 'success', 'Updated');
fetchSubscribers();
} catch (err) {
console.error(err);
showToast('Failed to update user subscription.', 'error', 'Update Failed');
}
}
document.addEventListener('click', (e) => {
const planModal = document.getElementById('plan-modal');
const userModal = document.getElementById('user-modal');
if (e.target === planModal) closePlanModal();
if (e.target === userModal) closeUserModal();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closePlanModal();
closeUserModal();
}
});
document.addEventListener('DOMContentLoaded', () => {
fetchPlans();
fetchSubscribers();
});
</script>
{% endblock %}