feat: search and status filter on the Подписки table
Another line off the fresh docs.rw comparison from last commit: "User Management Filters — Extended selection (Remnawave) vs Minimal options (Marzban)". The subscriptions table had none at all — no search, no status filter, just the raw list with a server-side limit=200. Fine with a handful of test subscriptions, useless once a real business has a few hundred customers and support needs to find one person's row. Pure client-side: the full list was already fetched in one call (/admin/api/subscriptions), so filtering it in the browser needs no new backend route and can't regress anything server-side. Refactored loadSubscriptions() to keep the fetched list in allSubs and render through a separate renderFilteredSubs(), which the existing revoke/hold/resume refresh calls now go through too — so the search box and status filter stay applied after an action instead of resetting the view. Search matches username, tg_id, node label, and plan label as one lowercased substring check. Status filter (active / on hold / expired- revoked / all) reuses the exact three-way split statusBadge() already draws, via a new subStatus() helper — same custom .dd dropdown as everywhere else in the panel, not a native <select>. Verification: extracted the actual subStatus()/renderFilteredSubs() filter predicate out of admin.html — not a reimplementation, diffed it against the file to confirm byte-for-byte match — and ran it under Node against four mock subscriptions covering all three statuses, including one with a null username (the real shape for gift-redeemed subs with no Telegram username set) to make sure the search doesn't throw on that. 13 checks: plain search, case-insensitivity, tg_id/node/plan matching, no-match, each status filter alone, and two combined search+status cases. node --check on the full extracted script, div-tag balance on the whole file, both clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
670579fccd
commit
b8c4949201
1 changed files with 42 additions and 2 deletions
44
admin.html
44
admin.html
|
|
@ -332,6 +332,10 @@
|
|||
<div id="view-subscriptions" class="view">
|
||||
<div class="page-title">Подписки</div>
|
||||
<div class="page-sub">Все выданные подписки</div>
|
||||
<div class="form-row" style="margin-bottom:16px">
|
||||
<div><input type="text" id="subs-search" placeholder="Поиск: юзернейм, tg id, сервер, тариф" oninput="renderFilteredSubs()"></div>
|
||||
<div style="flex:0;min-width:180px"><div id="subs-status-filter" class="dd"></div></div>
|
||||
</div>
|
||||
<div class="table-wrap"><table><thead><tr>
|
||||
<th>Пользователь</th><th>Сервер</th><th>Тариф</th><th>Выдана</th><th>Истекает</th><th>Статус</th><th></th>
|
||||
</tr></thead><tbody id="subs-body"></tbody></table></div>
|
||||
|
|
@ -969,6 +973,16 @@ function initDropdowns() {
|
|||
const countryOptions = COUNTRIES.map(([code, name]) => ({
|
||||
value: code, label: `${flagEmoji(code)} ${name}`,
|
||||
}));
|
||||
DD.subsStatusFilter = createDropdown("subs-status-filter", {
|
||||
options: [
|
||||
{ value: "all", label: "Все статусы" },
|
||||
{ value: "active", label: "Активные" },
|
||||
{ value: "held", label: "На паузе" },
|
||||
{ value: "expired", label: "Истекшие/отозванные" },
|
||||
],
|
||||
value: "all", placeholder: "Статус",
|
||||
onChange: () => renderFilteredSubs(),
|
||||
});
|
||||
DD.ngCountry = createDropdown("ng-country", {
|
||||
options: countryOptions, placeholder: "Страна", searchable: true,
|
||||
onChange: (code) => {
|
||||
|
|
@ -1413,8 +1427,9 @@ async function loadDashboard() {
|
|||
`).join("") : '<tr><td colspan="5"><div class="empty">Пока нет подписок</div></td></tr>';
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
const subs = await api("/admin/api/subscriptions");
|
||||
let allSubs = [];
|
||||
|
||||
function renderSubsTable(subs) {
|
||||
const body = document.getElementById("subs-body");
|
||||
body.innerHTML = subs.length ? subs.map((s, i) => `
|
||||
<tr ${rowAttr(i)}><td>${s.username ? "@" + esc(s.username) : "tg" + s.tg_id}</td><td>${esc(s.node_label)}</td><td>${esc(s.plan_label)}</td>
|
||||
|
|
@ -1429,6 +1444,31 @@ async function loadSubscriptions() {
|
|||
`).join("") : '<tr><td colspan="7"><div class="empty">Пока нет подписок</div></td></tr>';
|
||||
}
|
||||
|
||||
function subStatus(s) {
|
||||
if (s.held_at) return "held";
|
||||
if (!s.active) return "expired";
|
||||
return "active";
|
||||
}
|
||||
|
||||
function renderFilteredSubs() {
|
||||
const q = document.getElementById("subs-search").value.trim().toLowerCase();
|
||||
const statusFilter = DD.subsStatusFilter ? DD.subsStatusFilter.getValue() : "all";
|
||||
const filtered = allSubs.filter((s) => {
|
||||
if (statusFilter !== "all" && subStatus(s) !== statusFilter) return false;
|
||||
if (!q) return true;
|
||||
const haystack = [
|
||||
s.username || "", String(s.tg_id), s.node_label || "", s.plan_label || "",
|
||||
].join(" ").toLowerCase();
|
||||
return haystack.includes(q);
|
||||
});
|
||||
renderSubsTable(filtered);
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
allSubs = await api("/admin/api/subscriptions");
|
||||
renderFilteredSubs();
|
||||
}
|
||||
|
||||
let devicesTgId = null;
|
||||
async function openUserCard(tgId, label) {
|
||||
devicesTgId = tgId;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue