Restyle staff and console UIs; add UI-Docs walkthrough and review PDF.
Some checks are pending
offline / test (push) Waiting to run
Some checks are pending
offline / test (push) Waiting to run
Match CS/sales/accounting/access-staff to the portal indigo system with dollar amounts, skip links, and empty states. Fleet replica actions move into overflow menus, roles become chips, Docs become cards, and the header copy reflects the 0.0.0.0 bind. Simulator uses the same shell (orange only for faults). Portal API keys are masked; admin customers edit in a drawer. Catalog uses system-ui. New UI-Docs repo holds screenshots, usage notes, and UI-REVIEW.pdf.
This commit is contained in:
parent
ddf772454b
commit
cb07f5b321
72 changed files with 3284 additions and 242 deletions
|
|
@ -4,47 +4,99 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Accounting — customer review</title>
|
||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%234f46e5'/%3E%3C/svg%3E"/>
|
||||
<style>
|
||||
body { font-family: -apple-system, sans-serif; margin: 2rem; max-width: 960px; color: #171a26; }
|
||||
h1 { font-size: 1.3rem; }
|
||||
label { font-size: 0.8rem; font-weight: 600; display: block; margin-top: 0.6rem; }
|
||||
input { padding: 0.4rem 0.55rem; }
|
||||
button { margin-top: 0.8rem; padding: 0.4rem 0.8rem; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 0.6rem; }
|
||||
th, td { border-bottom: 1px solid #e5e7f0; padding: 0.4rem 0.5rem; text-align: left; font-size: 0.88rem; }
|
||||
.muted { color: #6b7186; }
|
||||
.card { border: 1px solid #e5e7f0; border-radius: 10px; padding: 1rem; margin: 1rem 0; }
|
||||
.stat { font-size: 1.4rem; font-weight: 800; }
|
||||
:root {
|
||||
--bg:#f4f5fb; --panel:#fff; --ink:#171a26; --muted:#6b7186; --line:#e5e7f0;
|
||||
--accent:#4f46e5; --accent-ink:#fff; --soft:#eef0fe; --danger:#dc2626; --ok:#047857;
|
||||
--radius:12px; --shadow:0 1px 2px rgba(23,26,38,.05), 0 8px 24px rgba(23,26,38,.06);
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; font:14px/1.45 -apple-system,"SF Pro Text","Segoe UI",sans-serif; background:var(--bg); color:var(--ink); }
|
||||
.skip { position:absolute; left:-999px; }
|
||||
.skip:focus { left:1rem; top:1rem; z-index:20; background:#fff; color:var(--accent); padding:.5rem .9rem; border-radius:8px; }
|
||||
header.app { background:linear-gradient(160deg,#312e81 0%,#4f46e5 60%,#7c74f0 100%); color:#eef0fe; padding:1.15rem 1.5rem 1.25rem; }
|
||||
.kicker { letter-spacing:.12em; text-transform:uppercase; font:700 10px system-ui; opacity:.75; }
|
||||
header.app h1 { margin:.2rem 0 .25rem; font-size:1.25rem; }
|
||||
header.app p { margin:0; font-size:13px; opacity:.88; max-width:42rem; }
|
||||
main { max-width:1080px; margin:0 auto; padding:1.5rem 1.25rem 3rem; }
|
||||
.card { background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:1.1rem 1.25rem; margin-bottom:1rem; }
|
||||
.card h3 { margin:0 0 .75rem; font-size:.95rem; }
|
||||
label { display:block; font-size:.72rem; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--muted); margin:.7rem 0 .25rem; }
|
||||
input { width:100%; max-width:22rem; padding:.5rem .65rem; border:1px solid var(--line); border-radius:8px; font-size:.95rem; }
|
||||
input:focus-visible, button:focus-visible, a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
||||
.row { display:flex; flex-wrap:wrap; gap:.6rem; align-items:end; }
|
||||
button, a.btn { border:0; border-radius:8px; padding:.5rem .9rem; font:650 13px system-ui; cursor:pointer; background:var(--accent); color:#fff; text-decoration:none; display:inline-block; }
|
||||
a.btn.ghost { background:#fff; color:var(--ink); border:1px solid var(--line); }
|
||||
table { border-collapse:collapse; width:100%; }
|
||||
th { text-align:left; font-size:.72rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); border-bottom:1px solid var(--line); padding:.45rem .55rem; }
|
||||
td { border-bottom:1px solid var(--line); padding:.5rem .55rem; }
|
||||
.money { font-variant-numeric:tabular-nums; }
|
||||
.muted { color:var(--muted); }
|
||||
.stat-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:.8rem; margin-bottom:1rem; }
|
||||
.stat { background:var(--panel); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:.9rem 1.1rem; }
|
||||
.stat .k { font-size:.72rem; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
||||
.stat .v { font-size:1.35rem; font-weight:800; margin-top:.2rem; font-variant-numeric:tabular-nums; }
|
||||
.empty { text-align:center; padding:1.5rem 1rem; color:var(--muted); }
|
||||
.empty svg { display:block; margin:0 auto .6rem; }
|
||||
.err { color:var(--danger); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Accounting</h1>
|
||||
<p class="muted">Review payments, usage, credits, and balances. Export invoices to QuickBooks IIF / CSV.</p>
|
||||
<div class="card">
|
||||
<label>Customer id</label>
|
||||
<input id="id" value="cust_1" />
|
||||
<button onclick="review()">Review</button>
|
||||
<p class="muted"><a href="/export/quickbooks.iif">QuickBooks IIF</a> · <a href="/export/accounting.csv">CSV</a></p>
|
||||
</div>
|
||||
<div id="out"></div>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="app">
|
||||
<div class="kicker">staff · accounting</div>
|
||||
<h1>Accounting</h1>
|
||||
<p>Review payments, usage, credits, and balances in dollars. Export invoices to QuickBooks IIF or CSV.</p>
|
||||
</header>
|
||||
<main id="main">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="id">Customer id or name</label>
|
||||
<input id="id" value="cust_1" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" onclick="review()">Review account</button>
|
||||
<a class="btn ghost" href="/export/quickbooks.iif">QuickBooks IIF</a>
|
||||
<a class="btn ghost" href="/export/accounting.csv">CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="out"></div>
|
||||
</main>
|
||||
<script>
|
||||
const fmt = (c) => (c < 0 ? '-$' : '$') + (Math.abs(c || 0) / 100).toFixed(2);
|
||||
const rows = (list, cols) =>
|
||||
(list || [])
|
||||
.map((r) => '<tr>' + cols.map((c) => `<td>${r[c] ?? ''}</td>`).join('') + '</tr>')
|
||||
.join('') || '<tr><td colspan="8" class="muted">none</td></tr>';
|
||||
const when = (v) => (v ? new Date(v).toLocaleString() : '—');
|
||||
const empty = (title, hint) =>
|
||||
`<div class="empty"><svg width="80" height="64" viewBox="0 0 80 64" fill="none" aria-hidden="true"><rect x="16" y="8" width="48" height="48" rx="8" fill="#eef0fe"/><rect x="24" y="20" width="32" height="4" rx="2" fill="#4f46e5" opacity=".35"/><rect x="24" y="30" width="24" height="4" rx="2" fill="#4f46e5" opacity=".2"/><rect x="24" y="40" width="28" height="4" rx="2" fill="#4f46e5" opacity=".2"/></svg><div>${title}</div><p>${hint}</p></div>`;
|
||||
function moneyTable(list, cols, emptyTitle) {
|
||||
if (!list || !list.length) return empty(emptyTitle, 'Nothing recorded for this customer yet.');
|
||||
const head = cols.map((c) => `<th>${c.label}</th>`).join('');
|
||||
const body = list.map((r) => '<tr>' + cols.map((c) => {
|
||||
const v = c.key === 'cents' ? fmt(r.cents) : c.key === 'at' ? when(r.at) : (r[c.key] ?? '—');
|
||||
return `<td class="${c.key === 'cents' ? 'money' : ''}">${v}</td>`;
|
||||
}).join('') + '</tr>').join('');
|
||||
return `<table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
async function review() {
|
||||
const id = document.getElementById('id').value.trim();
|
||||
const st = await (await fetch('/review/' + encodeURIComponent(id))).json();
|
||||
document.getElementById('out').innerHTML = `
|
||||
<div class="card"><div class="muted">Prepaid</div><div class="stat">${fmt(st.prepaidCents)}</div>
|
||||
<p class="muted">source ${st.source || 'unknown'}</p></div>
|
||||
<div class="card"><h3>Credits</h3><table><thead><tr><th>cents</th><th>reason</th><th>agent</th><th>at</th></tr></thead>
|
||||
<tbody>${rows(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>at</th></tr></thead>
|
||||
<tbody>${rows(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>at</th></tr></thead>
|
||||
<tbody>${rows(st.payments, ['cents','kind','reason','at'])}</tbody></table></div>`;
|
||||
try {
|
||||
const r = await fetch('/review/' + encodeURIComponent(id));
|
||||
const st = await r.json();
|
||||
if (!r.ok) throw new Error(st.error || ('HTTP ' + r.status));
|
||||
const name = st.name || st.customerName || id;
|
||||
document.getElementById('out').innerHTML = `
|
||||
<div class="stat-grid">
|
||||
<div class="stat"><div class="k">Customer</div><div class="v" style="font-size:1.05rem">${name}</div></div>
|
||||
<div class="stat"><div class="k">Prepaid balance</div><div class="v">${fmt(st.prepaidCents)}</div></div>
|
||||
<div class="stat"><div class="k">Payments</div><div class="v">${(st.payments||[]).length}</div></div>
|
||||
<div class="stat"><div class="k">Source</div><div class="v" style="font-size:1rem">${st.source || 'unknown'}</div></div>
|
||||
</div>
|
||||
<div class="card"><h3>Credits</h3>${moneyTable(st.credits, [{key:'cents',label:'Amount'},{key:'reason',label:'Reason'},{key:'agent',label:'Agent'},{key:'at',label:'When'}], 'No credits yet')}</div>
|
||||
<div class="card"><h3>Usage</h3>${moneyTable(st.usage, [{key:'endpointId',label:'Endpoint'},{key:'cents',label:'Amount'},{key:'at',label:'When'}], 'No usage yet')}</div>
|
||||
<div class="card"><h3>Payments</h3>${moneyTable(st.payments, [{key:'cents',label:'Amount'},{key:'kind',label:'Kind'},{key:'reason',label:'Reason'},{key:'at',label:'When'}], 'No payments yet')}</div>`;
|
||||
} catch (err) {
|
||||
document.getElementById('out').innerHTML = `<div class="card err">${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue