Banking — Ops Dashboard
An internal banking operations dashboard built with vanilla HTML, CSS and JavaScript. Surfaces live KPIs for transaction volume, active users, fraud flags and the KYC queue, alongside a layered credit-versus-debit volume chart, a triaged fraud-alert feed, a KYC approval queue and a recent disputes table. Operators can switch timeframes, approve or reject applicants inline, and dismiss alerts, each action confirmed with a toast and tabular-figure money formatting throughout.
MCP
Code
:root {
--navy: #0e1b3a;
--navy-2: #16264d;
--ink: #0e1726;
--ink-2: #3a4660;
--muted: #697089;
--accent: #3b6ef6;
--accent-d: #2a55cc;
--accent-50: #eaf0ff;
--teal: #0fb5a6;
--violet: #7c5cff;
--bg: #f5f7fb;
--surface: #ffffff;
--line: rgba(14, 27, 58, 0.10);
--line-2: rgba(14, 27, 58, 0.18);
--ok: #1f9d62;
--warn: #d9982b;
--danger: #d4493e;
--credit: #1f9d62;
--debit: #0e1726;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(14, 27, 58, 0.06), 0 1px 3px rgba(14, 27, 58, 0.05);
--sh-2: 0 4px 14px rgba(14, 27, 58, 0.08), 0 1px 3px rgba(14, 27, 58, 0.06);
--sh-3: 0 14px 40px rgba(14, 27, 58, 0.14);
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-variant-numeric: tabular-nums;
}
h1, h2 { margin: 0; font-weight: 700; letter-spacing: -0.01em; }
button { font-family: inherit; cursor: pointer; }
.ta-r { text-align: right; }
/* ---------- Shell ---------- */
.shell {
display: grid;
grid-template-columns: 244px 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: linear-gradient(178deg, var(--navy) 0%, var(--navy-2) 100%);
color: #cdd6ef;
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 22px;
position: sticky;
top: 0;
height: 100vh;
}
.brand { display: flex; align-items: center; gap: 11px; padding: 4px 6px; }
.brand-mark {
width: 38px; height: 38px; border-radius: 11px;
background: linear-gradient(135deg, var(--accent), var(--violet));
display: grid; place-items: center;
font-weight: 800; color: #fff; font-size: 19px;
box-shadow: 0 6px 16px rgba(59, 110, 246, 0.4);
}
.brand-name { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name strong { color: #fff; font-size: 15px; }
.brand-name span { font-size: 11px; color: #8ea0cc; letter-spacing: 0.04em; text-transform: uppercase; }
.nav { display: flex; flex-direction: column; gap: 3px; }
.nav-item {
display: flex; align-items: center; gap: 11px;
padding: 9px 12px; border-radius: 10px;
color: #aab6d8; text-decoration: none; font-size: 14px; font-weight: 500;
transition: background .15s, color .15s;
}
.nav-item .ni-dot {
width: 6px; height: 6px; border-radius: 50%;
background: #5a6a96; flex: none; transition: background .15s, box-shadow .15s;
}
.nav-item:hover { background: rgba(255, 255, 255, 0.06); color: #e6ecfb; }
.nav-item.is-active { background: rgba(59, 110, 246, 0.22); color: #fff; }
.nav-item.is-active .ni-dot { background: var(--accent); box-shadow: 0 0 0 4px rgba(59, 110, 246, 0.25); }
.sidebar-foot { margin-top: auto; font-size: 11.5px; display: flex; flex-direction: column; gap: 7px; padding: 0 6px; }
.status-line { display: flex; align-items: center; gap: 8px; color: #b7c2e0; }
.dot-ok { width: 8px; height: 8px; border-radius: 50%; background: var(--teal); box-shadow: 0 0 0 4px rgba(15, 181, 166, 0.22); }
.env-tag { color: #7b89b3; letter-spacing: 0.03em; }
/* ---------- Main ---------- */
.main { padding: 22px 26px 40px; min-width: 0; }
.topbar {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap; margin-bottom: 20px;
}
.topbar h1 { font-size: 22px; }
.sub { margin: 3px 0 0; color: var(--muted); font-size: 13px; }
.topbar-right { display: flex; align-items: center; gap: 14px; }
.timeframe { display: inline-flex; background: var(--surface); border: 1px solid var(--line); border-radius: 11px; padding: 3px; box-shadow: var(--sh-1); }
.tf-btn {
border: 0; background: transparent; color: var(--ink-2);
font-weight: 600; font-size: 13px; padding: 7px 14px; border-radius: 8px;
transition: background .15s, color .15s;
}
.tf-btn:hover { color: var(--ink); }
.tf-btn.is-active { background: var(--accent); color: #fff; box-shadow: 0 2px 8px rgba(59, 110, 246, 0.35); }
.op-user { display: flex; align-items: center; gap: 10px; }
.op-avatar {
width: 38px; height: 38px; border-radius: 50%;
background: var(--accent-50); color: var(--accent-d);
display: grid; place-items: center; font-weight: 700; font-size: 13px;
border: 1px solid var(--line);
}
.op-meta { display: flex; flex-direction: column; line-height: 1.2; }
.op-meta strong { font-size: 13.5px; }
.op-meta small { font-size: 11px; color: var(--ok); display: flex; align-items: center; gap: 4px; }
.op-meta .lock { font-size: 10px; }
/* ---------- KPIs ---------- */
.kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 18px; }
.kpi {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 16px 18px; box-shadow: var(--sh-1);
transition: transform .15s, box-shadow .15s;
}
.kpi:hover { transform: translateY(-2px); box-shadow: var(--sh-2); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-label { font-size: 12.5px; color: var(--muted); font-weight: 500; }
.kpi-ico {
width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center;
font-weight: 800; font-size: 14px;
}
.tone-blue { background: var(--accent-50); color: var(--accent-d); }
.tone-teal { background: rgba(15, 181, 166, 0.14); color: #0a8a7e; }
.tone-danger { background: rgba(212, 73, 62, 0.13); color: var(--danger); }
.tone-violet { background: rgba(124, 92, 255, 0.14); color: #5b3fd1; }
.kpi-val { font-size: 27px; font-weight: 800; letter-spacing: -0.02em; margin: 10px 0 4px; }
.kpi-foot { font-size: 12px; color: var(--muted); }
.delta { font-weight: 700; }
.delta.up { color: var(--ok); }
.delta.down { color: var(--ok); }
.delta.bad { color: var(--danger); }
.muted-foot { color: var(--warn); font-weight: 600; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 16px;
}
.card {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
padding: 18px 20px; box-shadow: var(--sh-1);
}
.chart-card { grid-column: 1; }
.feed-card { grid-column: 2; grid-row: 1 / span 2; }
.kyc-card { grid-column: 1; }
.disputes-card { grid-column: 1; }
.card-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; gap: 10px; }
.card-head h2 { font-size: 15px; }
.chip {
font-size: 11px; color: var(--ink-2); background: var(--bg);
border: 1px solid var(--line); border-radius: 999px; padding: 4px 10px; font-weight: 600;
}
.count-pill {
font-size: 11.5px; font-weight: 700; padding: 4px 11px; border-radius: 999px;
background: var(--accent-50); color: var(--accent-d);
}
/* ---------- Chart ---------- */
.chart {
display: flex; align-items: flex-end; gap: 6px;
height: 168px; padding-top: 6px;
}
.bar-col { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; gap: 2px; min-width: 0; }
.bar {
border-radius: 5px 5px 2px 2px;
transition: height .5s cubic-bezier(.2, .8, .2, 1), opacity .2s;
position: relative;
}
.bar.credit { background: linear-gradient(180deg, var(--credit), #1a7d4f); }
.bar.debit { background: linear-gradient(180deg, #3a4660, var(--ink)); border-radius: 2px 2px 5px 5px; }
.bar-col:hover .bar { opacity: 0.82; }
.bar-col:hover .bar::after {
content: attr(data-label);
position: absolute; top: -26px; left: 50%; transform: translateX(-50%);
background: var(--ink); color: #fff; font-size: 10.5px; font-weight: 600;
padding: 3px 7px; border-radius: 6px; white-space: nowrap; z-index: 3;
}
.chart-legend {
display: flex; align-items: center; gap: 16px; margin-top: 12px;
font-size: 11.5px; color: var(--muted); flex-wrap: wrap;
}
.lg { width: 10px; height: 10px; border-radius: 3px; display: inline-block; vertical-align: -1px; margin-right: 5px; }
.lg-credit { background: var(--credit); }
.lg-debit { background: var(--ink); }
.peak { margin-left: auto; font-weight: 600; color: var(--ink-2); }
/* ---------- Fraud feed ---------- */
.feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.alert {
display: flex; gap: 11px; align-items: flex-start;
border: 1px solid var(--line); border-left: 3px solid var(--warn);
border-radius: var(--r-sm); padding: 11px 12px;
background: var(--surface);
animation: pop .25s ease;
}
.alert.sev-high { border-left-color: var(--danger); }
.alert.sev-med { border-left-color: var(--warn); }
.alert.sev-low { border-left-color: var(--accent); }
.alert.dismissing { opacity: 0; transform: translateX(14px); transition: .28s; }
.al-body { flex: 1; min-width: 0; }
.al-top { display: flex; align-items: center; gap: 7px; margin-bottom: 2px; }
.sev-tag { font-size: 9.5px; font-weight: 800; letter-spacing: 0.05em; padding: 2px 6px; border-radius: 5px; text-transform: uppercase; }
.sev-high .sev-tag { background: rgba(212, 73, 62, 0.13); color: var(--danger); }
.sev-med .sev-tag { background: rgba(217, 152, 43, 0.16); color: #a8741b; }
.sev-low .sev-tag { background: var(--accent-50); color: var(--accent-d); }
.al-title { font-size: 13px; font-weight: 600; }
.al-meta { font-size: 11.5px; color: var(--muted); }
.al-meta b { color: var(--ink-2); font-weight: 600; }
.al-amt { font-weight: 700; color: var(--debit); }
.al-dismiss {
border: 1px solid var(--line); background: var(--surface); color: var(--muted);
width: 24px; height: 24px; border-radius: 7px; font-size: 13px; line-height: 1;
flex: none; transition: background .15s, color .15s, border-color .15s;
}
.al-dismiss:hover { background: var(--bg); color: var(--danger); border-color: var(--line-2); }
.feed-empty { text-align: center; color: var(--muted); font-size: 13px; padding: 26px 0; }
/* ---------- Tables ---------- */
.table-wrap { overflow-x: auto; }
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
.tbl thead th {
text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em;
color: var(--muted); font-weight: 600; padding: 0 10px 9px; border-bottom: 1px solid var(--line);
}
.tbl tbody td { padding: 11px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.tbl tbody tr:last-child td { border-bottom: 0; }
.tbl tbody tr { transition: background .12s; }
.tbl tbody tr:hover { background: rgba(59, 110, 246, 0.04); }
.tbl tr.removing td { opacity: 0; transition: opacity .25s; }
.applicant { display: flex; align-items: center; gap: 10px; }
.app-av { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; font-size: 11.5px; font-weight: 700; color: #fff; flex: none; }
.app-name { font-weight: 600; line-height: 1.2; }
.app-id { font-size: 11px; color: var(--muted); }
.risk { font-weight: 700; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; }
.risk::before { content: ""; width: 7px; height: 7px; border-radius: 50%; }
.risk.low { color: var(--ok); }
.risk.low::before { background: var(--ok); }
.risk.med { color: #a8741b; }
.risk.med::before { background: var(--warn); }
.risk.high { color: var(--danger); }
.risk.high::before { background: var(--danger); }
.kyc-actions { display: inline-flex; gap: 7px; justify-content: flex-end; }
.btn-s {
border: 1px solid var(--line); background: var(--surface); color: var(--ink-2);
font-size: 12px; font-weight: 600; padding: 6px 12px; border-radius: 8px;
transition: all .14s;
}
.btn-s:hover { border-color: var(--line-2); }
.btn-approve { color: var(--ok); border-color: rgba(31, 157, 98, 0.3); }
.btn-approve:hover { background: var(--ok); color: #fff; border-color: var(--ok); }
.btn-reject { color: var(--danger); border-color: rgba(212, 73, 62, 0.3); }
.btn-reject:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
.pill {
font-size: 11px; font-weight: 700; padding: 3px 10px; border-radius: 999px; display: inline-block;
}
.pill.pending { background: rgba(217, 152, 43, 0.15); color: #a8741b; }
.pill.review { background: var(--accent-50); color: var(--accent-d); }
.pill.cleared { background: rgba(31, 157, 98, 0.14); color: var(--ok); }
.pill.failed { background: rgba(212, 73, 62, 0.13); color: var(--danger); }
.amt-debit { font-weight: 700; color: var(--debit); }
/* ---------- Toast ---------- */
.toast-wrap { position: fixed; right: 18px; bottom: 18px; display: flex; flex-direction: column; gap: 10px; z-index: 50; }
.toast {
background: var(--ink); color: #fff; padding: 11px 15px; border-radius: var(--r-sm);
font-size: 13px; font-weight: 500; box-shadow: var(--sh-3);
display: flex; align-items: center; gap: 9px; min-width: 220px;
animation: slideIn .25s cubic-bezier(.2, .8, .2, 1);
}
.toast .t-ico { width: 18px; height: 18px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; flex: none; }
.toast.ok .t-ico { background: var(--ok); }
.toast.bad .t-ico { background: var(--danger); }
.toast.info .t-ico { background: var(--accent); }
.toast.fade { opacity: 0; transform: translateY(8px); transition: .3s; }
@keyframes slideIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
@keyframes pop { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.shell { grid-template-columns: 1fr; }
.sidebar { position: static; height: auto; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 12px; }
.nav { flex-direction: row; flex-wrap: wrap; flex: 1; }
.sidebar-foot { margin-top: 0; flex-direction: row; gap: 14px; }
.grid { grid-template-columns: 1fr; }
.feed-card { grid-column: 1; grid-row: auto; }
.kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.main { padding: 16px 14px 32px; }
.topbar h1 { font-size: 19px; }
.topbar-right { width: 100%; justify-content: space-between; }
.kpis { grid-template-columns: repeat(2, 1fr); gap: 11px; }
.kpi-val { font-size: 22px; }
.op-meta { display: none; }
.nav-item { padding: 8px 10px; font-size: 13px; }
.chart { height: 140px; }
.card { padding: 15px 15px; }
.toast-wrap { left: 12px; right: 12px; bottom: 12px; }
.toast { min-width: 0; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var DATA = {
"24h": {
kpis: { volume: "€4.82M", users: "38,204", fraud: "17", kyc: null },
deltas: { volume: ["up", "▲ 6.4%"], users: ["up", "▲ 2.1%"], fraud: ["down", "▼ 12%"] },
range: "last 24 hours",
chart: gen(24, 18, 6),
},
"7d": {
kpis: { volume: "€31.6M", users: "41,070", fraud: "104", kyc: null },
deltas: { volume: ["up", "▲ 9.1%"], users: ["up", "▲ 3.8%"], fraud: ["down", "▼ 4%"] },
range: "last 7 days",
chart: gen(14, 26, 9),
},
"30d": {
kpis: { volume: "€138.4M", users: "44,915", fraud: "471", kyc: null },
deltas: { volume: ["up", "▲ 12.7%"], users: ["up", "▲ 5.2%"], fraud: ["bad", "▲ 8%"] },
range: "last 30 days",
chart: gen(15, 30, 11),
},
};
function gen(n, base, spread) {
var out = [];
for (var i = 0; i < n; i++) {
var c = base + Math.round(Math.abs(Math.sin(i * 1.3) * spread) + Math.random() * spread);
var d = Math.round(c * (0.45 + Math.random() * 0.35));
out.push({ credit: c, debit: d });
}
return out;
}
var FRAUD = [
{ id: "FR-9281", sev: "high", title: "Velocity rule triggered", who: "Card •••• 4242", amt: "€2,450.00", note: "6 txns / 4 min · Lisbon" },
{ id: "FR-9277", sev: "high", title: "Impossible travel", who: "acct ••• 8841", amt: "€980.00", note: "Berlin → São Paulo · 14 min" },
{ id: "FR-9270", sev: "med", title: "New device + large transfer", who: "IBAN ••• 3390", amt: "€5,200.00", note: "unrecognized device" },
{ id: "FR-9264", sev: "med", title: "Merchant blacklist match", who: "Card •••• 7781", amt: "€149.99", note: "QuickPay Online" },
{ id: "FR-9258", sev: "low", title: "Address mismatch", who: "acct ••• 1102", amt: "€62.40", note: "AVS partial fail" },
];
var AVCOLORS = ["#3b6ef6", "#0fb5a6", "#7c5cff", "#d9982b", "#d4493e", "#1f9d62"];
var KYC = [
{ id: "KY-4410", name: "Mária Kovács", type: "Individual", risk: "low", time: "12 min ago" },
{ id: "KY-4408", name: "Helios Trading Ltd", type: "Business", risk: "high", time: "27 min ago" },
{ id: "KY-4405", name: "Daniel Okonkwo", type: "Individual", risk: "med", time: "41 min ago" },
{ id: "KY-4401", name: "Yuki Tanaka", type: "Individual", risk: "low", time: "1 h ago" },
{ id: "KY-4399", name: "Cedar & Vale GmbH", type: "Business", risk: "med", time: "2 h ago" },
{ id: "KY-4396", name: "Omar Haddad", type: "Individual", risk: "high", time: "3 h ago" },
];
var DISPUTES = [
{ id: "DSP-7720", merchant: "Skyline Airlines", amt: "€489.00", status: "pending" },
{ id: "DSP-7714", merchant: "NovaStream Media", amt: "€14.99", status: "review" },
{ id: "DSP-7709", merchant: "Harbor Electronics", amt: "€1,299.00", status: "cleared" },
{ id: "DSP-7702", merchant: "QuickPay Online", amt: "€149.99", status: "failed" },
{ id: "DSP-7698", merchant: "Aurora Hotels", amt: "€320.50", status: "review" },
];
/* ---------- Helpers ---------- */
function $(s, r) { return (r || document).querySelector(s); }
function el(tag, cls, html) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (html != null) e.innerHTML = html;
return e;
}
function initials(name) {
return name.split(/\s+/).slice(0, 2).map(function (w) { return w[0]; }).join("").toUpperCase();
}
function colorFor(s) {
var h = 0; for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
return AVCOLORS[h % AVCOLORS.length];
}
/* ---------- Toast ---------- */
var TW = $("#toast-wrap");
function toast(msg, kind) {
kind = kind || "info";
var icons = { ok: "✓", bad: "✕", info: "i" };
var t = el("div", "toast " + kind);
t.appendChild(el("span", "t-ico", icons[kind] || "i"));
t.appendChild(el("span", null, msg));
TW.appendChild(t);
setTimeout(function () {
t.classList.add("fade");
setTimeout(function () { t.remove(); }, 320);
}, 2600);
}
/* ---------- Clock ---------- */
var clock = $("#clock");
function tick() {
var d = new Date();
clock.textContent = d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", second: "2-digit" }) + " UTC";
}
tick(); setInterval(tick, 1000);
/* ---------- KPIs + chart by timeframe ---------- */
function renderKpis(tf) {
var d = DATA[tf];
setText('[data-kpi="volume"]', d.kpis.volume);
setText('[data-kpi="users"]', d.kpis.users);
setText('[data-kpi="fraud"]', d.kpis.fraud);
["volume", "users", "fraud"].forEach(function (k) {
var node = $('[data-delta="' + k + '"]');
if (node && d.deltas[k]) {
node.className = "delta " + d.deltas[k][0];
node.textContent = d.deltas[k][1];
}
});
}
function setText(sel, v) { var n = $(sel); if (n) n.textContent = v; }
function renderChart(tf) {
var d = DATA[tf];
var wrap = $("#chart");
wrap.innerHTML = "";
var max = 0, peakI = 0;
d.chart.forEach(function (p, i) {
var tot = p.credit + p.debit;
if (tot > max) { max = tot; peakI = i; }
});
d.chart.forEach(function (p, i) {
var col = el("div", "bar-col");
var tot = p.credit + p.debit;
var cH = Math.max(4, Math.round((p.credit / max) * 150));
var dH = Math.max(3, Math.round((p.debit / max) * 150));
var bc = el("div", "bar credit");
bc.style.height = "0px"; bc.setAttribute("data-label", "C €" + p.credit + "k");
var bd = el("div", "bar debit");
bd.style.height = "0px"; bd.setAttribute("data-label", "D €" + p.debit + "k");
col.appendChild(bc); col.appendChild(bd);
wrap.appendChild(col);
requestAnimationFrame(function () {
setTimeout(function () { bc.style.height = cH + "px"; bd.style.height = dH + "px"; }, i * 22);
});
});
$("#chart-range").textContent = d.range;
$("#chart-peak").textContent = "Peak €" + (d.chart[peakI].credit + d.chart[peakI].debit) + "k";
}
/* ---------- Fraud feed ---------- */
function renderFraud() {
var feed = $("#fraud-feed");
feed.innerHTML = "";
if (!FRAUD.length) {
feed.appendChild(el("div", "feed-empty", "✓ No open alerts — queue clear"));
$("#fraud-count").textContent = "0 open";
return;
}
FRAUD.forEach(function (a) {
var li = el("li", "alert sev-" + a.sev);
li.dataset.id = a.id;
li.innerHTML =
'<div class="al-body">' +
'<div class="al-top"><span class="sev-tag">' + a.sev + '</span>' +
'<span class="al-title">' + a.title + '</span></div>' +
'<div class="al-meta"><b>' + a.who + '</b> · <span class="al-amt">' + a.amt + '</span> · ' + a.note + '</div>' +
'</div>';
var btn = el("button", "al-dismiss", "✕");
btn.setAttribute("aria-label", "Dismiss alert " + a.id);
btn.addEventListener("click", function () { dismissAlert(a.id, li); });
li.appendChild(btn);
feed.appendChild(li);
});
$("#fraud-count").textContent = FRAUD.length + " open";
}
function dismissAlert(id, li) {
li.classList.add("dismissing");
setTimeout(function () {
var idx = FRAUD.findIndex(function (x) { return x.id === id; });
if (idx > -1) FRAUD.splice(idx, 1);
renderFraud();
}, 280);
toast("Alert " + id + " dismissed", "info");
}
/* ---------- KYC queue ---------- */
function renderKyc() {
var body = $("#kyc-body");
body.innerHTML = "";
KYC.forEach(function (k) {
var tr = document.createElement("tr");
tr.dataset.id = k.id;
var c = colorFor(k.name);
var riskLabel = { low: "Low", med: "Medium", high: "High" }[k.risk];
tr.innerHTML =
'<td><div class="applicant"><span class="app-av" style="background:' + c + '">' + initials(k.name) + '</span>' +
'<span><span class="app-name">' + k.name + '</span><br><span class="app-id">' + k.id + '</span></span></div></td>' +
'<td>' + k.type + '</td>' +
'<td><span class="risk ' + k.risk + '">' + riskLabel + '</span></td>' +
'<td style="color:var(--muted)">' + k.time + '</td>';
var act = document.createElement("td");
act.className = "ta-r";
var box = el("div", "kyc-actions");
var ap = el("button", "btn-s btn-approve", "Approve");
var rj = el("button", "btn-s btn-reject", "Reject");
ap.addEventListener("click", function () { resolveKyc(k.id, tr, true); });
rj.addEventListener("click", function () { resolveKyc(k.id, tr, false); });
box.appendChild(ap); box.appendChild(rj);
act.appendChild(box);
tr.appendChild(act);
body.appendChild(tr);
});
updateKycPill();
}
function resolveKyc(id, tr, approved) {
tr.classList.add("removing");
setTimeout(function () {
var idx = KYC.findIndex(function (x) { return x.id === id; });
if (idx > -1) KYC.splice(idx, 1);
renderKyc();
if (!KYC.length) {
$("#kyc-body").innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:24px">✓ Queue cleared — no applicants pending</td></tr>';
}
}, 250);
toast(id + (approved ? " approved — account activated" : " rejected — applicant notified"), approved ? "ok" : "bad");
}
function updateKycPill() {
$("#kyc-pill").textContent = KYC.length + " pending";
setText('[data-kpi="kyc"]', String(KYC.length));
}
/* ---------- Disputes ---------- */
function renderDisputes() {
var body = $("#dispute-body");
body.innerHTML = "";
var label = { pending: "Pending", review: "In review", cleared: "Cleared", failed: "Failed" };
DISPUTES.forEach(function (d) {
var tr = document.createElement("tr");
tr.innerHTML =
'<td style="font-weight:600">' + d.id + '</td>' +
'<td>' + d.merchant + '</td>' +
'<td class="ta-r"><span class="amt-debit">' + d.amt + '</span></td>' +
'<td><span class="pill ' + d.status + '">' + label[d.status] + '</span></td>';
body.appendChild(tr);
});
}
/* ---------- Timeframe toggle ---------- */
var tfBtns = document.querySelectorAll(".tf-btn");
tfBtns.forEach(function (b) {
b.addEventListener("click", function () {
tfBtns.forEach(function (x) { x.classList.remove("is-active"); });
b.classList.add("is-active");
var tf = b.dataset.tf;
renderKpis(tf);
renderChart(tf);
toast("Timeframe → " + b.textContent, "info");
});
});
/* ---------- Nav (cosmetic active state) ---------- */
document.querySelectorAll(".nav-item").forEach(function (n) {
n.addEventListener("click", function (e) {
e.preventDefault();
document.querySelectorAll(".nav-item").forEach(function (x) {
x.classList.remove("is-active"); x.removeAttribute("aria-current");
});
n.classList.add("is-active"); n.setAttribute("aria-current", "page");
});
});
/* ---------- Boot ---------- */
renderKpis("24h");
renderChart("24h");
renderFraud();
renderKyc();
renderDisputes();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northgate Bank — Ops Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Primary navigation">
<div class="brand">
<div class="brand-mark" aria-hidden="true">N</div>
<div class="brand-name">
<strong>Northgate</strong>
<span>Operations</span>
</div>
</div>
<nav class="nav">
<a href="#" class="nav-item is-active" aria-current="page"><span class="ni-dot"></span>Overview</a>
<a href="#" class="nav-item"><span class="ni-dot"></span>Transactions</a>
<a href="#" class="nav-item"><span class="ni-dot"></span>Fraud</a>
<a href="#" class="nav-item"><span class="ni-dot"></span>KYC Queue</a>
<a href="#" class="nav-item"><span class="ni-dot"></span>Disputes</a>
<a href="#" class="nav-item"><span class="ni-dot"></span>Reports</a>
</nav>
<div class="sidebar-foot">
<div class="status-line"><span class="dot-ok"></span>All systems nominal</div>
<div class="env-tag">PROD · region eu-west</div>
</div>
</aside>
<!-- Main -->
<main class="main">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-left">
<h1>Operations Overview</h1>
<p class="sub">Live monitoring · <span id="clock" aria-live="off">—</span></p>
</div>
<div class="topbar-right">
<div class="timeframe" role="group" aria-label="Timeframe">
<button class="tf-btn is-active" data-tf="24h">24h</button>
<button class="tf-btn" data-tf="7d">7d</button>
<button class="tf-btn" data-tf="30d">30d</button>
</div>
<div class="op-user" title="Signed in operator">
<span class="op-avatar">RV</span>
<span class="op-meta"><strong>R. Valdés</strong><small><span class="lock" aria-hidden="true">🔒</span> 2FA verified</small></span>
</div>
</div>
</header>
<!-- KPIs -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Transaction volume</span><span class="kpi-ico tone-blue">≈</span></div>
<div class="kpi-val" data-kpi="volume">€4.82M</div>
<div class="kpi-foot"><span class="delta up" data-delta="volume">▲ 6.4%</span> vs prev</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Active users</span><span class="kpi-ico tone-teal">◉</span></div>
<div class="kpi-val" data-kpi="users">38,204</div>
<div class="kpi-foot"><span class="delta up" data-delta="users">▲ 2.1%</span> vs prev</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">Fraud flags</span><span class="kpi-ico tone-danger">!</span></div>
<div class="kpi-val" data-kpi="fraud">17</div>
<div class="kpi-foot"><span class="delta down" data-delta="fraud">▼ 12%</span> vs prev</div>
</article>
<article class="kpi">
<div class="kpi-top"><span class="kpi-label">KYC queue</span><span class="kpi-ico tone-violet">⧗</span></div>
<div class="kpi-val" data-kpi="kyc">6</div>
<div class="kpi-foot"><span class="muted-foot" data-delta="kyc">awaiting review</span></div>
</article>
</section>
<!-- Grid -->
<section class="grid">
<!-- Volume chart -->
<article class="card chart-card">
<div class="card-head">
<h2>Transaction volume</h2>
<span class="chip" id="chart-range">last 24 hours</span>
</div>
<div class="chart" id="chart" role="img" aria-label="Bar chart of transaction volume over time"></div>
<div class="chart-legend">
<span><i class="lg lg-credit"></i> Credits</span>
<span><i class="lg lg-debit"></i> Debits</span>
<span class="peak" id="chart-peak">Peak —</span>
</div>
</article>
<!-- Fraud feed -->
<article class="card feed-card">
<div class="card-head">
<h2>Fraud alerts</h2>
<span class="count-pill" id="fraud-count">5 open</span>
</div>
<ul class="feed" id="fraud-feed" aria-live="polite"></ul>
</article>
<!-- KYC queue -->
<article class="card kyc-card">
<div class="card-head">
<h2>KYC approval queue</h2>
<span class="count-pill" id="kyc-pill">6 pending</span>
</div>
<div class="table-wrap">
<table class="tbl kyc-tbl">
<thead>
<tr><th>Applicant</th><th>Type</th><th>Risk</th><th>Submitted</th><th class="ta-r">Action</th></tr>
</thead>
<tbody id="kyc-body"></tbody>
</table>
</div>
</article>
<!-- Disputes -->
<article class="card disputes-card">
<div class="card-head">
<h2>Recent disputes</h2>
<span class="chip">chargebacks</span>
</div>
<div class="table-wrap">
<table class="tbl">
<thead>
<tr><th>Case</th><th>Merchant</th><th class="ta-r">Amount</th><th>Status</th></tr>
</thead>
<tbody id="dispute-body"></tbody>
</table>
</div>
</article>
</section>
</main>
</div>
<div class="toast-wrap" id="toast-wrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Ops Dashboard
A trust-first internal console for a fictional retail bank. The shell pairs a deep-navy sidebar (brand, sectioned navigation, environment tag, and a live “all systems nominal” indicator) with a calm, dense main surface. Four KPI cards lead the view — transaction volume, active users, fraud flags, and the KYC queue depth — each with a tinted icon, a colored delta, and tabular figures so amounts stay aligned. A topbar shows the signed-in operator with a 2FA-verified cue and a running UTC clock.
Below, a two-column grid holds a stacked credit/debit volume chart with animated bars and hover tooltips, a severity-coded fraud-alert feed, a KYC approval queue with risk pills and applicant avatars, and a recent-disputes table using clear status pills (pending, in review, cleared, failed). Everything is keyboard-usable, AA-contrast, and responsive down to ~360px, collapsing the sidebar into a horizontal bar and reflowing the grid into a single column.
The interactions are real: the 24h / 7d / 30d toggle re-renders the KPIs, deltas, chart, and
peak readout; approving or rejecting a KYC row animates it out, updates the pending count and
KPI, and fires a confirmation toast; dismissing a fraud alert slides it away and recomputes the
open count, showing an empty state when the queue clears. No frameworks, no build step — just a
small toast() helper and plain DOM rendering.
Illustrative UI only — not real banking software or financial advice.