Add NATS account-balance SoT and statement review for customers, CS, and sales
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Internal billing now uses verae.billing.* request-reply and pubs. zappier-account-balance tracks prepaid, credits, usage, and payments. Portal, admin, CS, and sales all review the same statement. Independent Forgejo repos stay split via push-module-repos.
This commit is contained in:
parent
a1a5b957fd
commit
ac38676645
135 changed files with 3078 additions and 130 deletions
|
|
@ -98,6 +98,7 @@ async function load() {
|
|||
renderEndpoints();
|
||||
renderTiers();
|
||||
renderCustomers();
|
||||
renderStatement();
|
||||
renderInvoices();
|
||||
renderReports();
|
||||
renderSystem();
|
||||
|
|
@ -277,7 +278,8 @@ function renderCustomers() {
|
|||
<td><select data-customer="${c.id}" data-field="tierId">${tierOptions(c.tierId)}</select></td>
|
||||
<td><input data-customer="${c.id}" data-field="multiplierOverride" type="number" step="any" size="5" value="${c.multiplierOverride ?? ''}" placeholder="—"></td>
|
||||
<td><select data-customer="${c.id}" data-field="billingType">${btOptions(c.billingType)}</select></td>
|
||||
<td class="row-actions"><button class="btn" onclick="saveCustomer('${c.id}')">Save</button></td>
|
||||
<td class="row-actions"><button class="btn ghost" onclick="reviewCustomer('${c.id}')">Statement</button>
|
||||
<button class="btn" onclick="saveCustomer('${c.id}')">Save</button></td>
|
||||
</tr>`,
|
||||
)
|
||||
.join('');
|
||||
|
|
@ -308,6 +310,47 @@ async function saveCustomer(id) {
|
|||
await load();
|
||||
}
|
||||
|
||||
function renderStatement() {
|
||||
const options = customerOptions(state.customers[0]?.id || '');
|
||||
document.getElementById('statement').innerHTML = `
|
||||
<h2>Customer statement</h2>
|
||||
<p class="lede">Credits, prepaid balance, usage, and payments. Prefers NATS account-balance.</p>
|
||||
<div class="card">
|
||||
<div class="filterbar">
|
||||
<label><span>Customer</span><select id="stmt-customer">${options}</select></label>
|
||||
<button class="btn" onclick="reviewCustomer(document.getElementById('stmt-customer').value)">Load</button>
|
||||
</div>
|
||||
<div id="stmt-out"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function reviewCustomer(id) {
|
||||
if (!id) return;
|
||||
const st = await api(`/statement/${id}`);
|
||||
const row = (list, cols) =>
|
||||
(list || [])
|
||||
.map((r) => `<tr>${cols.map((c) => `<td>${r[c] ?? ''}</td>`).join('')}</tr>`)
|
||||
.join('') || `<tr><td colspan="${cols.length}" style="color:var(--muted)">None.</td></tr>`;
|
||||
const html = `
|
||||
<p class="lede">${st.name || id} · prepaid ${fmt(st.prepaidCents || 0)} · source ${st.source || 'local'}</p>
|
||||
<div class="card"><h3>Credits</h3><table><thead><tr><th>Cents</th><th>Reason</th><th>Agent</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.credits, ['cents', 'reason', 'agent', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Usage</h3><table><thead><tr><th>Endpoint</th><th>Cents</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.usage, ['endpointId', 'cents', 'at'])}</tbody></table></div>
|
||||
<div class="card"><h3>Payments</h3><table><thead><tr><th>Cents</th><th>Kind</th><th>Reason</th><th>When</th></tr></thead>
|
||||
<tbody>${row(st.payments, ['cents', 'kind', 'reason', 'at'])}</tbody></table></div>`;
|
||||
const out = document.getElementById('stmt-out');
|
||||
if (out) out.innerHTML = html;
|
||||
else {
|
||||
document.getElementById('statement').innerHTML = `<h2>Customer statement</h2>${html}`;
|
||||
document.querySelectorAll('nav button').forEach((b) => b.classList.toggle('active', b.dataset.tab === 'statement'));
|
||||
document.querySelectorAll('main section').forEach((s) => (s.hidden = s.id !== 'statement'));
|
||||
}
|
||||
const sel = document.getElementById('stmt-customer');
|
||||
if (sel) sel.value = id;
|
||||
say(`Loaded statement for ${id}.`);
|
||||
}
|
||||
|
||||
async function addCustomer() {
|
||||
const name = document.getElementById('new-customer-name').value.trim();
|
||||
const tierId = document.getElementById('new-customer-tier').value;
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@
|
|||
<button data-tab="endpoints" class="active">▦ Rate card</button>
|
||||
<button data-tab="tiers">◈ Customer types</button>
|
||||
<button data-tab="customers">☺ Customers</button>
|
||||
<button data-tab="statement">☰ Statement</button>
|
||||
<button data-tab="invoices">▤ Invoices</button>
|
||||
<button data-tab="reports">↗ Reports</button>
|
||||
<button data-tab="system">⚙ System</button>
|
||||
|
|
@ -248,6 +249,7 @@
|
|||
<section id="endpoints"></section>
|
||||
<section id="tiers" hidden></section>
|
||||
<section id="customers" hidden></section>
|
||||
<section id="statement" hidden></section>
|
||||
<section id="invoices" hidden></section>
|
||||
<section id="reports" hidden></section>
|
||||
<section id="system" hidden></section>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue