Shop — Orders Manager
An operational orders console for a storefront back office. A sortable orders table lists ID, customer, total, payment and fulfillment status chips and date, with filter tabs for All, Unfulfilled, Paid and Refunded plus live search. Selecting a row slides out a detail panel showing line items, the customer block and a timeline. Fulfill, refund and print actions update the status badges and confirm with a toast, and a checkbox column drives bulk-fulfill across the visible selection.
MCP
Code
:root {
--bg: #ffffff;
--surface: #ffffff;
--canvas: #f6f7f9;
--ink: #16181d;
--muted: #6b7280;
--faint: #9aa1ad;
--brand: #3457ff;
--brand-d: #2742d6;
--brand-soft: #eef2ff;
--sale: #e0245e;
--ok: #1f9d55;
--ok-soft: #e7f6ee;
--warn: #b9760a;
--warn-soft: #fdf2dd;
--pend-soft: #eef1f5;
--line: rgba(16, 18, 29, .1);
--line-2: rgba(16, 18, 29, .06);
--shadow: 0 1px 2px rgba(16, 18, 29, .05), 0 8px 24px rgba(16, 18, 29, .06);
--shadow-lg: 0 12px 40px rgba(16, 18, 29, .18);
--radius: 14px;
--radius-sm: 9px;
--sidebar-w: 232px;
--panel-w: 420px;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink);
background: var(--canvas);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, input { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Layout shell ---------- */
.app {
display: grid;
grid-template-columns: var(--sidebar-w) 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.sidebar {
background: #fff;
border-right: 1px solid var(--line);
padding: 20px 16px;
display: flex;
flex-direction: column;
gap: 22px;
position: sticky;
top: 0;
height: 100vh;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 6px;
}
.brand-mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
background: linear-gradient(135deg, var(--brand), #6d8bff);
color: #fff;
font-size: 15px;
}
.brand-name { font-weight: 800; letter-spacing: -.01em; }
.nav { display: flex; flex-direction: column; gap: 3px; }
.nav-item {
display: flex;
align-items: center;
gap: 11px;
padding: 9px 11px;
border-radius: var(--radius-sm);
color: var(--muted);
font-weight: 600;
font-size: 14px;
transition: background .15s, color .15s;
}
.nav-item span { width: 16px; text-align: center; opacity: .85; }
.nav-item:hover { background: var(--canvas); color: var(--ink); }
.nav-item.is-active { background: var(--brand-soft); color: var(--brand-d); }
.nav-foot { margin-top: auto; }
.store-chip {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--canvas);
}
.store-dot {
width: 9px; height: 9px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px var(--ok-soft);
flex: none;
}
.store-meta { display: flex; flex-direction: column; line-height: 1.25; }
.store-meta strong { font-size: 13px; }
.store-meta span { font-size: 12px; color: var(--muted); }
/* ---------- Main ---------- */
.main {
padding: 24px 28px 40px;
min-width: 0;
}
.topbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.topbar-head h1 { margin: 0; font-size: 26px; letter-spacing: -.02em; }
.sub { margin: 3px 0 0; color: var(--muted); font-size: 14px; }
.topbar-actions { display: flex; align-items: center; gap: 10px; }
.search {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
border: 1px solid var(--line);
border-radius: 10px;
padding: 0 12px;
min-width: 270px;
box-shadow: var(--shadow);
transition: border-color .15s, box-shadow .15s;
}
.search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.search-ic { color: var(--faint); font-size: 16px; }
.search input {
border: 0;
background: transparent;
padding: 9px 0;
width: 100%;
outline: none;
}
.btn {
border: 1px solid transparent;
border-radius: 10px;
padding: 9px 15px;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background .15s, border-color .15s, transform .05s, box-shadow .15s;
white-space: nowrap;
}
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--brand); color: #fff; box-shadow: var(--shadow); }
.btn-primary:hover { background: var(--brand-d); }
.btn-ghost { background: #fff; border-color: var(--line); color: var(--ink); }
.btn-ghost:hover { background: var(--canvas); }
.btn-danger { background: #fff; border-color: rgba(224, 36, 94, .35); color: var(--sale); }
.btn-danger:hover { background: #fdeef3; }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-block { width: 100%; }
/* ---------- Stats ---------- */
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.stat {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px 16px;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.stat-val { font-size: 24px; letter-spacing: -.02em; }
.stat-delta { font-size: 12px; color: var(--faint); font-weight: 600; }
.stat-delta.up { color: var(--ok); }
.stat-delta.down { color: var(--sale); }
/* ---------- Toolbar / Tabs ---------- */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.tabs {
display: inline-flex;
background: #fff;
border: 1px solid var(--line);
border-radius: 12px;
padding: 4px;
gap: 2px;
box-shadow: var(--shadow);
flex-wrap: wrap;
}
.tab {
display: inline-flex;
align-items: center;
gap: 7px;
border: 0;
background: transparent;
border-radius: 9px;
padding: 8px 13px;
font-weight: 600;
font-size: 14px;
color: var(--muted);
cursor: pointer;
transition: background .15s, color .15s;
}
.tab:hover { color: var(--ink); }
.tab.is-active { background: var(--brand-soft); color: var(--brand-d); }
.tab-count {
display: inline-grid;
place-items: center;
min-width: 22px;
height: 20px;
padding: 0 6px;
border-radius: 999px;
background: var(--canvas);
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.tab.is-active .tab-count { background: #fff; color: var(--brand-d); }
/* ---------- Bulk bar ---------- */
.bulkbar {
display: flex;
align-items: center;
gap: 16px;
background: var(--ink);
color: #fff;
border-radius: 12px;
padding: 10px 16px;
margin-bottom: 12px;
box-shadow: var(--shadow-lg);
}
.bulkbar[hidden] { display: none; }
.bulk-count { font-size: 14px; }
.bulk-count strong { font-weight: 800; }
.bulk-total { color: rgba(255, 255, 255, .65); font-size: 13px; font-weight: 600; }
.bulk-actions { margin-left: auto; display: flex; gap: 8px; }
.bulkbar .btn-ghost { background: rgba(255, 255, 255, .08); border-color: rgba(255, 255, 255, .2); color: #fff; }
.bulkbar .btn-ghost:hover { background: rgba(255, 255, 255, .16); }
/* ---------- Table ---------- */
.table-wrap {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.orders {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.orders thead th {
text-align: left;
font-size: 12px;
letter-spacing: .03em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
background: #fcfcfd;
white-space: nowrap;
}
.orders th.num, .orders td.num { text-align: right; }
.sortable { cursor: pointer; user-select: none; }
.sortable:hover { color: var(--ink); }
.sortable[aria-sort]::after { content: " ↕"; opacity: .4; }
.sortable[aria-sort="ascending"]::after { content: " ↑"; opacity: 1; color: var(--brand); }
.sortable[aria-sort="descending"]::after { content: " ↓"; opacity: 1; color: var(--brand); }
.orders tbody tr {
cursor: pointer;
transition: background .12s;
}
.orders tbody tr:hover { background: var(--brand-soft); }
.orders tbody tr.is-selected { background: #f0f4ff; }
.orders tbody tr.is-open { box-shadow: inset 3px 0 0 var(--brand); background: #f0f4ff; }
.orders td {
padding: 13px 14px;
border-bottom: 1px solid var(--line-2);
vertical-align: middle;
}
.orders tbody tr:last-child td { border-bottom: 0; }
.col-check { width: 44px; }
.col-check input, .orders td .row-check { cursor: pointer; }
.order-id { font-weight: 700; }
.order-items { color: var(--faint); font-size: 12.5px; font-weight: 500; }
.cust-name { font-weight: 600; }
.cust-mail { color: var(--faint); font-size: 12.5px; }
.total-val { font-weight: 700; font-variant-numeric: tabular-nums; }
.date-val { color: var(--muted); white-space: nowrap; }
/* avatar */
.avatar {
display: inline-grid;
place-items: center;
width: 30px; height: 30px;
border-radius: 50%;
font-size: 12px;
font-weight: 700;
color: #fff;
flex: none;
}
.cust-cell { display: flex; align-items: center; gap: 10px; }
.cust-cell > div { min-width: 0; }
.cust-cell .cust-name, .cust-cell .cust-mail {
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px;
}
/* ---------- Badges ---------- */
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 9px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.badge::before {
content: "";
width: 6px; height: 6px; border-radius: 50%;
background: currentColor;
}
.b-paid { background: var(--ok-soft); color: #157a41; }
.b-pending { background: var(--pend-soft); color: #4b5563; }
.b-refunded { background: #fdeef3; color: #b81a4c; }
.b-fulfilled { background: var(--brand-soft); color: var(--brand-d); }
.b-unfulfilled { background: var(--warn-soft); color: var(--warn); }
.b-ful-refunded { background: #f0f0f3; color: #6b7280; }
.empty {
text-align: center;
color: var(--muted);
padding: 40px 16px;
margin: 0;
}
/* ---------- Detail panel ---------- */
.panel {
position: fixed;
top: 0; right: 0;
width: var(--panel-w);
max-width: 92vw;
height: 100vh;
background: #fff;
border-left: 1px solid var(--line);
box-shadow: var(--shadow-lg);
transform: translateX(105%);
transition: transform .28s cubic-bezier(.4, 0, .2, 1);
z-index: 40;
overflow-y: auto;
}
.panel.is-open { transform: translateX(0); }
.panel-inner { padding: 0 0 28px; }
.scrim {
position: fixed;
inset: 0;
background: rgba(16, 18, 29, .32);
z-index: 30;
opacity: 0;
animation: fade .2s forwards;
}
.scrim[hidden] { display: none; }
@keyframes fade { to { opacity: 1; } }
.panel-head {
position: sticky;
top: 0;
background: #fff;
border-bottom: 1px solid var(--line);
padding: 18px 22px 14px;
z-index: 2;
}
.panel-head-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.panel-head h2 { margin: 0; font-size: 19px; letter-spacing: -.01em; }
.panel-head .order-date { color: var(--muted); font-size: 13px; margin: 4px 0 0; }
.panel-badges { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.close-btn {
border: 1px solid var(--line);
background: #fff;
width: 34px; height: 34px;
border-radius: 9px;
font-size: 18px;
line-height: 1;
color: var(--muted);
cursor: pointer;
flex: none;
}
.close-btn:hover { background: var(--canvas); color: var(--ink); }
.panel-section { padding: 18px 22px; border-bottom: 1px solid var(--line-2); }
.panel-section h3 {
margin: 0 0 12px;
font-size: 12px;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--muted);
}
/* line items */
.line-item {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 0;
}
.line-item + .line-item { border-top: 1px solid var(--line-2); }
.thumb {
width: 44px; height: 44px;
border-radius: 10px;
flex: none;
display: grid;
place-items: center;
font-size: 20px;
}
.li-body { min-width: 0; flex: 1; }
.li-name { font-weight: 600; font-size: 14px; }
.li-meta { color: var(--faint); font-size: 12.5px; }
.li-price { font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* summary */
.summary { display: grid; gap: 7px; }
.summary-row { display: flex; justify-content: space-between; font-size: 14px; }
.summary-row span { color: var(--muted); }
.summary-row.total {
border-top: 1px solid var(--line);
margin-top: 4px;
padding-top: 10px;
font-size: 16px;
font-weight: 800;
}
.summary-row.total span { color: var(--ink); }
/* customer */
.cust-block { display: flex; flex-direction: column; gap: 10px; }
.cust-block .cust-top { display: flex; align-items: center; gap: 12px; }
.cust-block .cust-top strong { font-size: 15px; }
.cust-block .cust-top div { font-size: 13px; color: var(--muted); }
.addr { font-size: 13px; color: var(--muted); line-height: 1.5; }
.addr strong { color: var(--ink); display: block; margin-bottom: 2px; font-weight: 600; }
/* timeline */
.timeline { list-style: none; margin: 0; padding: 0 0 0 4px; }
.timeline li {
position: relative;
padding: 0 0 16px 22px;
font-size: 13.5px;
}
.timeline li::before {
content: "";
position: absolute;
left: 3px; top: 4px;
width: 9px; height: 9px;
border-radius: 50%;
background: var(--brand);
box-shadow: 0 0 0 3px var(--brand-soft);
}
.timeline li::after {
content: "";
position: absolute;
left: 7px; top: 13px; bottom: -3px;
width: 1px;
background: var(--line);
}
.timeline li:last-child { padding-bottom: 0; }
.timeline li:last-child::after { display: none; }
.tl-title { font-weight: 600; }
.tl-time { color: var(--faint); font-size: 12.5px; }
/* panel actions */
.panel-actions {
position: sticky;
bottom: 0;
background: #fff;
border-top: 1px solid var(--line);
padding: 14px 22px;
display: flex;
gap: 10px;
}
.panel-actions .btn { flex: 1; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 12px);
background: var(--ink);
color: #fff;
padding: 11px 18px;
border-radius: 11px;
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity .2s, transform .2s;
z-index: 60;
max-width: min(90vw, 420px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
.toast.ok { background: #14784a; }
.toast.warn { background: #b81a4c; }
/* data-label hidden by default (used in card view) */
.orders td .cell-label { display: none; }
/* ---------- Responsive ---------- */
@media (max-width: 1040px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 860px) {
:root { --sidebar-w: 64px; }
.app { grid-template-columns: var(--sidebar-w) 1fr; }
.brand-name, .nav-item span ~ *:not(span), .store-meta { display: none; }
.nav-item { justify-content: center; padding: 11px 0; font-size: 0; }
.nav-item span { font-size: 16px; width: auto; }
.store-chip { justify-content: center; padding: 8px; }
.main { padding: 18px 16px 36px; }
}
@media (max-width: 720px) {
.search { min-width: 0; flex: 1; }
.topbar-actions { width: 100%; }
.stats { grid-template-columns: 1fr 1fr; }
/* table → cards */
.table-wrap { background: transparent; border: 0; box-shadow: none; overflow: visible; }
.orders thead { display: none; }
.orders, .orders tbody, .orders tr, .orders td { display: block; width: 100%; }
.orders tbody tr {
background: #fff;
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 12px;
padding: 6px 4px;
}
.orders tbody tr:hover, .orders tbody tr.is-open { box-shadow: var(--shadow); background: #fff; }
.orders tbody tr.is-open { outline: 2px solid var(--brand); }
.orders td {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 0;
padding: 7px 14px;
}
.orders td .cell-label {
display: inline;
font-size: 12px;
text-transform: uppercase;
letter-spacing: .03em;
color: var(--muted);
font-weight: 700;
}
.col-check { display: flex; }
.cust-cell { justify-content: flex-start; }
.panel { width: 100vw; max-width: 100vw; }
}
@media (max-width: 420px) {
.stats { grid-template-columns: 1fr; }
.tabs { width: 100%; }
.tab { flex: 1; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; animation: none !important; }
}/* Shop — Orders Manager (vanilla JS) */
(function () {
"use strict";
/* ---------- Data ---------- */
const AVATAR_COLORS = ["#3457ff", "#e0245e", "#1f9d55", "#b9760a", "#7c3aed", "#0891b2", "#db2777", "#475569"];
// payment: paid | pending | refunded
// fulfillment: unfulfilled | fulfilled | refunded
const ORDERS = [
{
id: "1042", customer: "Maya Okonkwo", email: "maya.o@example.com",
payment: "pending", fulfillment: "unfulfilled", date: "2026-06-13T09:14:00",
ship: "Express", shipCost: 12, country: "United States",
addr: ["Maya Okonkwo", "418 Larkspur Ave, Apt 7", "Portland, OR 97204", "United States"],
items: [
{ name: "Aurora Desk Lamp", variant: "Brass · Warm white", qty: 1, price: 89, ico: "💡", tint: "#fff3e0" },
{ name: "Linen Lampshade", variant: "Sand", qty: 2, price: 24, ico: "🪔", tint: "#fdebf2" }
]
},
{
id: "1041", customer: "Theo Brandt", email: "theo.brandt@example.com",
payment: "paid", fulfillment: "unfulfilled", date: "2026-06-13T08:02:00",
ship: "Standard", shipCost: 0, country: "Germany",
addr: ["Theo Brandt", "Gartenstraße 12", "10115 Berlin", "Germany"],
items: [
{ name: "Trailhead Backpack 28L", variant: "Slate", qty: 1, price: 148, ico: "🎒", tint: "#eef2ff" }
]
},
{
id: "1040", customer: "Priya Raman", email: "priya.raman@example.com",
payment: "paid", fulfillment: "fulfilled", date: "2026-06-12T17:48:00",
ship: "Express", shipCost: 12, country: "United States",
addr: ["Priya Raman", "92 Cedar Hollow Rd", "Austin, TX 78701", "United States"],
items: [
{ name: "Ceramic Pour-Over Set", variant: "Matte black", qty: 1, price: 64, ico: "☕", tint: "#eef7f0" },
{ name: "House Blend Beans", variant: "1kg · Whole", qty: 1, price: 28, ico: "🫘", tint: "#f3efe7" }
]
},
{
id: "1039", customer: "Jonah Webb", email: "jonah.webb@example.com",
payment: "pending", fulfillment: "unfulfilled", date: "2026-06-12T15:20:00",
ship: "Standard", shipCost: 0, country: "Canada",
addr: ["Jonah Webb", "77 Maple Cres", "Toronto, ON M5V 2T6", "Canada"],
items: [
{ name: "Merino Crew Sweater", variant: "Oat · M", qty: 1, price: 118, ico: "🧶", tint: "#fbf3e8" }
]
},
{
id: "1038", customer: "Aiko Tanaka", email: "aiko.t@example.com",
payment: "paid", fulfillment: "fulfilled", date: "2026-06-12T11:05:00",
ship: "Express", shipCost: 12, country: "Japan",
addr: ["Aiko Tanaka", "3-14-2 Shibuya", "Tokyo 150-0002", "Japan"],
items: [
{ name: "Studio Headphones", variant: "Graphite", qty: 1, price: 219, ico: "🎧", tint: "#eef2ff" },
{ name: "Braided USB-C Cable", variant: "2m", qty: 1, price: 18, ico: "🔌", tint: "#eef7f0" }
]
},
{
id: "1037", customer: "Lucas Moreau", email: "lucas.m@example.com",
payment: "refunded", fulfillment: "refunded", date: "2026-06-11T19:33:00",
ship: "Standard", shipCost: 0, country: "France",
addr: ["Lucas Moreau", "8 Rue des Lilas", "75011 Paris", "France"],
items: [
{ name: "Canvas Sneakers", variant: "Ecru · 43", qty: 1, price: 95, ico: "👟", tint: "#fdebf2" }
]
},
{
id: "1036", customer: "Hana Bauer", email: "hana.bauer@example.com",
payment: "paid", fulfillment: "unfulfilled", date: "2026-06-11T14:11:00",
ship: "Express", shipCost: 12, country: "Austria",
addr: ["Hana Bauer", "Mariahilfer Str. 45", "1060 Vienna", "Austria"],
items: [
{ name: "Weighted Throw Blanket", variant: "Fog · 7kg", qty: 1, price: 132, ico: "🛋️", tint: "#fbf3e8" },
{ name: "Lavender Candle", variant: "Set of 2", qty: 1, price: 36, ico: "🕯️", tint: "#fdebf2" }
]
},
{
id: "1035", customer: "Devon Carter", email: "devon.c@example.com",
payment: "paid", fulfillment: "fulfilled", date: "2026-06-10T22:47:00",
ship: "Standard", shipCost: 0, country: "United States",
addr: ["Devon Carter", "210 Birch St", "Seattle, WA 98101", "United States"],
items: [
{ name: "Mechanical Keyboard", variant: "75% · Brown", qty: 1, price: 159, ico: "⌨️", tint: "#eef2ff" }
]
},
{
id: "1034", customer: "Sofia Esposito", email: "sofia.e@example.com",
payment: "pending", fulfillment: "unfulfilled", date: "2026-06-10T16:29:00",
ship: "Express", shipCost: 12, country: "Italy",
addr: ["Sofia Esposito", "Via Roma 18", "20121 Milan", "Italy"],
items: [
{ name: "Leather Card Wallet", variant: "Cognac", qty: 1, price: 58, ico: "👛", tint: "#f3efe7" },
{ name: "Keyring Charm", variant: "Brass", qty: 1, price: 14, ico: "🔑", tint: "#fff3e0" }
]
},
{
id: "1033", customer: "Mateo Vargas", email: "mateo.v@example.com",
payment: "paid", fulfillment: "fulfilled", date: "2026-06-09T13:08:00",
ship: "Express", shipCost: 12, country: "Spain",
addr: ["Mateo Vargas", "Calle Mayor 7", "28013 Madrid", "Spain"],
items: [
{ name: "Smart Plant Sensor", variant: "Pack of 3", qty: 1, price: 72, ico: "🌱", tint: "#eef7f0" }
]
}
];
const FILTER_FNS = {
all: () => true,
unfulfilled: (o) => o.fulfillment === "unfulfilled",
paid: (o) => o.payment === "paid",
refunded: (o) => o.payment === "refunded" || o.fulfillment === "refunded"
};
const PAY_BADGE = {
paid: { cls: "b-paid", label: "Paid" },
pending: { cls: "b-pending", label: "Pending" },
refunded: { cls: "b-refunded", label: "Refunded" }
};
const FUL_BADGE = {
unfulfilled: { cls: "b-unfulfilled", label: "Unfulfilled" },
fulfilled: { cls: "b-fulfilled", label: "Fulfilled" },
refunded: { cls: "b-ful-refunded", label: "Refunded" }
};
/* ---------- State ---------- */
const state = {
filter: "all",
query: "",
sortKey: "date",
sortDir: "desc",
selected: new Set(),
openId: null
};
/* ---------- Helpers ---------- */
const $ = (sel, root) => (root || document).querySelector(sel);
const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel));
const money = (n) =>
"$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function orderTotal(o) {
const sub = o.items.reduce((s, it) => s + it.price * it.qty, 0);
return sub + o.shipCost;
}
function orderSubtotal(o) {
return o.items.reduce((s, it) => s + it.price * it.qty, 0);
}
function itemCount(o) {
return o.items.reduce((s, it) => s + it.qty, 0);
}
function initials(name) {
return name.split(/\s+/).slice(0, 2).map((p) => p[0]).join("").toUpperCase();
}
function avatarColor(id) {
let h = 0;
for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
return AVATAR_COLORS[h % AVATAR_COLORS.length];
}
function fmtDate(iso) {
const d = new Date(iso);
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) +
", " + d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
}
function fmtFullDate(iso) {
return new Date(iso).toLocaleDateString("en-US", {
weekday: "short", month: "long", day: "numeric", year: "numeric",
hour: "numeric", minute: "2-digit"
});
}
function nowTime() {
return new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
}
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
/* ---------- Toast ---------- */
let toastTimer;
const toastEl = $("#toast");
function toast(msg, kind) {
toastEl.textContent = msg;
toastEl.className = "toast show" + (kind ? " " + kind : "");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { toastEl.className = "toast"; }, 2600);
}
/* ---------- Timeline seed ---------- */
ORDERS.forEach((o) => {
o.timeline = [];
o.timeline.push({ title: "Order placed", time: fmtFullDate(o.date) });
if (o.payment === "paid" || o.payment === "refunded") {
o.timeline.push({ title: "Payment captured · " + o.ship + " shipping", time: fmtFullDate(o.date) });
} else {
o.timeline.push({ title: "Awaiting payment", time: "Payment authorization pending" });
}
if (o.fulfillment === "fulfilled") {
o.timeline.push({ title: "Items fulfilled & shipped", time: "Tracking emailed to customer" });
}
if (o.fulfillment === "refunded" || o.payment === "refunded") {
o.timeline.push({ title: "Order refunded", time: "Funds returned to original method" });
}
});
/* ---------- Derived list ---------- */
function visibleOrders() {
const q = state.query.trim().toLowerCase();
let list = ORDERS.filter(FILTER_FNS[state.filter]);
if (q) {
list = list.filter((o) =>
o.id.toLowerCase().includes(q) ||
o.customer.toLowerCase().includes(q) ||
o.email.toLowerCase().includes(q)
);
}
const dir = state.sortDir === "asc" ? 1 : -1;
list.sort((a, b) => {
let av, bv;
switch (state.sortKey) {
case "total": av = orderTotal(a); bv = orderTotal(b); break;
case "customer": av = a.customer.toLowerCase(); bv = b.customer.toLowerCase(); break;
case "id": av = parseInt(a.id, 10); bv = parseInt(b.id, 10); break;
default: av = new Date(a.date).getTime(); bv = new Date(b.date).getTime();
}
if (av < bv) return -1 * dir;
if (av > bv) return 1 * dir;
return 0;
});
return list;
}
/* ---------- Render table ---------- */
const tbody = $("#ordersBody");
const emptyState = $("#emptyState");
function badge(map, key) {
const b = map[key];
return '<span class="badge ' + b.cls + '">' + b.label + "</span>";
}
function renderTable() {
const list = visibleOrders();
if (!list.length) {
tbody.innerHTML = "";
emptyState.hidden = false;
} else {
emptyState.hidden = true;
tbody.innerHTML = list.map((o) => {
const sel = state.selected.has(o.id);
const open = state.openId === o.id;
const n = itemCount(o);
return (
'<tr data-id="' + o.id + '"' +
(sel ? ' class="is-selected' + (open ? " is-open" : "") + '"' : (open ? ' class="is-open"' : "")) +
'>' +
'<td class="col-check" data-noopen>' +
'<input type="checkbox" class="row-check" ' + (sel ? "checked" : "") +
' aria-label="Select order ' + o.id + '" />' +
"</td>" +
'<td class="col-id">' +
'<span class="cell-label">Order</span>' +
'<span><span class="order-id">#' + o.id + "</span>" +
' <span class="order-items">' + n + " item" + (n > 1 ? "s" : "") + "</span></span>" +
"</td>" +
'<td class="col-cust">' +
'<span class="cell-label">Customer</span>' +
'<span class="cust-cell">' +
'<span class="avatar" style="background:' + avatarColor(o.id) + '" aria-hidden="true">' +
esc(initials(o.customer)) + "</span>" +
"<div><div class=\"cust-name\">" + esc(o.customer) + "</div>" +
'<div class="cust-mail">' + esc(o.email) + "</div></div>" +
"</span>" +
"</td>" +
'<td class="col-pay"><span class="cell-label">Payment</span>' + badge(PAY_BADGE, o.payment) + "</td>" +
'<td class="col-ful"><span class="cell-label">Fulfillment</span>' + badge(FUL_BADGE, o.fulfillment) + "</td>" +
'<td class="col-total num"><span class="cell-label">Total</span>' +
'<span class="total-val">' + money(orderTotal(o)) + "</span></td>" +
'<td class="col-date"><span class="cell-label">Date</span>' +
'<span class="date-val">' + fmtDate(o.date) + "</span></td>" +
"</tr>"
);
}).join("");
}
syncCheckAll(list);
}
/* ---------- Tabs / counts ---------- */
function updateCounts() {
$$(".tab-count").forEach((el) => {
const key = el.getAttribute("data-count");
const q = state.query.trim().toLowerCase();
let n = ORDERS.filter(FILTER_FNS[key]).length;
if (q) {
n = ORDERS.filter(FILTER_FNS[key]).filter((o) =>
o.id.toLowerCase().includes(q) ||
o.customer.toLowerCase().includes(q) ||
o.email.toLowerCase().includes(q)).length;
}
el.textContent = n;
});
$("#statUnfulfilled").textContent = ORDERS.filter(FILTER_FNS.unfulfilled).length;
}
/* ---------- Select-all + bulk bar ---------- */
const checkAll = $("#checkAll");
const bulkbar = $("#bulkbar");
function syncCheckAll(list) {
const ids = list.map((o) => o.id);
const selectable = ids.filter((id) => find(id).fulfillment === "unfulfilled");
const allSel = selectable.length > 0 && selectable.every((id) => state.selected.has(id));
const someSel = selectable.some((id) => state.selected.has(id));
checkAll.checked = allSel;
checkAll.indeterminate = !allSel && someSel;
checkAll.disabled = selectable.length === 0;
}
function updateBulkBar() {
const ids = Array.from(state.selected);
if (!ids.length) {
bulkbar.hidden = true;
return;
}
bulkbar.hidden = false;
$("#bulkCount").textContent = ids.length;
const total = ids.reduce((s, id) => s + orderTotal(find(id)), 0);
$("#bulkTotal").textContent = money(total) + " total";
}
function find(id) {
return ORDERS.find((o) => o.id === id);
}
/* ---------- Detail panel ---------- */
const panel = $("#panel");
const panelInner = $("#panelInner");
const scrim = $("#scrim");
function openDetail(id) {
const o = find(id);
if (!o) return;
state.openId = id;
panelInner.innerHTML = renderDetail(o);
panel.classList.add("is-open");
panel.setAttribute("aria-hidden", "false");
scrim.hidden = false;
renderTable();
wireDetail(o);
const closeBtn = $(".close-btn", panel);
if (closeBtn) closeBtn.focus();
}
function closeDetail() {
state.openId = null;
panel.classList.remove("is-open");
panel.setAttribute("aria-hidden", "true");
scrim.hidden = true;
renderTable();
}
function renderDetail(o) {
const items = o.items.map((it) =>
'<div class="line-item">' +
'<span class="thumb" style="background:' + it.tint + '" aria-hidden="true">' + it.ico + "</span>" +
'<div class="li-body">' +
'<div class="li-name">' + esc(it.name) + "</div>" +
'<div class="li-meta">' + esc(it.variant) + " · Qty " + it.qty + "</div>" +
"</div>" +
'<div class="li-price">' + money(it.price * it.qty) + "</div>" +
"</div>"
).join("");
const timeline = o.timeline.map((t) =>
"<li><div class=\"tl-title\">" + esc(t.title) + "</div>" +
'<div class="tl-time">' + esc(t.time) + "</div></li>"
).join("");
const canFulfill = o.fulfillment === "unfulfilled" && o.payment !== "refunded";
const canRefund = o.payment === "paid" && o.fulfillment !== "refunded";
return (
'<div class="panel-head">' +
'<div class="panel-head-top">' +
"<div><h2>Order #" + o.id + "</h2>" +
'<p class="order-date">' + fmtFullDate(o.date) + "</p></div>" +
'<button class="close-btn" data-close type="button" aria-label="Close detail">×</button>' +
"</div>" +
'<div class="panel-badges">' + badge(PAY_BADGE, o.payment) + badge(FUL_BADGE, o.fulfillment) + "</div>" +
"</div>" +
'<div class="panel-section"><h3>Items (' + itemCount(o) + ")</h3>" + items + "</div>" +
'<div class="panel-section"><h3>Summary</h3><div class="summary">' +
'<div class="summary-row"><span>Subtotal</span><span>' + money(orderSubtotal(o)) + "</span></div>" +
'<div class="summary-row"><span>Shipping (' + esc(o.ship) + ")</span><span>" +
(o.shipCost ? money(o.shipCost) : "Free") + "</span></div>" +
'<div class="summary-row total"><span>Total</span><span>' + money(orderTotal(o)) + "</span></div>" +
"</div></div>" +
'<div class="panel-section"><h3>Customer</h3><div class="cust-block">' +
'<div class="cust-top">' +
'<span class="avatar" style="background:' + avatarColor(o.id) + '" aria-hidden="true">' +
esc(initials(o.customer)) + "</span>" +
"<div><strong>" + esc(o.customer) + "</strong><div>" + esc(o.email) + "</div></div>" +
"</div>" +
'<div class="addr"><strong>Shipping address</strong>' +
o.addr.map(esc).join("<br>") + "</div>" +
"</div></div>" +
'<div class="panel-section"><h3>Timeline</h3><ul class="timeline">' + timeline + "</ul></div>" +
'<div class="panel-actions">' +
'<button class="btn btn-primary" data-fulfill type="button"' + (canFulfill ? "" : " disabled") + ">Fulfill order</button>" +
'<button class="btn btn-danger" data-refund type="button"' + (canRefund ? "" : " disabled") + ">Refund</button>" +
'<button class="btn btn-ghost" data-print type="button">Print</button>' +
"</div>"
);
}
function wireDetail(o) {
$("[data-close]", panel).addEventListener("click", closeDetail);
const fBtn = $("[data-fulfill]", panel);
if (fBtn) fBtn.addEventListener("click", () => {
fulfillOrder(o.id);
panelInner.innerHTML = renderDetail(find(o.id));
wireDetail(o);
});
const rBtn = $("[data-refund]", panel);
if (rBtn) rBtn.addEventListener("click", () => {
refundOrder(o.id);
panelInner.innerHTML = renderDetail(find(o.id));
wireDetail(o);
});
$("[data-print]", panel).addEventListener("click", () => {
toast("Packing slip for #" + o.id + " sent to printer");
});
}
/* ---------- Actions ---------- */
function fulfillOrder(id) {
const o = find(id);
if (!o || o.fulfillment !== "unfulfilled") return false;
o.fulfillment = "fulfilled";
if (o.payment === "pending") o.payment = "paid";
o.timeline.push({ title: "Items fulfilled & shipped", time: "Today at " + nowTime() });
state.selected.delete(id);
refreshAll();
toast("Order #" + id + " fulfilled", "ok");
return true;
}
function refundOrder(id) {
const o = find(id);
if (!o || o.payment !== "paid") return false;
o.payment = "refunded";
o.fulfillment = "refunded";
o.timeline.push({ title: "Order refunded", time: "Today at " + nowTime() });
state.selected.delete(id);
refreshAll();
toast("Order #" + id + " refunded · " + money(orderTotal(o)), "warn");
return true;
}
function bulkFulfill() {
const ids = Array.from(state.selected).filter((id) => find(id).fulfillment === "unfulfilled");
if (!ids.length) {
toast("No unfulfilled orders selected");
return;
}
ids.forEach((id) => {
const o = find(id);
o.fulfillment = "fulfilled";
if (o.payment === "pending") o.payment = "paid";
o.timeline.push({ title: "Items fulfilled & shipped (bulk)", time: "Today at " + nowTime() });
});
state.selected.clear();
if (state.openId && ids.includes(state.openId)) {
panelInner.innerHTML = renderDetail(find(state.openId));
wireDetail(find(state.openId));
}
refreshAll();
toast(ids.length + " order" + (ids.length > 1 ? "s" : "") + " fulfilled", "ok");
}
function refreshAll() {
renderTable();
updateCounts();
updateBulkBar();
}
/* ---------- Events ---------- */
// Tabs
$$(".tab").forEach((tab) => {
tab.addEventListener("click", () => {
$$(".tab").forEach((t) => { t.classList.remove("is-active"); t.setAttribute("aria-selected", "false"); });
tab.classList.add("is-active");
tab.setAttribute("aria-selected", "true");
state.filter = tab.getAttribute("data-filter");
renderTable();
});
});
// Search
let searchTimer;
$("#search").addEventListener("input", (e) => {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
state.query = e.target.value;
renderTable();
updateCounts();
}, 120);
});
// Sorting
$$(".sortable").forEach((th) => {
th.addEventListener("click", () => {
const key = th.getAttribute("data-sort");
if (state.sortKey === key) {
state.sortDir = state.sortDir === "asc" ? "desc" : "asc";
} else {
state.sortKey = key;
state.sortDir = key === "customer" ? "asc" : "desc";
}
$$(".sortable").forEach((h) => h.removeAttribute("aria-sort"));
th.setAttribute("aria-sort", state.sortDir === "asc" ? "ascending" : "descending");
renderTable();
});
});
// Row interactions (delegated)
tbody.addEventListener("click", (e) => {
const checkCell = e.target.closest("[data-noopen]");
const row = e.target.closest("tr");
if (!row) return;
if (checkCell) return; // checkbox handled separately
openDetail(row.getAttribute("data-id"));
});
tbody.addEventListener("change", (e) => {
if (!e.target.classList.contains("row-check")) return;
const row = e.target.closest("tr");
const id = row.getAttribute("data-id");
if (e.target.checked) state.selected.add(id);
else state.selected.delete(id);
row.classList.toggle("is-selected", e.target.checked);
updateBulkBar();
syncCheckAll(visibleOrders());
});
// Select-all (only unfulfilled in current view)
checkAll.addEventListener("change", () => {
const list = visibleOrders().filter((o) => o.fulfillment === "unfulfilled");
if (checkAll.checked) list.forEach((o) => state.selected.add(o.id));
else list.forEach((o) => state.selected.delete(o.id));
refreshAll();
});
// Bulk bar buttons
$("#bulkFulfill").addEventListener("click", bulkFulfill);
$("#bulkClear").addEventListener("click", () => {
state.selected.clear();
refreshAll();
});
// Export (demo)
$("#exportBtn").addEventListener("click", () => {
toast("Exported " + visibleOrders().length + " orders to CSV");
});
// Close panel
scrim.addEventListener("click", closeDetail);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && state.openId) closeDetail();
});
/* ---------- Init ---------- */
refreshAll();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Shop — Orders Manager</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">
<!-- Sidebar -->
<aside class="sidebar" aria-label="Admin navigation">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◈</span>
<span class="brand-name">Northwind</span>
</div>
<nav class="nav" aria-label="Sections">
<a href="#" class="nav-item"><span aria-hidden="true">▦</span> Dashboard</a>
<a href="#" class="nav-item"><span aria-hidden="true">▤</span> Products</a>
<a href="#" class="nav-item is-active" aria-current="page"><span aria-hidden="true">▥</span> Orders</a>
<a href="#" class="nav-item"><span aria-hidden="true">▧</span> Customers</a>
<a href="#" class="nav-item"><span aria-hidden="true">▨</span> Analytics</a>
</nav>
<div class="nav-foot">
<div class="store-chip">
<span class="store-dot" aria-hidden="true"></span>
<div class="store-meta">
<strong>Northwind Goods</strong>
<span>Store online</span>
</div>
</div>
</div>
</aside>
<!-- Main -->
<main class="main" aria-labelledby="page-title">
<header class="topbar">
<div class="topbar-head">
<h1 id="page-title">Orders</h1>
<p class="sub">Manage payments and fulfillment across your store.</p>
</div>
<div class="topbar-actions">
<label class="search" aria-label="Search orders">
<span class="search-ic" aria-hidden="true">⌕</span>
<input id="search" type="search" placeholder="Search order # or customer…" autocomplete="off" />
</label>
<button class="btn btn-ghost" id="exportBtn" type="button">Export</button>
</div>
</header>
<!-- Stat strip -->
<section class="stats" aria-label="Order summary">
<div class="stat">
<span class="stat-label">Orders today</span>
<strong class="stat-val">38</strong>
<span class="stat-delta up">▲ 12%</span>
</div>
<div class="stat">
<span class="stat-label">Awaiting fulfillment</span>
<strong class="stat-val" id="statUnfulfilled">—</strong>
<span class="stat-delta">needs action</span>
</div>
<div class="stat">
<span class="stat-label">Revenue (7d)</span>
<strong class="stat-val">$24,118</strong>
<span class="stat-delta up">▲ 6%</span>
</div>
<div class="stat">
<span class="stat-label">Refunds (7d)</span>
<strong class="stat-val">$412</strong>
<span class="stat-delta down">▼ 2 orders</span>
</div>
</section>
<!-- Filter tabs -->
<div class="toolbar">
<div class="tabs" role="tablist" aria-label="Filter orders by status">
<button class="tab is-active" role="tab" aria-selected="true" data-filter="all" type="button">
All <span class="tab-count" data-count="all">0</span>
</button>
<button class="tab" role="tab" aria-selected="false" data-filter="unfulfilled" type="button">
Unfulfilled <span class="tab-count" data-count="unfulfilled">0</span>
</button>
<button class="tab" role="tab" aria-selected="false" data-filter="paid" type="button">
Paid <span class="tab-count" data-count="paid">0</span>
</button>
<button class="tab" role="tab" aria-selected="false" data-filter="refunded" type="button">
Refunded <span class="tab-count" data-count="refunded">0</span>
</button>
</div>
</div>
<!-- Bulk bar -->
<div class="bulkbar" id="bulkbar" hidden>
<span class="bulk-count"><strong id="bulkCount">0</strong> selected</span>
<span class="bulk-total" id="bulkTotal">$0.00</span>
<div class="bulk-actions">
<button class="btn btn-primary" id="bulkFulfill" type="button">Fulfill selected</button>
<button class="btn btn-ghost" id="bulkClear" type="button">Clear</button>
</div>
</div>
<!-- Orders table -->
<section class="table-wrap" aria-label="Orders">
<table class="orders" id="ordersTable">
<thead>
<tr>
<th class="col-check" scope="col">
<input type="checkbox" id="checkAll" aria-label="Select all visible orders" />
</th>
<th class="col-id sortable" scope="col" data-sort="id">Order</th>
<th class="col-cust sortable" scope="col" data-sort="customer">Customer</th>
<th class="col-pay" scope="col">Payment</th>
<th class="col-ful" scope="col">Fulfillment</th>
<th class="col-total sortable num" scope="col" data-sort="total">Total</th>
<th class="col-date sortable" scope="col" data-sort="date">Date</th>
</tr>
</thead>
<tbody id="ordersBody"><!-- rows injected --></tbody>
</table>
<p class="empty" id="emptyState" hidden>No orders match this view.</p>
</section>
</main>
<!-- Detail panel -->
<aside class="panel" id="panel" aria-label="Order detail" aria-hidden="true">
<div class="panel-inner" id="panelInner"><!-- detail injected --></div>
</aside>
<div class="scrim" id="scrim" hidden></div>
</div>
<!-- Toast -->
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Orders Manager
An admin orders console for the fictional Northwind storefront. The main table lists every order with its ID, customer, item count, total, a payment chip (Paid, Pending or Refunded) and a fulfillment chip (Unfulfilled, Fulfilled or Refunded), plus the order date. Filter tabs across the top — All, Unfulfilled, Paid and Refunded — narrow the view, each carrying a live count, and a search box matches against order number or customer name as you type. Column headers sort by date, total or customer.
Clicking any row slides out a detail panel with the order’s line items and per-line pricing, the customer and shipping block, an order summary with subtotal, shipping and total, and a vertical timeline of events. The panel’s Fulfill order, Refund and Print actions update the badges in place and confirm with a toast, while refunds append a timeline entry. A checkbox column plus a sticky bulk bar let you select several unfulfilled orders and fulfill them all at once, with the selection count and totals kept in sync.
Everything is generated from a small in-memory order model, so the table, tabs, search, sorting and detail panel all read from the same source of truth. The layout collapses the panel to a full-width sheet and switches the table to a stacked card view below about 720px, staying usable down to 360px.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.