zappier-sales-pricing/public/index.html

153 lines
9.5 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sales — 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>
: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 { border:0; border-radius:8px; padding:.5rem .9rem; font:650 13px system-ui; cursor:pointer; background:var(--accent); color:#fff; }
button.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); }
.kv { display:grid; grid-template-columns:160px 1fr; gap:.4rem .8rem; }
.kv dt { color:var(--muted); font-size:.82rem; }
.kv dd { margin:0; font-weight:600; }
</style>
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<header class="app">
<div class="kicker">staff · sales</div>
<h1>Sales</h1>
<p>Review a customers balance, credits, usage, and payments. Open a quote with names and dollar amounts — not a JSON dump.</p>
</header>
<main id="main">
<div class="card">
<div class="row">
<div>
<label for="id">Customer</label>
<input id="id" list="cust-list" placeholder="Type a name or id" autocomplete="off" />
<datalist id="cust-list"></datalist>
</div>
<button type="button" onclick="review()">Review account</button>
<button type="button" class="ghost" onclick="quote()">Show quote</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>`;
}
function paintStatement(st, id) {
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>`;
}
async function loadCustomers() {
try {
const { customers } = await (await fetch('/customers')).json();
document.getElementById('cust-list').innerHTML = (customers || [])
.map((c) => `<option value="${c.name}">${c.id}</option>`)
.join('');
if (customers && customers[0] && !document.getElementById('id').value) {
document.getElementById('id').value = customers[0].name;
}
} catch { /* edge optional */ }
}
loadCustomers();
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));
paintStatement(st, id);
} catch (err) {
document.getElementById('out').innerHTML = `<div class="card err">${err.message}</div>`;
}
}
function pretty(v) {
if (v == null) return '—';
if (typeof v === 'number' && /cents|Cents/.test(String(v))) return fmt(v);
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
}
async function quote() {
const id = document.getElementById('id').value.trim();
try {
const r = await fetch('/quotes/' + encodeURIComponent(id));
const q = await r.json();
if (!r.ok) throw new Error(q.error || ('HTTP ' + r.status));
const name = q.name || q.customerName || id;
const skip = new Set(['name', 'customerName']);
const rows = Object.entries(q)
.filter(([k, v]) => !skip.has(k) && v != null && typeof v !== 'object')
.map(([k, v]) => {
const label = k.replace(/Cents$/, ' (USD)');
const val = /cents|Cents$/i.test(k) && typeof v === 'number' ? fmt(v) : v;
return `<dt>${label}</dt><dd>${val}</dd>`;
}).join('');
const nested = Object.entries(q).filter(([, v]) => v && typeof v === 'object')
.map(([k, v]) => `<div class="card"><h3>${k}</h3><pre class="muted" style="white-space:pre-wrap">${JSON.stringify(v, null, 2)}</pre></div>`).join('');
document.getElementById('out').innerHTML = `
<div class="card">
<h3>Quote — ${name}</h3>
<dl class="kv">${rows || '<dt>Customer</dt><dd>' + name + '</dd>'}</dl>
</div>${nested}`;
} catch (err) {
document.getElementById('out').innerHTML = `<div class="card err">${err.message}</div>`;
}
}
</script>
</body>
</html>