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
134
packages/verae-access-staff/public/index.html
Normal file
134
packages/verae-access-staff/public/index.html
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Staff access — account 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>
|
||||
: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; }
|
||||
.row > div { min-width:10rem; }
|
||||
button { border:0; border-radius:8px; padding:.5rem .9rem; font:650 13px system-ui; cursor:pointer; background:var(--accent); color:#fff; }
|
||||
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>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="app">
|
||||
<div class="kicker">staff plane · after authz</div>
|
||||
<h1>Staff access</h1>
|
||||
<p>CS / sales / accounting door. Review and credit go through <code>verae.access.authz.check</code>, then account-balance. Amounts are dollars; the customer name is shown when the ledger has it.</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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Apply credit</h3>
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="dollars">Amount (USD)</label>
|
||||
<input id="dollars" type="number" min="0.01" step="0.01" value="5.00" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="reason">Reason</label>
|
||||
<input id="reason" value="goodwill" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="agent">Agent name</label>
|
||||
<input id="agent" value="cs" />
|
||||
</div>
|
||||
<button type="button" onclick="credit()">Apply credit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="out"></div>
|
||||
</main>
|
||||
<script>
|
||||
const fmt = (c) => (c < 0 ? '-$' : '$') + (Math.abs(c || 0) / 100).toFixed(2);
|
||||
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();
|
||||
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">Credits</div><div class="v">${(st.credits||[]).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>`;
|
||||
}
|
||||
}
|
||||
async function credit() {
|
||||
const body = {
|
||||
customerId: document.getElementById('id').value.trim(),
|
||||
cents: Math.round(Number(document.getElementById('dollars').value) * 100),
|
||||
reason: document.getElementById('reason').value,
|
||||
agent: document.getElementById('agent').value,
|
||||
};
|
||||
const r = await fetch('/credits', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!r.ok) {
|
||||
const st = await r.json().catch(() => ({}));
|
||||
document.getElementById('out').innerHTML = `<div class="card err">${st.error || r.status}</div>`;
|
||||
return;
|
||||
}
|
||||
await review();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue