Panel: add Admin category under Members view; exclude operator accounts from member list/counts
This commit is contained in:
1 parent
345eca7f1e
commit
bee02c6e4d
1 file changed
+62
-1
+62
-1
@@ -276,7 +276,10 @@ PAGE_HTML = """<!DOCTYPE html>
|
|||||||
.range-btn.active { background: var(--green-mid); color: #fff; border-color: var(--green-mid); }
|
.range-btn.active { background: var(--green-mid); color: #fff; border-color: var(--green-mid); }
|
||||||
.members-all-time .balance-col.allowance-only,
|
.members-all-time .balance-col.allowance-only,
|
||||||
.members-all-time .balance-col.remaining-only { display: none; }
|
.members-all-time .balance-col.remaining-only { display: none; }
|
||||||
.members-all-time .credits-col-head .mode-note::after { content: " (total purchased)"; }
|
.admins-all-time .balance-col.allowance-only,
|
||||||
|
.admins-all-time .balance-col.remaining-only { display: none; }
|
||||||
|
.members-all-time .credits-col-head .mode-note::after,
|
||||||
|
.admins-all-time .credits-col-head .mode-note::after { content: " (total purchased)"; }
|
||||||
.table-scroll { overflow-x: auto; border-radius: 12px; }
|
.table-scroll { overflow-x: auto; border-radius: 12px; }
|
||||||
.table-scroll table { min-width: 980px; }
|
.table-scroll table { min-width: 980px; }
|
||||||
th.sortable { cursor: pointer; user-select: none; }
|
th.sortable { cursor: pointer; user-select: none; }
|
||||||
@@ -336,6 +339,29 @@ PAGE_HTML = """<!DOCTYPE html>
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="admins-section" style="display:none;">
|
||||||
|
<h3 style="margin:20px 0 8px;">Admin</h3>
|
||||||
|
<p style="color:var(--muted);font-size:12px;margin:0 0 8px;">Operator accounts (SSO owner and bot). Not members — excluded from member counts; their usage is a collective expense.</p>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table id="table-admins">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Account</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Activated</th>
|
||||||
|
<th class="balance-col allowance-only">Allowance</th>
|
||||||
|
<th class="balance-col credits-col-head">Credits<span class="mode-note"></span></th>
|
||||||
|
<th><span id="spend-mode-label-admin">Spend</span></th>
|
||||||
|
<th class="balance-col">Remaining</th>
|
||||||
|
<th class="balance-col">Reset</th>
|
||||||
|
<th>Adjust</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="admin-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-scroll" style="margin-top:12px;">
|
<div class="table-scroll" style="margin-top:12px;">
|
||||||
<table id="table-models" style="display:none;">
|
<table id="table-models" style="display:none;">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -386,6 +412,7 @@ async function load() {
|
|||||||
$('tot-spend-cycle').textContent = fmt(d.totals.total_spend);
|
$('tot-spend-cycle').textContent = fmt(d.totals.total_spend);
|
||||||
$('tot-spend-all').textContent = fmt(d.totals.total_all_time_spend ?? d.totals.total_spend);
|
$('tot-spend-all').textContent = fmt(d.totals.total_all_time_spend ?? d.totals.total_spend);
|
||||||
membersData = d.members || [];
|
membersData = d.members || [];
|
||||||
|
adminsData = d.admins || [];
|
||||||
if (currentView === 'members') renderMembers();
|
if (currentView === 'members') renderMembers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
flash('Could not load: ' + e.message, false);
|
flash('Could not load: ' + e.message, false);
|
||||||
@@ -394,6 +421,7 @@ async function load() {
|
|||||||
|
|
||||||
// ---- shared state ----
|
// ---- shared state ----
|
||||||
let membersData = [];
|
let membersData = [];
|
||||||
|
let adminsData = [];
|
||||||
let modelsData = [];
|
let modelsData = [];
|
||||||
let currentView = 'members';
|
let currentView = 'members';
|
||||||
let memberSort = { key: 'spend', dir: -1 }; // leaderboard default: most spend first
|
let memberSort = { key: 'spend', dir: -1 }; // leaderboard default: most spend first
|
||||||
@@ -442,8 +470,12 @@ function setMemberMode(mode) {
|
|||||||
$('mmode-all').className = 'range-btn' + (mode === 'all' ? ' active' : '');
|
$('mmode-all').className = 'range-btn' + (mode === 'all' ? ' active' : '');
|
||||||
// Header label reflects the mode
|
// Header label reflects the mode
|
||||||
$('spend-mode-label').textContent = mode === 'all' ? 'All-time spend' : 'Spend';
|
$('spend-mode-label').textContent = mode === 'all' ? 'All-time spend' : 'Spend';
|
||||||
|
const adminLabel = document.getElementById('spend-mode-label-admin');
|
||||||
|
if (adminLabel) adminLabel.textContent = mode === 'all' ? 'All-time spend' : 'Spend';
|
||||||
// Balance-only columns (Allowance/Credits/Remaining/Reset) hide in all-time mode
|
// Balance-only columns (Allowance/Credits/Remaining/Reset) hide in all-time mode
|
||||||
$('table-members').parentElement.classList.toggle('members-all-time', mode === 'all');
|
$('table-members').parentElement.classList.toggle('members-all-time', mode === 'all');
|
||||||
|
const adminTable = document.getElementById('table-admins');
|
||||||
|
if (adminTable) adminTable.parentElement.classList.toggle('admins-all-time', mode === 'all');
|
||||||
renderMembers();
|
renderMembers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,6 +544,35 @@ function renderMembers() {
|
|||||||
'</tr>';
|
'</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
clearArrows('members', memberSort);
|
clearArrows('members', memberSort);
|
||||||
|
|
||||||
|
// Admin category — operator accounts (SSO owner + bot), separate from members.
|
||||||
|
const adminSection = document.getElementById('admins-section');
|
||||||
|
if (!adminsData.length) { adminSection.style.display = 'none'; return; }
|
||||||
|
adminSection.style.display = '';
|
||||||
|
$('admin-rows').innerHTML = adminsData.map(m => {
|
||||||
|
const reset = m.reset_at ? m.reset_at.slice(0,10) : '—';
|
||||||
|
const credits = m.credit_balance ?? 0;
|
||||||
|
const creditsLeft = m.credits_remaining ?? credits;
|
||||||
|
const creditsShown = memberMode === 'all' ? credits : creditsLeft;
|
||||||
|
const creditsCell = creditsShown > 0
|
||||||
|
? fmt(creditsShown)
|
||||||
|
: '<span style="color:var(--muted)">—</span>';
|
||||||
|
return '<tr>' +
|
||||||
|
'<td>' + esc(m.email) + (m.username ? '<div style="color:var(--muted);font-size:11px">' + esc(m.username) + '</div>' : '') + '</td>' +
|
||||||
|
'<td><span class="pill manual" title="Operator account (SSO owner / bot), not a member">admin</span></td>' +
|
||||||
|
'<td>' + (m.activated ? '<span class="pill active">yes</span>' : '<span class="pill inactive">pending</span>') + '</td>' +
|
||||||
|
'<td class="num balance-col allowance-only">' + fmt(m.balance) + '</td>' +
|
||||||
|
'<td class="num balance-col">' + creditsCell + '</td>' +
|
||||||
|
'<td class="num">' + fmt(memberSpend(m)) + '</td>' +
|
||||||
|
'<td class="num balance-col remaining-col remaining-only">' + fmt(m.remaining) + '</td>' +
|
||||||
|
'<td class="balance-col">' + esc(reset) + '</td>' +
|
||||||
|
'<td><div class="adjust">' +
|
||||||
|
'<input id="add-' + i(m.email) + '" placeholder="+$"/>' +
|
||||||
|
'<button data-email="' + esc(m.email) + '" onclick="addBalance(this.dataset.email)" title="Add monthly allowance">Allow</button>' +
|
||||||
|
'<button data-email="' + esc(m.email) + '" class="credits-btn" onclick="addCredits(this.dataset.email)" title="Add non-expiring credits">Credit</button>' +
|
||||||
|
'</div></td>' +
|
||||||
|
'</tr>';
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addCredits(email) {
|
async function addCredits(email) {
|
||||||
|
|||||||
Reference in new issue
Block a user