Delivery — Dispatch Board
A desktop dispatch board for a fictional food-delivery hub, built as a horizontal kanban with an unassigned-orders column beside one queue per driver. Each order card shows pickup and drop-off legs, a priority pill, a live ETA, and the customer; cards drag between columns or assign through a driver picker dialog. A live status sidebar tracks each courier with availability dots and active load, and a sticky toolbar drives priority filters, search, and real-time counts.
MCP
Code
:root {
--brand: #ff5a2c;
--brand-d: #e0461d;
--ink: #16181d;
--ink-2: #3b3f4a;
--muted: #71757f;
--bg: #f4f5f7;
--surface: #ffffff;
--line: rgba(22, 24, 29, 0.1);
--ok: #1f9d62;
--warn: #e89422;
--danger: #d4493e;
--track: #5b8def;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(22, 24, 29, 0.06), 0 1px 3px rgba(22, 24, 29, 0.05);
--sh-2: 0 8px 24px rgba(22, 24, 29, 0.1);
--sh-3: 0 18px 50px rgba(22, 24, 29, 0.22);
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button { font-family: inherit; }
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip-link:focus { left: 8px; }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
padding: 12px 20px;
background: var(--surface);
border-bottom: 1px solid var(--line);
box-shadow: var(--sh-1);
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
color: #fff;
font-size: 18px;
box-shadow: var(--sh-1);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 15px; font-weight: 800; letter-spacing: -0.01em; }
.brand-text span { font-size: 12px; color: var(--muted); font-weight: 500; }
.brand-text span::before {
content: "";
display: inline-block;
width: 7px; height: 7px;
border-radius: 50%;
background: var(--ok);
margin-right: 5px;
vertical-align: middle;
box-shadow: 0 0 0 0 rgba(31, 157, 98, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(31, 157, 98, 0.45); }
70% { box-shadow: 0 0 0 7px rgba(31, 157, 98, 0); }
100% { box-shadow: 0 0 0 0 rgba(31, 157, 98, 0); }
}
.topbar-stats { display: flex; gap: 8px; }
.stat {
display: flex;
flex-direction: column;
align-items: center;
min-width: 64px;
padding: 6px 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.stat-num { font-size: 20px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; }
.stat-label { font-size: 11px; color: var(--muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.topbar-tools {
display: flex;
align-items: center;
gap: 12px;
margin-left: auto;
flex-wrap: wrap;
}
.search {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
}
.search:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px rgba(255, 90, 44, 0.15); }
.search input {
border: 0;
background: none;
outline: none;
font-size: 14px;
color: var(--ink);
width: 200px;
}
.search input::placeholder { color: var(--muted); }
.filters { display: flex; gap: 6px; }
.chip {
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
padding: 7px 13px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.chip:hover { border-color: var(--brand); color: var(--brand-d); }
.chip.is-active { background: var(--ink); color: #fff; border-color: var(--ink); }
.chip:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 280px;
gap: 18px;
padding: 18px 20px 36px;
max-width: 1500px;
margin: 0 auto;
}
/* ---------- Board ---------- */
.board {
display: flex;
gap: 14px;
overflow-x: auto;
padding-bottom: 8px;
scroll-snap-type: x proximity;
}
.column {
flex: 0 0 274px;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
scroll-snap-align: start;
max-height: calc(100vh - 150px);
}
.column.unassigned {
border-color: rgba(255, 90, 44, 0.4);
background: linear-gradient(180deg, rgba(255, 90, 44, 0.05), var(--surface) 70px);
}
.col-head {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 14px;
border-bottom: 1px solid var(--line);
}
.col-avatar {
display: grid;
place-items: center;
width: 34px; height: 34px;
border-radius: 50%;
font-size: 13px;
font-weight: 700;
color: #fff;
flex: 0 0 auto;
}
.col-avatar.tag-orders { background: linear-gradient(135deg, var(--brand), var(--brand-d)); }
.col-title { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.col-title strong { font-size: 14px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.col-title small { font-size: 12px; color: var(--muted); font-weight: 500; }
.col-count {
margin-left: auto;
font-size: 12px;
font-weight: 700;
background: var(--bg);
color: var(--ink-2);
border-radius: 999px;
padding: 3px 9px;
border: 1px solid var(--line);
}
.dstatus {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
}
.dstatus i { width: 7px; height: 7px; border-radius: 50%; }
.dstatus.ok { color: var(--ok); background: rgba(31, 157, 98, 0.12); }
.dstatus.ok i { background: var(--ok); }
.dstatus.warn { color: var(--warn); background: rgba(232, 148, 34, 0.14); }
.dstatus.warn i { background: var(--warn); }
.dstatus.off { color: var(--muted); background: rgba(113, 117, 127, 0.14); }
.dstatus.off i { background: var(--muted); }
.col-body {
flex: 1;
overflow-y: auto;
padding: 12px;
display: flex;
flex-direction: column;
gap: 11px;
min-height: 90px;
transition: background 0.15s;
}
.col-body.drag-over {
background: rgba(91, 141, 239, 0.08);
outline: 2px dashed var(--track);
outline-offset: -8px;
border-radius: var(--r-md);
}
.col-empty {
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 22px 6px;
border: 1.5px dashed var(--line);
border-radius: var(--r-md);
}
/* ---------- Order card ---------- */
.card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-left: 4px solid var(--track);
border-radius: var(--r-md);
padding: 11px 12px 12px;
box-shadow: var(--sh-1);
cursor: grab;
transition: box-shadow 0.15s, transform 0.12s, border-color 0.15s;
}
.card:hover { box-shadow: var(--sh-2); transform: translateY(-1px); }
.card:active { cursor: grabbing; }
.card.dragging { opacity: 0.45; }
.card.prio-express { border-left-color: var(--danger); }
.card.prio-standard { border-left-color: var(--track); }
.card.prio-scheduled { border-left-color: var(--warn); }
.card:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.card-top { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.card-id { font-size: 13px; font-weight: 800; letter-spacing: -0.01em; }
.pill {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
padding: 2px 7px;
border-radius: 999px;
}
.pill.express { color: #fff; background: var(--danger); }
.pill.standard { color: var(--track); background: rgba(91, 141, 239, 0.14); }
.pill.scheduled { color: var(--warn); background: rgba(232, 148, 34, 0.16); }
.card-eta {
margin-left: auto;
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
}
.card-eta b { color: var(--brand-d); }
.route { display: flex; flex-direction: column; gap: 7px; margin-bottom: 10px; }
.leg { display: flex; gap: 8px; align-items: flex-start; font-size: 12.5px; }
.leg .marker {
flex: 0 0 auto;
width: 12px; height: 12px;
margin-top: 3px;
border-radius: 50%;
border: 2px solid var(--brand);
position: relative;
}
.leg.drop .marker { border-color: var(--ok); border-radius: 3px; }
.leg .marker::after {
content: "";
position: absolute;
left: 50%; top: 14px;
width: 2px; height: 11px;
background: var(--line);
transform: translateX(-50%);
}
.leg.drop .marker::after { display: none; }
.leg .place { line-height: 1.35; min-width: 0; }
.leg .place strong { display: block; font-weight: 600; color: var(--ink); }
.leg .place span { color: var(--muted); font-size: 11.5px; }
.card-foot {
display: flex;
align-items: center;
gap: 8px;
padding-top: 9px;
border-top: 1px dashed var(--line);
}
.cust { font-size: 12px; color: var(--ink-2); font-weight: 500; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cust b { color: var(--ink); font-weight: 700; }
.card-actions { margin-left: auto; display: flex; gap: 6px; }
.mini-btn {
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
font-size: 12px;
font-weight: 600;
padding: 5px 9px;
border-radius: var(--r-sm);
cursor: pointer;
white-space: nowrap;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.mini-btn:hover { background: var(--bg); }
.mini-btn.primary { background: var(--brand); color: #fff; border-color: var(--brand); }
.mini-btn.primary:hover { background: var(--brand-d); border-color: var(--brand-d); }
.mini-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ---------- Sidebar ---------- */
.sidebar {
align-self: start;
position: sticky;
top: 84px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
padding: 14px;
}
.sidebar-title { margin: 2px 0 12px; font-size: 14px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-2); }
.driver-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.driver {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
}
.driver .av {
display: grid;
place-items: center;
width: 36px; height: 36px;
border-radius: 50%;
color: #fff;
font-weight: 700;
font-size: 13px;
flex: 0 0 auto;
}
.driver .meta { min-width: 0; flex: 1; }
.driver .meta strong { display: block; font-size: 13.5px; font-weight: 700; }
.driver .meta span { font-size: 11.5px; color: var(--muted); }
.driver .load {
font-size: 12px;
font-weight: 700;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 3px 9px;
}
.legend {
display: flex;
gap: 14px;
flex-wrap: wrap;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--muted);
font-weight: 500;
}
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
.dot.ok { background: var(--ok); }
.dot.warn { background: var(--warn); }
.dot.off { background: var(--muted); }
/* ---------- Modal ---------- */
.modal { position: fixed; inset: 0; z-index: 60; display: grid; place-items: center; }
.modal[hidden] { display: none; }
.modal-backdrop { position: absolute; inset: 0; background: rgba(22, 24, 29, 0.5); backdrop-filter: blur(2px); }
.modal-card {
position: relative;
width: min(420px, calc(100vw - 32px));
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
padding: 18px;
animation: pop 0.18s ease;
}
@keyframes pop { from { transform: translateY(8px) scale(0.98); opacity: 0; } to { transform: none; opacity: 1; } }
.modal-head { display: flex; align-items: center; justify-content: space-between; }
.modal-head h3 { margin: 0; font-size: 17px; font-weight: 800; }
.modal-sub { margin: 4px 0 14px; font-size: 13px; color: var(--muted); }
.icon-btn {
border: 0;
background: var(--bg);
width: 30px; height: 30px;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--ink-2);
font-size: 14px;
}
.icon-btn:hover { background: #ececef; }
.assign-options { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; max-height: 56vh; overflow-y: auto; }
.assign-opt {
display: flex;
align-items: center;
gap: 11px;
width: 100%;
text-align: left;
border: 1px solid var(--line);
background: var(--surface);
border-radius: var(--r-md);
padding: 10px 12px;
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
}
.assign-opt:hover:not(:disabled) { border-color: var(--brand); background: rgba(255, 90, 44, 0.05); }
.assign-opt:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.assign-opt:disabled { opacity: 0.5; cursor: not-allowed; }
.assign-opt .av { width: 34px; height: 34px; border-radius: 50%; display: grid; place-items: center; color: #fff; font-weight: 700; font-size: 13px; flex: 0 0 auto; }
.assign-opt .meta { flex: 1; min-width: 0; }
.assign-opt .meta strong { display: block; font-size: 14px; }
.assign-opt .meta span { font-size: 12px; color: var(--muted); }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
z-index: 80;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
background: var(--ink);
color: #fff;
padding: 11px 16px;
border-radius: 999px;
font-size: 13.5px;
font-weight: 600;
box-shadow: var(--sh-2);
display: flex;
align-items: center;
gap: 9px;
animation: toastIn 0.22s ease;
}
.toast.ok { background: var(--ok); }
.toast.warn { background: var(--warn); color: var(--ink); }
.toast.brand { background: var(--brand); }
.toast::before { content: "●"; font-size: 9px; opacity: 0.85; }
.toast.hide { animation: toastOut 0.25s ease forwards; }
@keyframes toastIn { from { transform: translateY(14px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes toastOut { to { transform: translateY(10px); opacity: 0; } }
/* ---------- Responsive ---------- */
@media (max-width: 980px) {
.layout { grid-template-columns: 1fr; }
.sidebar { position: static; }
.driver-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
}
@media (max-width: 520px) {
.topbar { gap: 12px; padding: 10px 14px; }
.topbar-tools { width: 100%; margin-left: 0; }
.search input { width: 100%; }
.search { flex: 1; }
.topbar-stats { width: 100%; justify-content: space-between; }
.stat { flex: 1; min-width: 0; }
.layout { padding: 14px 12px 30px; }
.column { flex-basis: 86vw; max-height: none; }
.driver-list { grid-template-columns: 1fr; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var drivers = [
{ id: "d1", name: "Marisol Vega", vehicle: "E-bike · NG-204", status: "ok", color: "#1f9d62" },
{ id: "d2", name: "Tobias Crane", vehicle: "Scooter · NG-118", status: "warn", color: "#e89422" },
{ id: "d3", name: "Priya Naidoo", vehicle: "Car · NG-077", status: "ok", color: "#5b8def" },
{ id: "d4", name: "Owen Becker", vehicle: "E-bike · NG-251", status: "off", color: "#71757f" }
];
var orders = [
{ id: "FH-4821", prio: "express", eta: "14m", pickup: "Sakura Ramen", pickupArea: "Market St · 0.4 mi", drop: "Apt 7B, Linden Court", dropArea: "Northgate · 1.2 mi", customer: "J. Whitlock", driver: null },
{ id: "FH-4822", prio: "standard", eta: "28m", pickup: "Green Bowl Co.", pickupArea: "5th & Pine · 0.9 mi", drop: "Riverside Office, Lvl 3", dropArea: "Dockside · 2.1 mi", customer: "A. Mbeki", driver: null },
{ id: "FH-4823", prio: "scheduled", eta: "6:40 PM", pickup: "Nonna's Pizzeria", pickupArea: "Old Town · 1.5 mi", drop: "14 Maple Row", dropArea: "Hillcrest · 3.0 mi", customer: "L. Romero", driver: null },
{ id: "FH-4824", prio: "express", eta: "11m", pickup: "Bao & Brew", pickupArea: "Market St · 0.3 mi", drop: "Studio 2, Harbor Lofts", dropArea: "Dockside · 1.8 mi", customer: "S. Patel", driver: null },
{ id: "FH-4825", prio: "standard", eta: "22m", pickup: "The Daily Grind", pickupArea: "Elm Ave · 0.7 mi", drop: "Unit 12, Cedar Park", dropArea: "Northgate · 1.0 mi", customer: "K. Andersen", driver: null },
{ id: "FH-4826", prio: "standard", eta: "31m", pickup: "Taco Verde", pickupArea: "9th St · 1.1 mi", drop: "Reception, Atlas Tower", dropArea: "Civic · 2.6 mi", customer: "M. Okonkwo", driver: "d2" },
{ id: "FH-4827", prio: "express", eta: "9m", pickup: "Sakura Ramen", pickupArea: "Market St · 0.4 mi", drop: "Apt 3, Birch Mews", dropArea: "Northgate · 0.8 mi", customer: "D. Fontaine", driver: "d2" },
{ id: "FH-4828", prio: "scheduled", eta: "7:15 PM", pickup: "Saffron House", pickupArea: "Old Town · 1.6 mi", drop: "88 Willow Bend", dropArea: "Hillcrest · 3.4 mi", customer: "R. Tanaka", driver: "d3" }
];
var state = { prio: "all", query: "" };
/* ---------- Helpers ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var board = $("#board");
var driverList = $("#driverList");
var toastWrap = $("#toastWrap");
function initials(name) {
return name.split(/\s+/).map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function driverById(id) {
for (var i = 0; i < drivers.length; i++) if (drivers[i].id === id) return drivers[i];
return null;
}
function loadFor(id) {
return orders.filter(function (o) { return o.driver === id; }).length;
}
function matches(o) {
if (state.prio !== "all" && o.prio !== state.prio) return false;
if (state.query) {
var hay = (o.id + " " + o.customer + " " + o.pickup + " " + o.drop + " " + o.dropArea).toLowerCase();
if (hay.indexOf(state.query) === -1) return false;
}
return true;
}
function toast(msg, kind) {
var t = document.createElement("div");
t.className = "toast" + (kind ? " " + kind : "");
t.textContent = msg;
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("hide");
setTimeout(function () { t.remove(); }, 260);
}, 2400);
}
/* ---------- Card markup ---------- */
function cardEl(o) {
var el = document.createElement("article");
el.className = "card prio-" + o.prio;
el.setAttribute("draggable", "true");
el.setAttribute("tabindex", "0");
el.setAttribute("data-id", o.id);
el.setAttribute("role", "listitem");
el.setAttribute("aria-label",
o.id + ", " + o.prio + " priority, from " + o.pickup + " to " + o.drop +
(o.driver ? ", assigned to " + driverById(o.driver).name : ", unassigned"));
var assignLabel = o.driver ? "Reassign" : "Assign";
el.innerHTML =
'<div class="card-top">' +
'<span class="card-id">' + o.id + '</span>' +
'<span class="pill ' + o.prio + '">' + o.prio + '</span>' +
'<span class="card-eta">ETA <b>' + o.eta + '</b></span>' +
'</div>' +
'<div class="route">' +
'<div class="leg"><span class="marker"></span><span class="place"><strong>' + o.pickup + '</strong><span>' + o.pickupArea + '</span></span></div>' +
'<div class="leg drop"><span class="marker"></span><span class="place"><strong>' + o.drop + '</strong><span>' + o.dropArea + '</span></span></div>' +
'</div>' +
'<div class="card-foot">' +
'<span class="cust">Customer · <b>' + o.customer + '</b></span>' +
'<div class="card-actions">' +
'<button class="mini-btn primary" data-assign="' + o.id + '">' + assignLabel + '</button>' +
'</div>' +
'</div>';
return el;
}
/* ---------- Render board ---------- */
function render() {
board.innerHTML = "";
// Unassigned column
board.appendChild(buildColumn(null));
// One column per online-ish driver (include all so dispatcher can park)
drivers.forEach(function (d) { board.appendChild(buildColumn(d)); });
renderSidebar();
updateStats();
wireDnD();
}
function buildColumn(driver) {
var col = document.createElement("section");
var list = orders.filter(function (o) {
return (driver ? o.driver === driver.id : o.driver === null);
});
var visible = list.filter(matches);
col.className = "column" + (driver ? "" : " unassigned");
col.setAttribute("data-driver", driver ? driver.id : "");
col.setAttribute("aria-label", driver ? driver.name + " queue" : "Unassigned orders");
var head;
if (driver) {
var st = driver.status === "ok" ? "ok" : driver.status === "warn" ? "warn" : "off";
var stTxt = driver.status === "ok" ? "Available" : driver.status === "warn" ? "On run" : "Break";
head =
'<div class="col-head">' +
'<span class="col-avatar" style="background:' + driver.color + '">' + initials(driver.name) + '</span>' +
'<span class="col-title"><strong>' + driver.name + '</strong>' +
'<span class="dstatus ' + st + '"><i></i>' + stTxt + '</span></span>' +
'<span class="col-count">' + list.length + '</span>' +
'</div>';
} else {
head =
'<div class="col-head">' +
'<span class="col-avatar tag-orders">⬡</span>' +
'<span class="col-title"><strong>Unassigned</strong><small>Awaiting dispatch</small></span>' +
'<span class="col-count">' + list.length + '</span>' +
'</div>';
}
col.innerHTML = head + '<div class="col-body" role="list"></div>';
var body = $(".col-body", col);
if (visible.length === 0) {
var empty = document.createElement("div");
empty.className = "col-empty";
empty.textContent = list.length === 0
? (driver ? "No active runs" : "All caught up — drop orders here")
: "No orders match the filter";
body.appendChild(empty);
} else {
visible.forEach(function (o) { body.appendChild(cardEl(o)); });
}
return col;
}
function renderSidebar() {
driverList.innerHTML = "";
drivers.forEach(function (d) {
var li = document.createElement("li");
li.className = "driver";
var st = d.status === "ok" ? "ok" : d.status === "warn" ? "warn" : "off";
var stTxt = d.status === "ok" ? "Available" : d.status === "warn" ? "On run" : "On break";
li.innerHTML =
'<span class="av" style="background:' + d.color + '">' + initials(d.name) + '</span>' +
'<span class="meta"><strong>' + d.name + '</strong>' +
'<span class="dstatus ' + st + '"><i></i>' + stTxt + ' · ' + d.vehicle + '</span></span>' +
'<span class="load">' + loadFor(d.id) + '</span>';
driverList.appendChild(li);
});
}
function updateStats() {
var unassigned = orders.filter(function (o) { return o.driver === null; }).length;
var assigned = orders.length - unassigned;
var online = drivers.filter(function (d) { return d.status !== "off"; }).length;
$("#statUnassigned").textContent = unassigned;
$("#statAssigned").textContent = assigned;
$("#statDrivers").textContent = online;
}
/* ---------- Assign logic ---------- */
function assign(orderId, driverId, silent) {
var o = null;
for (var i = 0; i < orders.length; i++) if (orders[i].id === orderId) o = orders[i];
if (!o) return;
var prev = o.driver;
if (prev === driverId) return;
o.driver = driverId;
render();
if (silent) return;
if (driverId === null) {
toast(o.id + " returned to unassigned", "warn");
} else {
var d = driverById(driverId);
var verb = prev ? "reassigned to" : "assigned to";
toast(o.id + " " + verb + " " + d.name, "ok");
}
}
/* ---------- Assign modal ---------- */
var modal = $("#assignModal");
var modalOpts = $("#assignOptions");
var modalSub = $("#assignSub");
var lastFocus = null;
function openAssign(orderId) {
var o = null;
for (var i = 0; i < orders.length; i++) if (orders[i].id === orderId) o = orders[i];
if (!o) return;
lastFocus = document.activeElement;
modalSub.textContent = "Choose a driver for " + o.id + " (" + o.pickup + " → " + o.drop + ").";
modalOpts.innerHTML = "";
// Unassign option if already assigned
if (o.driver) {
modalOpts.appendChild(optionBtn(orderId, null, "Unassigned", "Return to dispatch pool", "#16181d", "↺"));
}
drivers.forEach(function (d) {
var disabled = d.id === o.driver;
var sub = (d.status === "off" ? "On break · " : "") + d.vehicle + " · " + loadFor(d.id) + " active";
modalOpts.appendChild(optionBtn(orderId, d.id, d.name + (disabled ? " (current)" : ""), sub, d.color, initials(d.name), disabled));
});
modal.hidden = false;
var first = modalOpts.querySelector(".assign-opt:not(:disabled)");
if (first) first.focus();
document.addEventListener("keydown", onModalKey);
}
function optionBtn(orderId, driverId, name, sub, color, badge, disabled) {
var li = document.createElement("li");
var b = document.createElement("button");
b.className = "assign-opt";
b.disabled = !!disabled;
b.innerHTML =
'<span class="av" style="background:' + color + '">' + badge + '</span>' +
'<span class="meta"><strong>' + name + '</strong><span>' + sub + '</span></span>';
b.addEventListener("click", function () {
closeAssign();
assign(orderId, driverId);
});
li.appendChild(b);
return li;
}
function closeAssign() {
modal.hidden = true;
document.removeEventListener("keydown", onModalKey);
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
function onModalKey(e) { if (e.key === "Escape") closeAssign(); }
modal.addEventListener("click", function (e) {
if (e.target.hasAttribute("data-close")) closeAssign();
});
/* ---------- Drag & drop ---------- */
var dragId = null;
function wireDnD() {
var cards = board.querySelectorAll(".card");
cards.forEach(function (c) {
c.addEventListener("dragstart", function (e) {
dragId = c.getAttribute("data-id");
c.classList.add("dragging");
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", dragId);
}
});
c.addEventListener("dragend", function () {
c.classList.remove("dragging");
dragId = null;
clearDragOver();
});
});
var bodies = board.querySelectorAll(".col-body");
bodies.forEach(function (body) {
var col = body.closest(".column");
body.addEventListener("dragover", function (e) {
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
body.classList.add("drag-over");
});
body.addEventListener("dragleave", function () { body.classList.remove("drag-over"); });
body.addEventListener("drop", function (e) {
e.preventDefault();
body.classList.remove("drag-over");
var id = dragId || (e.dataTransfer && e.dataTransfer.getData("text/plain"));
if (!id) return;
var target = col.getAttribute("data-driver") || null;
assign(id, target);
});
});
}
function clearDragOver() {
board.querySelectorAll(".drag-over").forEach(function (b) { b.classList.remove("drag-over"); });
}
/* ---------- Delegated clicks (assign buttons + keyboard) ---------- */
board.addEventListener("click", function (e) {
var btn = e.target.closest("[data-assign]");
if (btn) { openAssign(btn.getAttribute("data-assign")); return; }
});
board.addEventListener("keydown", function (e) {
if ((e.key === "Enter" || e.key === " ") && e.target.classList.contains("card")) {
e.preventDefault();
openAssign(e.target.getAttribute("data-id"));
}
});
/* ---------- Filters ---------- */
var chips = document.querySelectorAll(".chip");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) { c.classList.remove("is-active"); c.setAttribute("aria-pressed", "false"); });
chip.classList.add("is-active");
chip.setAttribute("aria-pressed", "true");
state.prio = chip.getAttribute("data-prio");
render();
});
});
/* ---------- Search ---------- */
var search = $("#search");
var debounce;
search.addEventListener("input", function () {
clearTimeout(debounce);
debounce = setTimeout(function () {
state.query = search.value.trim().toLowerCase();
render();
}, 120);
});
/* ---------- Simulated live ETA ticking ---------- */
setInterval(function () {
var changed = false;
orders.forEach(function (o) {
var m = /^(\d+)m$/.exec(o.eta);
if (m) {
var v = parseInt(m[1], 10) - 1;
o.eta = (v <= 1 ? 1 : v) + "m";
changed = true;
}
});
if (changed) render();
}, 12000);
/* ---------- Go ---------- */
render();
setTimeout(function () { toast("Dispatch board live · " + orders.filter(function (o) { return !o.driver; }).length + " orders waiting", "brand"); }, 500);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Foodhy Dispatch — Live Board</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>
<a class="skip-link" href="#board">Skip to dispatch board</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◆</span>
<div class="brand-text">
<strong>Foodhy Dispatch</strong>
<span>Northgate hub · Live</span>
</div>
</div>
<div class="topbar-stats" role="group" aria-label="Live counts">
<div class="stat">
<span class="stat-num" id="statUnassigned">0</span>
<span class="stat-label">Unassigned</span>
</div>
<div class="stat">
<span class="stat-num" id="statAssigned">0</span>
<span class="stat-label">In progress</span>
</div>
<div class="stat">
<span class="stat-num" id="statDrivers">0</span>
<span class="stat-label">Online</span>
</div>
</div>
<div class="topbar-tools">
<label class="search" aria-label="Search orders">
<span aria-hidden="true">⌕</span>
<input type="search" id="search" placeholder="Search order, customer, area…" autocomplete="off" />
</label>
<div class="filters" role="group" aria-label="Filter by priority">
<button class="chip is-active" data-prio="all" aria-pressed="true">All</button>
<button class="chip" data-prio="express" aria-pressed="false">Express</button>
<button class="chip" data-prio="standard" aria-pressed="false">Standard</button>
<button class="chip" data-prio="scheduled" aria-pressed="false">Scheduled</button>
</div>
</div>
</header>
<main class="layout">
<section id="board" class="board" aria-label="Dispatch board">
<!-- columns injected by JS -->
</section>
<aside class="sidebar" aria-label="Driver status">
<h2 class="sidebar-title">Drivers</h2>
<ul class="driver-list" id="driverList"></ul>
<div class="legend" aria-hidden="true">
<span><i class="dot ok"></i>Available</span>
<span><i class="dot warn"></i>On run</span>
<span><i class="dot off"></i>Break</span>
</div>
</aside>
</main>
<!-- Assign dialog -->
<div class="modal" id="assignModal" hidden>
<div class="modal-backdrop" data-close></div>
<div class="modal-card" role="dialog" aria-modal="true" aria-labelledby="assignTitle">
<header class="modal-head">
<h3 id="assignTitle">Assign order</h3>
<button class="icon-btn" data-close aria-label="Close">✕</button>
</header>
<p class="modal-sub" id="assignSub">Choose a driver for this delivery.</p>
<ul class="assign-options" id="assignOptions"></ul>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Dispatch Board
A self-contained operations screen for a fictional delivery hub. The board is a horizontal kanban: a highlighted Unassigned column sits first, followed by one queue per courier. Every order card carries an ID, a priority pill (Express, Standard, or Scheduled), a live ETA, a two-leg route with pickup and drop-off markers, and the customer name. A sticky toolbar keeps the live unassigned, in-progress, and online-driver counts in view, plus a search field and priority filter chips.
Dispatching works two ways, both vanilla JavaScript with no dependencies. Drag any card onto a driver column (or back to Unassigned) to reassign it, with a dashed drop zone and toast confirmation; or use the per-card Assign button to open an accessible driver-picker dialog that lists each courier’s vehicle, current load, and availability. Counts, column tallies, and the driver sidebar all recompute on every move, and a lightweight timer ticks down the minute-based ETAs to feel live.
The right-hand sidebar mirrors driver status with colored availability dots (available, on a run, on break) and an active-order badge per courier. Cards are keyboard-operable with visible focus rings, the dialog traps Escape-to-close and restores focus, and the layout reflows from a desktop multi-column board down to a stacked, swipeable view at narrow widths.
Illustrative UI only — fictional brand, not a real delivery service.