Banking — Disputes Queue
A trust-first chargeback and disputes console for bank operators, with a navy header carrying a 2FA-verified session badge, summary stats for open cases, at-risk aging, escalations and total exposure, and a dense table of disputes showing case IDs, masked card numbers, customers, reason codes, tabular-figure amounts, age and status pills. Filter by status, search across cases, and open a sliding detail drawer with evidence, an activity timeline and accept, reject or escalate actions that transition status live.
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 6px 18px rgba(14, 27, 58, 0.10), 0 2px 6px rgba(14, 27, 58, 0.06);
--sh-3: 0 18px 50px rgba(14, 27, 58, 0.22);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 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;
}
.num, td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
button { font-family: inherit; cursor: pointer; }
h1 { font-size: 20px; font-weight: 800; letter-spacing: -0.02em; margin: 0; }
/* ---------- Topbar ---------- */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 24px;
background: linear-gradient(120deg, var(--navy), var(--navy-2));
color: #fff;
}
.brand { display: flex; align-items: center; gap: 12px; }
.logo {
width: 38px; height: 38px;
display: grid; place-items: center;
border-radius: 11px;
background: rgba(255, 255, 255, 0.10);
color: #cfe0ff;
border: 1px solid rgba(255, 255, 255, 0.14);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.25; }
.brand-text strong { font-size: 15px; font-weight: 700; }
.brand-text span { font-size: 12px; color: #aab8d8; }
.topbar-right { display: flex; align-items: center; gap: 16px; }
.verified {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12px; font-weight: 600; color: #b6f0e3;
background: rgba(15, 181, 166, 0.16);
border: 1px solid rgba(15, 181, 166, 0.3);
padding: 5px 10px; border-radius: 999px;
}
.op { display: flex; align-items: center; gap: 9px; }
.op-avatar {
width: 32px; height: 32px; border-radius: 50%;
display: grid; place-items: center;
background: linear-gradient(140deg, var(--accent), var(--violet));
font-size: 12px; font-weight: 700; color: #fff;
}
.op-name { font-size: 13px; font-weight: 600; }
/* ---------- Layout ---------- */
.layout { padding: 22px; max-width: 1180px; margin: 0 auto; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
overflow: hidden;
}
.panel-head {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap;
padding: 20px 22px 14px;
}
.sub { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
.search {
position: relative; display: flex; align-items: center;
min-width: 280px;
}
.search svg { position: absolute; left: 12px; color: var(--muted); }
.search input {
width: 100%;
font: inherit; font-size: 13.5px;
padding: 10px 12px 10px 36px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--bg);
color: var(--ink);
}
.search input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-50);
background: #fff;
}
/* ---------- Stats ---------- */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 6px 22px 16px;
}
.stat {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 14px;
background: linear-gradient(180deg, #fff, #fbfcff);
}
.stat .k { font-size: 11.5px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.stat .v { font-size: 22px; font-weight: 800; letter-spacing: -0.02em; margin-top: 3px; }
.stat .v small { font-size: 13px; font-weight: 600; color: var(--muted); }
.stat.alert .v { color: var(--danger); }
.stat.money .v { color: var(--navy); }
/* ---------- Filters ---------- */
.filters {
display: flex; gap: 8px; flex-wrap: wrap;
padding: 0 22px 14px;
}
.chip {
border: 1px solid var(--line-2);
background: #fff;
color: var(--ink-2);
font-size: 13px; font-weight: 600;
padding: 7px 14px;
border-radius: 999px;
transition: all 0.14s ease;
}
.chip:hover { border-color: var(--accent); color: var(--accent-d); }
.chip.is-active {
background: var(--navy); border-color: var(--navy); color: #fff;
}
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* ---------- Table ---------- */
.table-wrap { overflow-x: auto; border-top: 1px solid var(--line); }
.table-wrap:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.grid { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.grid thead th {
text-align: left;
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
color: var(--muted);
padding: 11px 16px;
background: #fafbff;
border-bottom: 1px solid var(--line);
white-space: nowrap;
}
.grid tbody tr {
border-bottom: 1px solid var(--line);
cursor: pointer;
transition: background 0.12s ease;
}
.grid tbody tr:hover { background: var(--accent-50); }
.grid tbody tr:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.grid tbody tr:last-child { border-bottom: none; }
.grid td { padding: 13px 16px; vertical-align: middle; }
.case-id { font-weight: 700; color: var(--navy); font-size: 13px; }
.case-card { display: block; font-size: 11.5px; color: var(--muted); margin-top: 2px; }
.cust { display: flex; align-items: center; gap: 9px; }
.cust .av {
width: 30px; height: 30px; border-radius: 50%; flex: none;
display: grid; place-items: center; font-size: 11px; font-weight: 700;
color: #fff; background: linear-gradient(140deg, var(--accent), var(--teal));
}
.cust .nm { font-weight: 600; }
.cust .nm small { display: block; color: var(--muted); font-weight: 500; font-size: 11.5px; }
.reason { color: var(--ink-2); }
.reason .code { display: block; font-size: 11px; color: var(--muted); font-weight: 600; }
.amt { font-weight: 700; color: var(--debit); white-space: nowrap; }
.age { color: var(--ink-2); white-space: nowrap; }
.age.old { color: var(--danger); font-weight: 600; }
.empty { padding: 40px 16px; text-align: center; color: var(--muted); font-size: 14px; }
/* ---------- Status pill ---------- */
.pill {
display: inline-flex; align-items: center; gap: 6px;
font-size: 11.5px; font-weight: 700;
padding: 4px 10px; border-radius: 999px;
white-space: nowrap;
}
.pill::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.pill.new { color: var(--accent-d); background: var(--accent-50); }
.pill.review { color: #8a6a17; background: #fbf2dc; }
.pill.escalated { color: #9a3329; background: #fbe6e3; }
.pill.resolved { color: #14613f; background: #e3f5ec; }
/* ---------- Drawer ---------- */
.scrim {
position: fixed; inset: 0;
background: rgba(14, 23, 38, 0.46);
backdrop-filter: blur(2px);
z-index: 40;
opacity: 0; animation: fade 0.18s ease forwards;
}
@keyframes fade { to { opacity: 1; } }
.drawer {
position: fixed; top: 0; right: 0; bottom: 0;
width: min(440px, 92vw);
background: var(--surface);
box-shadow: var(--sh-3);
z-index: 50;
transform: translateX(100%);
transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
display: flex; flex-direction: column;
overflow-y: auto;
}
.drawer.open { transform: translateX(0); }
.drawer-inner { display: flex; flex-direction: column; min-height: 100%; }
.dr-head {
padding: 18px 20px;
background: linear-gradient(120deg, var(--navy), var(--navy-2));
color: #fff;
position: sticky; top: 0; z-index: 2;
}
.dr-head .row1 { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.dr-close {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #fff; border-radius: 8px;
width: 32px; height: 32px; flex: none;
display: grid; place-items: center;
}
.dr-close:hover { background: rgba(255, 255, 255, 0.22); }
.dr-case { font-size: 12px; color: #aab8d8; font-weight: 600; }
.dr-amount { font-size: 30px; font-weight: 800; letter-spacing: -0.02em; margin-top: 6px; }
.dr-merchant { font-size: 13px; color: #c9d5f0; margin-top: 2px; }
.dr-body { padding: 18px 20px; flex: 1; }
.dr-section + .dr-section { margin-top: 18px; }
.dr-section h3 {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--muted); margin: 0 0 9px; font-weight: 700;
}
.kv { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; font-size: 13px; }
.kv dt { color: var(--muted); }
.kv dd { margin: 0; font-weight: 600; text-align: right; }
.card-mask {
display: inline-flex; align-items: center; gap: 7px;
font-weight: 600;
}
.card-mask svg { color: var(--muted); }
.evidence { display: flex; flex-direction: column; gap: 8px; }
.ev {
display: flex; align-items: center; gap: 10px;
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 12px;
font-size: 13px;
}
.ev .ic {
width: 30px; height: 30px; border-radius: 8px; flex: none;
display: grid; place-items: center;
background: var(--accent-50); color: var(--accent-d);
}
.ev .meta { flex: 1; }
.ev .meta b { font-weight: 600; }
.ev .meta small { display: block; color: var(--muted); font-size: 11.5px; }
.ev .ok { color: var(--ok); font-size: 12px; font-weight: 700; }
.timeline { list-style: none; margin: 0; padding: 0 0 0 4px; }
.timeline li {
position: relative; padding: 0 0 14px 18px;
font-size: 12.5px; color: var(--ink-2);
}
.timeline li::before {
content: ""; position: absolute; left: 0; top: 4px;
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent); box-shadow: 0 0 0 3px var(--accent-50);
}
.timeline li:not(:last-child)::after {
content: ""; position: absolute; left: 3.5px; top: 12px; bottom: 0;
width: 1.5px; background: var(--line-2);
}
.timeline b { color: var(--ink); display: block; font-weight: 600; }
.timeline time { color: var(--muted); font-size: 11.5px; }
.dr-foot {
position: sticky; bottom: 0;
padding: 14px 20px;
background: var(--surface);
border-top: 1px solid var(--line);
display: grid; grid-template-columns: 1fr 1fr; gap: 9px;
}
.dr-foot .full { grid-column: 1 / -1; }
.btn {
border-radius: var(--r-sm);
font-size: 13px; font-weight: 700;
padding: 11px 12px;
border: 1px solid transparent;
display: inline-flex; align-items: center; justify-content: center; gap: 7px;
transition: all 0.14s ease;
}
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-accept { background: var(--ok); color: #fff; }
.btn-accept:hover { filter: brightness(1.06); }
.btn-reject { background: #fff; color: var(--danger); border-color: rgba(212, 73, 62, 0.4); }
.btn-reject:hover { background: #fceeed; }
.btn-escalate { background: var(--navy); color: #fff; }
.btn-escalate:hover { background: var(--navy-2); }
.btn[disabled] { opacity: 0.5; cursor: not-allowed; }
.dr-resolved {
margin: 0 20px 16px; padding: 14px;
border: 1px dashed var(--line-2); border-radius: var(--r-md);
background: #f8faff; font-size: 13px; color: var(--ink-2);
display: flex; align-items: center; gap: 10px;
}
.dr-resolved svg { color: var(--ok); flex: none; }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; z-index: 80;
pointer-events: none;
}
.toast {
background: var(--ink); color: #fff;
padding: 11px 16px; border-radius: 999px;
font-size: 13px; font-weight: 600;
box-shadow: var(--sh-2);
display: flex; align-items: center; gap: 8px;
animation: toastIn 0.22s ease, toastOut 0.3s ease 2.4s forwards;
}
.toast .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--teal); }
.toast.warn .dot { background: var(--warn); }
.toast.danger .dot { background: var(--danger); }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }
@keyframes toastOut { to { opacity: 0; transform: translateY(8px); } }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.topbar { padding: 12px 14px; }
.op-name, .brand-text span { display: none; }
.verified { padding: 5px 8px; }
.layout { padding: 12px; }
.panel-head { padding: 16px 14px 10px; }
.stats, .filters { padding-left: 14px; padding-right: 14px; }
.search { min-width: 0; width: 100%; }
.grid td:nth-child(2) .nm small,
.grid thead th:nth-child(3),
.grid td:nth-child(3) { display: none; }
.grid td, .grid thead th { padding: 11px 10px; }
.dr-amount { font-size: 26px; }
}(function () {
"use strict";
var STATUS = {
new: { label: "New", cls: "new" },
review: { label: "Under review", cls: "review" },
escalated: { label: "Escalated", cls: "escalated" },
resolved: { label: "Resolved", cls: "resolved" },
};
var disputes = [
{
id: "DSP-4821", card: "4242", network: "Visa",
customer: "Marisol Tan", email: "m.tan@inboxly.io", initials: "MT",
merchant: "Nimbus Cloud Hosting", amount: 489.00,
reasonCode: "10.4", reason: "Fraud — card not present",
status: "new", ageH: 4, txnDate: "Jun 12, 2026", filed: "Jun 16, 09:12",
evidence: [
{ name: "Cardholder statement", note: "Signed declaration", icon: "doc", ok: true },
{ name: "IP / device log", note: "Mismatch flagged", icon: "shield", ok: false },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 16, 09:12" },
{ t: "Auto-assigned to fraud queue", d: "Jun 16, 09:12" },
],
},
{
id: "DSP-4818", card: "8830", network: "Mastercard",
customer: "Devon Okafor", email: "devon.o@maily.net", initials: "DO",
merchant: "Atlas Fitness Co.", amount: 79.99,
reasonCode: "13.2", reason: "Cancelled recurring",
status: "review", ageH: 31, txnDate: "Jun 09, 2026", filed: "Jun 15, 02:40",
evidence: [
{ name: "Cancellation email", note: "Sent May 28", icon: "mail", ok: true },
{ name: "Merchant rebuttal", note: "Awaiting upload", icon: "doc", ok: false },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 15, 02:40" },
{ t: "Evidence requested from merchant", d: "Jun 15, 08:00" },
{ t: "Moved to under review", d: "Jun 15, 10:22" },
],
},
{
id: "DSP-4805", card: "1109", network: "Visa",
customer: "Priya Raman", email: "priya@swiftmail.co", initials: "PR",
merchant: "Lumen Electronics", amount: 1240.00,
reasonCode: "13.3", reason: "Not as described",
status: "escalated", ageH: 76, txnDate: "Jun 04, 2026", filed: "Jun 13, 14:05",
evidence: [
{ name: "Product photos", note: "3 images", icon: "img", ok: true },
{ name: "Chat transcript", note: "Verified", icon: "doc", ok: true },
{ name: "Shipping proof", note: "Delivered", icon: "shield", ok: true },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 13, 14:05" },
{ t: "Merchant rebuttal received", d: "Jun 14, 11:30" },
{ t: "Escalated to arbitration", d: "Jun 15, 16:48" },
],
},
{
id: "DSP-4799", card: "4242", network: "Visa",
customer: "Sefa Kowalski", email: "sefa.k@postbox.eu", initials: "SK",
merchant: "Orbit Rideshare", amount: 28.40,
reasonCode: "12.5", reason: "Incorrect amount",
status: "new", ageH: 9, txnDate: "Jun 14, 2026", filed: "Jun 16, 04:31",
evidence: [
{ name: "Trip receipt", note: "Auto-pulled", icon: "doc", ok: true },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 16, 04:31" },
{ t: "Auto-assigned to disputes queue", d: "Jun 16, 04:31" },
],
},
{
id: "DSP-4790", card: "7715", network: "Amex",
customer: "Joana Beck", email: "j.beck@mailhub.io", initials: "JB",
merchant: "Harbor Books Ltd.", amount: 54.20,
reasonCode: "11.3", reason: "Duplicate charge",
status: "resolved", ageH: 120, txnDate: "Jun 02, 2026", filed: "Jun 11, 19:50",
resolution: "Accepted — provisional credit issued to cardholder.",
evidence: [
{ name: "Two matching auths", note: "Confirmed", icon: "doc", ok: true },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 11, 19:50" },
{ t: "Duplicate confirmed", d: "Jun 12, 09:14" },
{ t: "Accepted — credit issued", d: "Jun 12, 09:30" },
],
},
{
id: "DSP-4787", card: "8830", network: "Mastercard",
customer: "Tariq Mensah", email: "tariq.m@zmail.org", initials: "TM",
merchant: "Peak Travel Group", amount: 932.75,
reasonCode: "13.1", reason: "Services not provided",
status: "review", ageH: 52, txnDate: "Jun 06, 2026", filed: "Jun 14, 07:15",
evidence: [
{ name: "Booking confirmation", note: "Verified", icon: "doc", ok: true },
{ name: "No-show report", note: "Pending review", icon: "shield", ok: false },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 14, 07:15" },
{ t: "Moved to under review", d: "Jun 14, 12:00" },
],
},
{
id: "DSP-4771", card: "1109", network: "Visa",
customer: "Mei Lin", email: "mei.lin@boxmail.cc", initials: "ML",
merchant: "Glow Skincare", amount: 64.00,
reasonCode: "10.4", reason: "Fraud — card not present",
status: "resolved", ageH: 168, txnDate: "May 30, 2026", filed: "Jun 09, 13:22",
resolution: "Rejected — transaction verified with 3-D Secure.",
evidence: [
{ name: "3-D Secure auth", note: "Passed", icon: "shield", ok: true },
],
timeline: [
{ t: "Dispute filed by cardholder", d: "Jun 09, 13:22" },
{ t: "3-D Secure proof attached", d: "Jun 10, 08:00" },
{ t: "Rejected — liability shifted", d: "Jun 10, 15:40" },
],
},
];
var fmt = function (n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
var ageLabel = function (h) {
if (h < 24) return h + "h";
return Math.floor(h / 24) + "d " + (h % 24) + "h";
};
var ICONS = {
doc: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>',
shield: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 4 5v6c0 5 3.4 8.5 8 11 4.6-2.5 8-6 8-11V5z"/></svg>',
mail: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>',
img: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/></svg>',
};
var CHECK = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>';
var rowsEl = document.getElementById("rows");
var emptyEl = document.getElementById("empty");
var statsEl = document.getElementById("stats");
var countEl = document.getElementById("result-count");
var searchEl = document.getElementById("search");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip"));
var scrim = document.getElementById("scrim");
var drawer = document.getElementById("drawer");
var drawerInner = document.getElementById("drawer-inner");
var toastWrap = document.getElementById("toast-wrap");
var state = { status: "all", q: "", openId: null, lastFocus: null };
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.innerHTML = '<span class="dot"></span>' + esc(msg);
toastWrap.appendChild(el);
setTimeout(function () { el.remove(); }, 2900);
}
function renderStats() {
var open = disputes.filter(function (d) { return d.status !== "resolved"; });
var atRisk = open.filter(function (d) { return d.ageH >= 48; }).length;
var exposure = open.reduce(function (s, d) { return s + d.amount; }, 0);
var escalated = disputes.filter(function (d) { return d.status === "escalated"; }).length;
statsEl.innerHTML =
stat("Open cases", open.length, "") +
stat("At-risk >48h", atRisk, "alert") +
stat("Escalated", escalated, "") +
moneyStat("Exposure", fmt(exposure));
}
function stat(k, v, cls) {
return '<div class="stat ' + cls + '"><div class="k">' + k + '</div><div class="v">' + v + "</div></div>";
}
function moneyStat(k, v) {
return '<div class="stat money"><div class="k">' + k + '</div><div class="v num">' + v + "</div></div>";
}
function visible() {
var q = state.q.trim().toLowerCase();
return disputes.filter(function (d) {
if (state.status !== "all" && d.status !== state.status) return false;
if (!q) return true;
return (
d.id.toLowerCase().indexOf(q) !== -1 ||
d.customer.toLowerCase().indexOf(q) !== -1 ||
d.merchant.toLowerCase().indexOf(q) !== -1
);
});
}
function renderRows() {
var list = visible();
countEl.textContent = list.length + (list.length === 1 ? " dispute" : " disputes");
if (!list.length) {
rowsEl.innerHTML = "";
emptyEl.hidden = false;
return;
}
emptyEl.hidden = true;
rowsEl.innerHTML = list.map(function (d) {
var st = STATUS[d.status];
var old = d.status !== "resolved" && d.ageH >= 48;
return (
'<tr tabindex="0" data-id="' + d.id + '">' +
'<td><span class="case-id">' + d.id + '</span>' +
'<span class="case-card">•••• ' + d.card + " · " + d.network + "</span></td>" +
'<td><div class="cust"><span class="av">' + d.initials + '</span>' +
'<span class="nm">' + esc(d.customer) + "<small>" + esc(d.email) + "</small></span></div></td>" +
'<td><div class="reason">' + esc(d.reason) + '<span class="code">Code ' + d.reasonCode + " · " + esc(d.merchant) + "</span></div></td>" +
'<td class="num"><span class="amt">' + fmt(d.amount) + "</span></td>" +
'<td><span class="age' + (old ? " old" : "") + '">' + ageLabel(d.ageH) + "</span></td>" +
'<td><span class="pill ' + st.cls + '">' + st.label + "</span></td>" +
"</tr>"
);
}).join("");
}
function drawerHTML(d) {
var st = STATUS[d.status];
var ev = d.evidence.map(function (e) {
return (
'<div class="ev"><span class="ic">' + ICONS[e.icon] + "</span>" +
'<span class="meta"><b>' + esc(e.name) + "</b><small>" + esc(e.note) + "</small></span>" +
(e.ok ? '<span class="ok">' + CHECK + " ok</span>" : '<span style="color:var(--muted);font-size:11.5px;font-weight:600">pending</span>') +
"</div>"
);
}).join("");
var tl = d.timeline.map(function (t) {
return "<li><b>" + esc(t.t) + "</b><time>" + esc(t.d) + "</time></li>";
}).join("");
var foot;
if (d.status === "resolved") {
foot =
'<div class="dr-resolved">' +
'<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8.5 12 2.5 2.5 4.5-5"/></svg>' +
"<span>" + esc(d.resolution || "Case closed.") + "</span></div>";
} else {
foot =
'<div class="dr-foot">' +
'<button class="btn btn-accept" data-act="accept">' + CHECK + " Accept</button>" +
'<button class="btn btn-reject" data-act="reject">Reject</button>' +
'<button class="btn btn-escalate full" data-act="escalate"' + (d.status === "escalated" ? " disabled" : "") + ">" +
(d.status === "escalated" ? "Already escalated" : "Escalate to arbitration") + "</button>" +
"</div>";
}
return (
'<div class="dr-head"><div class="row1"><div>' +
'<span class="dr-case">' + d.id + " · " + esc(d.network) + " •••• " + d.card + "</span>" +
'<div class="dr-amount num">' + fmt(d.amount) + "</div>" +
'<div class="dr-merchant">' + esc(d.merchant) + "</div></div>" +
'<button class="dr-close" data-act="close" aria-label="Close detail">' +
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>' +
'</div><div style="margin-top:10px"><span class="pill ' + st.cls + '">' + st.label + "</span></div></div>" +
'<div class="dr-body">' +
'<div class="dr-section"><h3>Case details</h3><dl class="kv">' +
"<dt>Customer</dt><dd>" + esc(d.customer) + "</dd>" +
"<dt>Reason</dt><dd>" + esc(d.reason) + "</dd>" +
"<dt>Reason code</dt><dd>" + d.reasonCode + "</dd>" +
'<dt>Card</dt><dd><span class="card-mask">' +
'<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><path d="M7 11V8a5 5 0 0 1 10 0v3"/></svg>' +
"•••• " + d.card + "</span></dd>" +
"<dt>Transaction</dt><dd>" + esc(d.txnDate) + "</dd>" +
"<dt>Filed</dt><dd>" + esc(d.filed) + "</dd>" +
"<dt>Age</dt><dd>" + ageLabel(d.ageH) + "</dd>" +
"</dl></div>" +
'<div class="dr-section"><h3>Evidence</h3><div class="evidence">' + ev + "</div></div>" +
'<div class="dr-section"><h3>Activity</h3><ul class="timeline">' + tl + "</ul></div>" +
"</div>" + foot
);
}
function openDrawer(id) {
var d = disputes.filter(function (x) { return x.id === id; })[0];
if (!d) return;
state.openId = id;
state.lastFocus = document.activeElement;
drawerInner.innerHTML = drawerHTML(d);
scrim.hidden = false;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
var close = drawer.querySelector(".dr-close");
if (close) close.focus();
}
function closeDrawer() {
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
scrim.hidden = true;
document.body.style.overflow = "";
state.openId = null;
if (state.lastFocus && state.lastFocus.focus) state.lastFocus.focus();
}
function transition(act) {
var d = disputes.filter(function (x) { return x.id === state.openId; })[0];
if (!d) return;
var stamp = "Jun 16, " + ("0" + new Date().getHours()).slice(-2) + ":" + ("0" + new Date().getMinutes()).slice(-2);
if (act === "accept") {
d.status = "resolved";
d.resolution = "Accepted — provisional credit of " + fmt(d.amount) + " issued to cardholder.";
d.timeline.push({ t: "Accepted by " + opName() + " — credit issued", d: stamp });
toast(d.id + " accepted · credit issued", "");
} else if (act === "reject") {
d.status = "resolved";
d.resolution = "Rejected — charge upheld and merchant evidence accepted.";
d.timeline.push({ t: "Rejected by " + opName(), d: stamp });
toast(d.id + " rejected · charge upheld", "danger");
} else if (act === "escalate") {
if (d.status === "escalated") return;
d.status = "escalated";
d.timeline.push({ t: "Escalated to arbitration by " + opName(), d: stamp });
toast(d.id + " escalated to arbitration", "warn");
}
drawerInner.innerHTML = drawerHTML(d);
var close = drawer.querySelector(".dr-close");
if (close) close.focus();
renderStats();
renderRows();
}
function opName() { return "R. Velez"; }
// ---- events ----
rowsEl.addEventListener("click", function (e) {
var tr = e.target.closest("tr[data-id]");
if (tr) openDrawer(tr.getAttribute("data-id"));
});
rowsEl.addEventListener("keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
var tr = e.target.closest("tr[data-id]");
if (tr) { e.preventDefault(); openDrawer(tr.getAttribute("data-id")); }
});
drawer.addEventListener("click", function (e) {
var b = e.target.closest("[data-act]");
if (!b) return;
var act = b.getAttribute("data-act");
if (act === "close") closeDrawer();
else transition(act);
});
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && state.openId) closeDrawer();
});
chips.forEach(function (c) {
c.addEventListener("click", function () {
chips.forEach(function (x) {
x.classList.remove("is-active");
x.setAttribute("aria-selected", "false");
});
c.classList.add("is-active");
c.setAttribute("aria-selected", "true");
state.status = c.getAttribute("data-status");
renderRows();
});
});
var debounce;
searchEl.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(function () {
state.q = searchEl.value;
renderRows();
}, 120);
});
renderStats();
renderRows();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Banking — Disputes Queue</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="app">
<header class="topbar">
<div class="brand">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
</span>
<div class="brand-text">
<strong>Meridian Bank</strong>
<span>Risk & Disputes Console</span>
</div>
</div>
<div class="topbar-right">
<span class="verified" title="Operator session verified with 2FA">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
2FA verified
</span>
<div class="op" aria-label="Operator">
<span class="op-avatar" aria-hidden="true">RV</span>
<span class="op-name">Rina Velez</span>
</div>
</div>
</header>
<main class="layout">
<section class="panel" aria-labelledby="queue-title">
<div class="panel-head">
<div>
<h1 id="queue-title">Disputes Queue</h1>
<p class="sub" id="result-count">— disputes</p>
</div>
<div class="search">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="search" type="search" placeholder="Search case, customer or merchant…" aria-label="Search disputes" autocomplete="off" />
</div>
</div>
<div class="stats" id="stats" aria-label="Queue summary"></div>
<div class="filters" role="tablist" aria-label="Filter by status">
<button class="chip is-active" role="tab" aria-selected="true" data-status="all">All</button>
<button class="chip" role="tab" aria-selected="false" data-status="new">New</button>
<button class="chip" role="tab" aria-selected="false" data-status="review">Under review</button>
<button class="chip" role="tab" aria-selected="false" data-status="escalated">Escalated</button>
<button class="chip" role="tab" aria-selected="false" data-status="resolved">Resolved</button>
</div>
<div class="table-wrap" role="region" aria-label="Disputes table" tabindex="0">
<table class="grid">
<thead>
<tr>
<th scope="col">Case</th>
<th scope="col">Customer</th>
<th scope="col">Reason</th>
<th scope="col" class="num">Amount</th>
<th scope="col">Age</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
<p class="empty" id="empty" hidden>No disputes match your filters.</p>
</div>
</section>
</main>
</div>
<!-- Detail drawer -->
<div class="scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" aria-label="Dispute detail" aria-hidden="true">
<div class="drawer-inner" id="drawer-inner"><!-- injected --></div>
</aside>
<div class="toast-wrap" id="toast-wrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Disputes Queue
A calm, dense back-office console for the people who work chargebacks. A navy header carries the bank mark, a 2FA-verified session pill, and the operator’s avatar, while a row of summary stats tracks open cases, disputes aging past 48 hours, escalations, and the total exposure at risk in tabular figures. Below sits the queue itself: each row shows the case ID, a masked card and network (•••• 4242 · Visa), the customer with avatar and email, the reason and scheme reason code, the right-aligned disputed amount, the case age, and a color-coded status pill for new, under review, escalated, and resolved.
Status filter chips and a live search box narrow the table instantly — search matches case IDs, customers, and merchants. Clicking or pressing Enter on any row slides in a detail drawer from the right with a navy summary header, a key/value breakdown, an evidence checklist with verification cues, and a connected activity timeline.
The drawer’s resolve actions are genuinely stateful: accept issues a provisional credit, reject upholds the charge, and escalate sends the case to arbitration — each transition updates the status pill, appends a timestamped timeline entry, recalculates the summary stats, and fires a toast. The whole thing is vanilla JS, keyboard-usable with Escape-to-close and focus restoration, and responsive down to ~360px.
Illustrative UI only — not real banking software or financial advice.