Auto — Shop Dashboard
A service-manager dashboard for an auto repair shop, blending industrial garage styling with status-forward data. It surfaces four headline KPIs, an animated revenue and labor-hours chart, a clickable service-mix donut, live bay-utilization tiles that drill into vehicle, VIN, plate and work-order details, an approvals queue with one-tap authorization, and a top-technician efficiency board. A timeframe toggle redraws every chart and metric between today, week and month views.
MCP
Code
:root {
--garage: #141518;
--garage-2: #1f2127;
--steel: #5b6470;
--steel-l: #8a929d;
--orange: #ff6a13;
--orange-d: #e2540a;
--orange-50: #fff0e6;
--ink: #16181c;
--ink-2: #3b4049;
--muted: #737a85;
--bg: #f3f4f6;
--surface: #ffffff;
--line: rgba(20, 21, 24, 0.1);
--line-2: rgba(20, 21, 24, 0.18);
--ok: #2f9e6f;
--warn: #e0962a;
--danger: #d4493e;
--waiting: #e0962a;
--inprogress: #2b7fff;
--done: #2f9e6f;
--hold: #d4493e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 18px;
--sh-sm: 0 1px 2px rgba(20, 21, 24, 0.06), 0 1px 3px rgba(20, 21, 24, 0.05);
--sh-md: 0 6px 18px rgba(20, 21, 24, 0.08);
--sh-lg: 0 14px 40px rgba(20, 21, 24, 0.14);
}
* { 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;
}
.tnum { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1; }
h1, h2 { margin: 0; }
button { font-family: inherit; cursor: pointer; }
/* ---------- Layout ---------- */
.app {
display: grid;
grid-template-columns: 248px 1fr;
min-height: 100vh;
}
/* ---------- Sidebar ---------- */
.rail {
background: var(--garage);
color: #e8eaee;
display: flex;
flex-direction: column;
padding: 20px 16px;
position: sticky;
top: 0;
height: 100vh;
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.rail-brand {
display: flex;
align-items: center;
gap: 11px;
padding: 4px 8px 22px;
}
.rail-logo {
width: 38px;
height: 38px;
border-radius: 10px;
display: grid;
place-items: center;
background: linear-gradient(135deg, var(--orange), var(--orange-d));
color: #fff;
box-shadow: 0 4px 14px rgba(255, 106, 19, 0.4);
flex: none;
}
.rail-name { font-weight: 800; font-size: 16px; letter-spacing: -0.2px; }
.rail-nav { display: flex; flex-direction: column; gap: 2px; }
.rail-link {
display: flex;
align-items: center;
gap: 11px;
padding: 10px 12px;
border-radius: var(--r-sm);
color: var(--steel-l);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.15s, color 0.15s;
}
.rail-link .dot {
width: 7px; height: 7px; border-radius: 50%;
background: currentColor; opacity: 0.55; flex: none;
}
.rail-link:hover { background: rgba(255, 255, 255, 0.05); color: #fff; }
.rail-link.is-active {
background: rgba(255, 106, 19, 0.14);
color: #fff;
}
.rail-link.is-active .dot { background: var(--orange); opacity: 1; }
.rail-foot { margin-top: auto; padding-top: 18px; }
.rail-shop {
display: flex; align-items: center; gap: 11px;
padding: 12px;
background: var(--garage-2);
border-radius: var(--r-md);
border: 1px solid rgba(255, 255, 255, 0.06);
}
.rail-shop-ico {
width: 34px; height: 34px; border-radius: 9px; flex: none;
display: grid; place-items: center;
background: rgba(255, 255, 255, 0.08);
font-weight: 700; font-size: 12px; color: var(--orange);
}
.rail-shop strong { display: block; font-size: 13px; color: #fff; }
.rail-shop small { display: block; font-size: 11.5px; color: var(--steel-l); }
/* ---------- Main ---------- */
.main { min-width: 0; display: flex; flex-direction: column; }
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px 28px;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 20;
}
.topbar-l { display: flex; align-items: center; gap: 12px; }
.topbar-title { font-size: 19px; font-weight: 800; letter-spacing: -0.4px; }
.topbar-sub { margin: 1px 0 0; font-size: 12.5px; color: var(--muted); }
.topbar-r { display: flex; align-items: center; gap: 12px; }
.icon-btn {
width: 38px; height: 38px; border-radius: var(--r-sm);
border: 1px solid var(--line); background: var(--surface);
display: grid; place-items: center; color: var(--ink-2);
}
.icon-btn:hover { background: var(--bg); }
.menu-btn { display: none; }
.seg {
display: inline-flex; background: var(--bg);
border: 1px solid var(--line); border-radius: var(--r-sm);
padding: 3px;
}
.seg-btn {
border: 0; background: transparent; color: var(--muted);
font-size: 13px; font-weight: 600; padding: 6px 14px;
border-radius: 6px; transition: all 0.15s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-active {
background: var(--surface); color: var(--ink);
box-shadow: var(--sh-sm);
}
.ghost-btn {
display: inline-flex; align-items: center; gap: 7px;
border: 1px solid var(--line-2); background: var(--surface);
color: var(--ink-2); font-size: 13px; font-weight: 600;
padding: 8px 14px; border-radius: var(--r-sm);
transition: all 0.15s;
}
.ghost-btn:hover { background: var(--garage); color: #fff; border-color: var(--garage); }
.avatar {
width: 38px; height: 38px; border-radius: 50%; flex: none;
display: grid; place-items: center;
background: linear-gradient(135deg, var(--steel), var(--garage-2));
color: #fff; font-weight: 700; font-size: 13px;
}
.content { padding: 24px 28px 36px; }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
box-shadow: var(--sh-sm);
position: relative;
overflow: hidden;
}
.kpi::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: var(--orange);
}
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-label { font-size: 12.5px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; }
.kpi-chip {
font-size: 11.5px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
}
.kpi-chip.up { color: var(--ok); background: rgba(47, 158, 111, 0.12); }
.kpi-chip.down { color: var(--danger); background: rgba(212, 73, 62, 0.12); }
.kpi-val {
display: block; font-size: 30px; font-weight: 800;
letter-spacing: -1px; margin: 10px 0 4px;
}
.kpi-pre { font-size: 19px; font-weight: 700; color: var(--steel); }
.kpi-foot { font-size: 12px; color: var(--muted); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 16px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px 20px;
box-shadow: var(--sh-sm);
}
.chart-card { grid-column: 1; grid-row: 1; }
.mix-card { grid-column: 2; grid-row: 1 / span 2; }
.bays-card { grid-column: 1; grid-row: 2; }
.appr-card { grid-column: 1; grid-row: 3; }
.techs-card { grid-column: 2; grid-row: 3; }
.card-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 12px; margin-bottom: 16px;
}
.card-title { font-size: 15.5px; font-weight: 700; letter-spacing: -0.2px; }
.card-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.card-tag {
font-size: 11.5px; font-weight: 700; color: var(--steel);
background: var(--bg); border: 1px solid var(--line);
padding: 4px 9px; border-radius: 999px; white-space: nowrap;
}
/* ---------- Chart ---------- */
.legend { display: flex; gap: 14px; }
.lg { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); font-weight: 500; }
.sw { width: 11px; height: 11px; border-radius: 3px; }
.sw-rev { background: var(--orange); }
.sw-hrs { background: var(--inprogress); }
.chart-wrap { width: 100%; }
.chart { width: 100%; height: 250px; display: block; }
.chart .bar { transition: y 0.5s cubic-bezier(.2,.7,.3,1), height 0.5s cubic-bezier(.2,.7,.3,1), fill 0.2s; cursor: pointer; }
.chart .bar:hover { fill: var(--orange-d); }
.chart .hline { stroke: var(--line); stroke-width: 1; }
.chart .area { fill: rgba(43, 127, 255, 0.1); transition: d 0.5s; }
.chart .line { fill: none; stroke: var(--inprogress); stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; transition: d 0.5s; }
.chart .pt { fill: var(--surface); stroke: var(--inprogress); stroke-width: 2.5; transition: cx 0.5s, cy 0.5s; }
.chart-axis {
display: flex; justify-content: space-between;
margin-top: 6px; font-size: 11px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ---------- Service mix donut ---------- */
.donut-wrap {
position: relative; width: 168px; height: 168px;
margin: 4px auto 18px;
}
.donut { width: 100%; height: 100%; transform: rotate(-90deg); }
.donut .seg { transition: stroke-width 0.2s; cursor: pointer; }
.donut .seg:hover { stroke-width: 16; }
.donut-center {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; text-align: center;
pointer-events: none;
}
.donut-center strong { font-size: 26px; font-weight: 800; letter-spacing: -0.5px; }
.donut-center small { font-size: 11.5px; color: var(--muted); font-weight: 600; }
.mix-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.mix-row {
display: flex; align-items: center; gap: 10px;
padding: 8px 6px; border-radius: var(--r-sm);
cursor: pointer; transition: background 0.15s;
}
.mix-row:hover { background: var(--bg); }
.mix-row.is-on { background: var(--orange-50); }
.mix-swatch { width: 10px; height: 10px; border-radius: 3px; flex: none; }
.mix-name { font-size: 13px; font-weight: 500; flex: 1; }
.mix-pct { font-size: 13px; font-weight: 700; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.mix-cnt { font-size: 11.5px; color: var(--muted); width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
/* ---------- Bays ---------- */
.bays {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.bay {
border: 1px solid var(--line); border-radius: var(--r-md);
padding: 12px; background: var(--surface); text-align: left;
display: flex; flex-direction: column; gap: 8px;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.1s;
}
.bay:hover { border-color: var(--line-2); box-shadow: var(--sh-sm); }
.bay:active { transform: translateY(1px); }
.bay.is-sel { border-color: var(--orange); box-shadow: 0 0 0 2px var(--orange-50); }
.bay-top { display: flex; align-items: center; justify-content: space-between; }
.bay-no { font-size: 13px; font-weight: 700; }
.bay-status {
width: 9px; height: 9px; border-radius: 50%;
}
.bay-status.inprogress { background: var(--inprogress); }
.bay-status.waiting { background: var(--waiting); }
.bay-status.done { background: var(--done); }
.bay-status.hold { background: var(--hold); }
.bay-status.free { background: var(--steel-l); }
.bay-meter {
height: 6px; border-radius: 999px; background: var(--bg); overflow: hidden;
}
.bay-meter span {
display: block; height: 100%; border-radius: 999px;
background: linear-gradient(90deg, var(--orange-d), var(--orange));
transition: width 0.5s cubic-bezier(.2,.7,.3,1);
}
.bay-pct { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.bay-detail {
margin-top: 14px; padding: 14px;
background: var(--garage); color: #e8eaee;
border-radius: var(--r-md);
}
.bay-detail-head {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 10px;
}
.bay-detail-head strong { font-size: 14px; color: #fff; }
.link-btn { border: 0; background: transparent; color: var(--orange); font-size: 12.5px; font-weight: 600; padding: 0; }
.link-btn:hover { text-decoration: underline; }
.bd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.bd-cell { background: var(--garage-2); border-radius: var(--r-sm); padding: 9px 11px; }
.bd-cell .k { font-size: 10.5px; color: var(--steel-l); text-transform: uppercase; letter-spacing: 0.4px; }
.bd-cell .v { font-size: 14px; font-weight: 700; color: #fff; font-variant-numeric: tabular-nums; }
.bd-cell .v.mono { letter-spacing: 0.4px; }
/* ---------- Approvals ---------- */
.appr-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
.appr {
display: flex; align-items: center; gap: 14px;
border: 1px solid var(--line); border-radius: var(--r-md);
padding: 12px 14px; transition: border-color 0.15s, opacity 0.3s, transform 0.3s;
}
.appr.is-leaving { opacity: 0; transform: translateX(12px); }
.appr-thumb {
width: 52px; height: 40px; border-radius: 8px; flex: none;
background: linear-gradient(135deg, var(--steel), var(--garage-2));
position: relative; overflow: hidden;
}
.appr-thumb::after {
content: ""; position: absolute; left: 8px; right: 8px; bottom: 7px; height: 9px;
border-radius: 4px; background: rgba(255, 255, 255, 0.18);
}
.appr-main { flex: 1; min-width: 0; }
.appr-veh { font-size: 13.5px; font-weight: 700; }
.appr-meta { font-size: 12px; color: var(--muted); display: flex; gap: 8px; flex-wrap: wrap; }
.appr-meta .plate {
font-weight: 700; color: var(--ink-2); letter-spacing: 0.5px;
background: var(--bg); border: 1px solid var(--line); padding: 0 5px; border-radius: 4px;
font-variant-numeric: tabular-nums;
}
.appr-meta .code { color: var(--danger); font-weight: 700; }
.appr-amt { font-size: 15px; font-weight: 800; font-variant-numeric: tabular-nums; white-space: nowrap; }
.appr-acts { display: flex; gap: 6px; flex: none; }
.mini-btn {
border: 1px solid var(--line-2); background: var(--surface);
font-size: 12px; font-weight: 600; padding: 7px 11px; border-radius: var(--r-sm);
transition: all 0.15s; color: var(--ink-2);
}
.mini-btn.approve { background: var(--ok); border-color: var(--ok); color: #fff; }
.mini-btn.approve:hover { background: #28865f; }
.mini-btn.decline:hover { background: rgba(212, 73, 62, 0.1); border-color: var(--danger); color: var(--danger); }
.appr-empty {
text-align: center; color: var(--muted); font-size: 13px;
padding: 22px 0; margin: 0;
}
/* ---------- Top techs ---------- */
.techs { list-style: none; margin: 0; padding: 0; counter-reset: t; display: flex; flex-direction: column; gap: 4px; }
.tech {
display: flex; align-items: center; gap: 12px;
padding: 9px 6px; border-radius: var(--r-sm);
transition: background 0.15s;
}
.tech:hover { background: var(--bg); }
.tech-rank {
width: 24px; height: 24px; border-radius: 7px; flex: none;
display: grid; place-items: center; font-size: 12px; font-weight: 700;
background: var(--bg); color: var(--steel); border: 1px solid var(--line);
}
.tech:nth-child(1) .tech-rank { background: var(--orange); color: #fff; border-color: var(--orange); }
.tech-av {
width: 32px; height: 32px; border-radius: 50%; flex: none;
display: grid; place-items: center; font-size: 12px; font-weight: 700; color: #fff;
}
.tech-main { flex: 1; min-width: 0; }
.tech-name { font-size: 13px; font-weight: 600; }
.tech-sub { font-size: 11.5px; color: var(--muted); }
.tech-bar { height: 5px; border-radius: 999px; background: var(--bg); margin-top: 5px; overflow: hidden; }
.tech-bar span { display: block; height: 100%; border-radius: 999px; background: var(--orange); transition: width 0.6s; }
.tech-eff { font-size: 13px; font-weight: 800; font-variant-numeric: tabular-nums; flex: none; }
/* ---------- Toast ---------- */
.toast-host {
position: fixed; right: 20px; bottom: 20px; z-index: 100;
display: flex; flex-direction: column; gap: 10px; align-items: flex-end;
}
.toast {
display: flex; align-items: center; gap: 10px;
background: var(--garage); color: #fff;
padding: 11px 16px; border-radius: var(--r-md);
box-shadow: var(--sh-lg); font-size: 13px; font-weight: 500;
transform: translateY(10px); opacity: 0;
transition: transform 0.25s, opacity 0.25s;
border-left: 3px solid var(--orange);
max-width: 320px;
}
.toast.show { transform: translateY(0); opacity: 1; }
.toast.ok { border-left-color: var(--ok); }
.toast.warn { border-left-color: var(--warn); }
.toast.danger { border-left-color: var(--danger); }
/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
.grid { grid-template-columns: 1fr; }
.chart-card, .mix-card, .bays-card, .appr-card, .techs-card {
grid-column: auto; grid-row: auto;
}
}
@media (max-width: 880px) {
.app { grid-template-columns: 1fr; }
.rail {
position: fixed; left: 0; top: 0; z-index: 60; width: 248px;
transform: translateX(-100%); transition: transform 0.25s;
}
.rail.is-open { transform: translateX(0); box-shadow: var(--sh-lg); }
.menu-btn { display: grid; }
.kpis { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.topbar { padding: 12px 16px; flex-wrap: wrap; }
.topbar-r { width: 100%; justify-content: space-between; }
.content { padding: 16px 14px 28px; }
.kpis { grid-template-columns: 1fr 1fr; gap: 12px; }
.kpi-val { font-size: 25px; }
.bays { grid-template-columns: repeat(2, 1fr); }
.bd-grid { grid-template-columns: 1fr; }
.appr { flex-wrap: wrap; }
.appr-acts { width: 100%; }
.appr-acts .mini-btn { flex: 1; }
.ghost-btn { display: none; }
.seg-btn { padding: 6px 11px; }
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastHost = document.getElementById("toastHost");
function toast(msg, kind) {
var el = document.createElement("div");
el.className = "toast" + (kind ? " " + kind : "");
el.textContent = msg;
toastHost.appendChild(el);
requestAnimationFrame(function () { el.classList.add("show"); });
setTimeout(function () {
el.classList.remove("show");
setTimeout(function () { el.remove(); }, 280);
}, 2600);
}
function fmtMoney(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
/* ---------- Data per timeframe ---------- */
var DATA = {
today: {
sub: "Hourly intake — today",
labels: ["7a", "8a", "9a", "10a", "11a", "12p", "1p", "2p", "3p", "4p", "5p", "6p"],
revenue: [620, 980, 1340, 1810, 1520, 940, 1280, 1990, 2240, 1860, 1140, 700],
hours: [3, 5, 7, 9, 8, 5, 6, 10, 11, 9, 6, 4],
kpi: {
revenue: 18420, revenuePrev: 16990, revenueDelta: "8.4%", revDir: "up",
util: 82, utilPrev: 79, utilDelta: "3.1%", utilDir: "up",
ticket: 486, ticketDelta: "1.9%", ticketDir: "down",
cars: 38, carsPrev: 7, carsDelta: "12", carsDir: "up"
}
},
week: {
sub: "Daily revenue — this week",
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
revenue: [14200, 18420, 16800, 19940, 22100, 24800, 9200],
hours: [78, 96, 88, 104, 118, 132, 41],
kpi: {
revenue: 125460, revenuePrev: 118900, revenueDelta: "5.5%", revDir: "up",
util: 86, utilPrev: 81, utilDelta: "6.2%", utilDir: "up",
ticket: 512, ticketDelta: "2.4%", ticketDir: "up",
cars: 245, carsPrev: 18, carsDelta: "31", carsDir: "up"
}
},
month: {
sub: "Weekly revenue — June",
labels: ["W1", "W2", "W3", "W4"],
revenue: [108400, 125460, 119800, 131200],
hours: [612, 657, 631, 689],
kpi: {
revenue: 484860, revenuePrev: 451200, revenueDelta: "7.5%", revDir: "up",
util: 84, utilPrev: 80, utilDelta: "5.0%", utilDir: "up",
ticket: 498, ticketDelta: "0.8%", ticketDir: "down",
cars: 974, carsPrev: 22, carsDelta: "63", carsDir: "up"
}
}
};
/* ---------- Chart rendering ---------- */
var svgNS = "http://www.w3.org/2000/svg";
var chart = document.getElementById("revChart");
var chartAxis = document.getElementById("chartAxis");
var chartSub = document.getElementById("chartSub");
var VB = { w: 720, h: 260, padL: 8, padR: 8, padT: 16, padB: 18 };
function el(name, attrs) {
var n = document.createElementNS(svgNS, name);
for (var k in attrs) n.setAttribute(k, attrs[k]);
return n;
}
function renderChart(range) {
var d = DATA[range];
chartSub.textContent = d.sub;
while (chart.firstChild) chart.removeChild(chart.firstChild);
var n = d.revenue.length;
var maxRev = Math.max.apply(null, d.revenue) * 1.12;
var maxHrs = Math.max.apply(null, d.hours) * 1.18;
var plotW = VB.w - VB.padL - VB.padR;
var plotH = VB.h - VB.padT - VB.padB;
var slot = plotW / n;
var bw = Math.min(slot * 0.52, 34);
// gridlines
for (var g = 0; g <= 3; g++) {
var gy = VB.padT + (plotH * g) / 3;
chart.appendChild(el("line", { class: "hline", x1: VB.padL, y1: gy, x2: VB.w - VB.padR, y2: gy }));
}
// bars (revenue)
d.revenue.forEach(function (v, i) {
var h = (v / maxRev) * plotH;
var x = VB.padL + slot * i + slot / 2 - bw / 2;
var y = VB.padT + plotH - h;
var bar = el("rect", {
class: "bar", x: x, y: VB.padT + plotH, width: bw, height: 0,
rx: 4, fill: "var(--orange)"
});
bar.style.cursor = "pointer";
bar.addEventListener("click", function () {
toast(d.labels[i] + " · " + fmtMoney(v) + " · " + d.hours[i] + " labor hrs", "");
});
chart.appendChild(bar);
// animate
requestAnimationFrame(function () {
bar.setAttribute("y", y);
bar.setAttribute("height", h);
});
});
// line + area (hours)
var pts = d.hours.map(function (v, i) {
var x = VB.padL + slot * i + slot / 2;
var y = VB.padT + plotH - (v / maxHrs) * plotH;
return [x, y];
});
var linePath = pts.map(function (p, i) { return (i ? "L" : "M") + p[0].toFixed(1) + " " + p[1].toFixed(1); }).join(" ");
var areaPath = linePath + " L" + pts[pts.length - 1][0].toFixed(1) + " " + (VB.padT + plotH) +
" L" + pts[0][0].toFixed(1) + " " + (VB.padT + plotH) + " Z";
chart.appendChild(el("path", { class: "area", d: areaPath }));
chart.appendChild(el("path", { class: "line", d: linePath }));
pts.forEach(function (p) {
chart.appendChild(el("circle", { class: "pt", cx: p[0], cy: p[1], r: 3.5 }));
});
// axis labels
chartAxis.innerHTML = "";
d.labels.forEach(function (lab) {
var s = document.createElement("span");
s.textContent = lab;
chartAxis.appendChild(s);
});
}
/* ---------- KPIs ---------- */
function setText(sel, val) {
var n = document.querySelector(sel);
if (n) n.textContent = val;
}
function updateKpis(range) {
var k = DATA[range].kpi;
setText('[data-kpi="revenue"]', k.revenue.toLocaleString("en-US"));
setText('[data-kpi-prev="revenue"]', fmtMoney(k.revenuePrev));
setText('[data-kpi-delta="revenue"]', k.revenueDelta);
setText('[data-kpi="util"]', k.util);
setText('[data-kpi-prev="util"]', k.utilPrev + "%");
setText('[data-kpi-delta="util"]', k.utilDelta);
setText('[data-kpi="ticket"]', k.ticket);
setText('[data-kpi-delta="ticket"]', k.ticketDelta);
setText('[data-kpi="cars"]', k.cars);
setText('[data-kpi-prev="cars"]', k.carsPrev);
setText('[data-kpi-delta="cars"]', k.carsDelta);
setChip('[data-kpi-delta="revenue"]', k.revDir);
setChip('[data-kpi-delta="util"]', k.utilDir);
setChip('[data-kpi-delta="ticket"]', k.ticketDir);
setChip('[data-kpi-delta="cars"]', k.carsDir);
}
function setChip(sel, dir) {
var span = document.querySelector(sel);
if (!span) return;
var chip = span.closest(".kpi-chip");
if (!chip) return;
chip.classList.toggle("up", dir === "up");
chip.classList.toggle("down", dir === "down");
chip.childNodes[0].textContent = dir === "up" ? "▲ " : "▼ ";
}
/* ---------- Service mix donut ---------- */
var MIX = [
{ name: "Maintenance", count: 16, color: "#ff6a13" },
{ name: "Brakes & Suspension", count: 8, color: "#2b7fff" },
{ name: "Diagnostics", count: 6, color: "#2f9e6f" },
{ name: "Tires & Alignment", count: 5, color: "#e0962a" },
{ name: "Body & Detail", count: 3, color: "#8a929d" }
];
var donut = document.getElementById("donut");
var donutVal = document.getElementById("donutVal");
var donutLabel = document.getElementById("donutLabel");
var mixList = document.getElementById("mixList");
var mixTotal = document.getElementById("mixTotal");
function renderMix() {
var total = MIX.reduce(function (a, b) { return a + b.count; }, 0);
mixTotal.textContent = total + " ROs";
var R = 50, C = 2 * Math.PI * R;
var offset = 0;
while (donut.firstChild) donut.removeChild(donut.firstChild);
donut.appendChild(el("circle", { cx: 60, cy: 60, r: R, fill: "none", stroke: "var(--bg)", "stroke-width": 12 }));
mixList.innerHTML = "";
MIX.forEach(function (m, i) {
var pct = m.count / total;
var dash = pct * C;
var seg = el("circle", {
class: "seg", cx: 60, cy: 60, r: R, fill: "none",
stroke: m.color, "stroke-width": 12,
"stroke-dasharray": dash + " " + (C - dash),
"stroke-dashoffset": -offset
});
donut.appendChild(seg);
offset += dash;
var li = document.createElement("li");
li.className = "mix-row";
li.tabIndex = 0;
li.setAttribute("role", "button");
li.innerHTML =
'<span class="mix-swatch" style="background:' + m.color + '"></span>' +
'<span class="mix-name">' + m.name + '</span>' +
'<span class="mix-pct">' + Math.round(pct * 100) + '%</span>' +
'<span class="mix-cnt">' + m.count + '</span>';
function focusSeg() {
donutVal.textContent = Math.round(pct * 100) + "%";
donutLabel.textContent = m.name;
Array.prototype.forEach.call(mixList.children, function (c) { c.classList.remove("is-on"); });
li.classList.add("is-on");
}
li.addEventListener("mouseenter", focusSeg);
li.addEventListener("click", focusSeg);
seg.addEventListener("mouseenter", focusSeg);
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); focusSeg(); }
});
mixList.appendChild(li);
if (i === 0) { donutVal.textContent = Math.round(pct * 100) + "%"; donutLabel.textContent = m.name; }
});
mixList.children[0].classList.add("is-on");
}
/* ---------- Bays ---------- */
var BAYS = [
{ no: "Bay 1", util: 94, status: "inprogress", veh: "2021 Ford F-150", plate: "TRK 4821", vin: "1FTFW1E5XMF", odo: "62,140", job: "Brake job + rotors", tech: "L. Ortega", eta: "1h 10m" },
{ no: "Bay 2", util: 88, status: "inprogress", veh: "2019 Honda CR-V", plate: "GHT 119", vin: "5J6RW2H58KL", odo: "48,902", job: "60k service", tech: "P. Nair", eta: "45m" },
{ no: "Bay 3", util: 76, status: "waiting", veh: "2022 Tesla Model 3", plate: "EV 7702", vin: "5YJ3E1EA4NF", odo: "21,330", job: "Tire rotation", tech: "Unassigned", eta: "—" },
{ no: "Bay 4", util: 100, status: "inprogress", veh: "2017 RAM 2500", plate: "HVY 308", vin: "3C6UR5DL2HG", odo: "118,455", job: "Diagnostic P0301", tech: "D. Cole", eta: "2h" },
{ no: "Bay 5", util: 62, status: "done", veh: "2020 Subaru Outback", plate: "OUT 556", vin: "4S4BTACC6L3", odo: "39,710", job: "Oil + filter", tech: "S. Kim", eta: "Ready" },
{ no: "Bay 6", util: 71, status: "hold", veh: "2018 BMW 330i", plate: "BMR 990", vin: "WBA8E1C57JA", odo: "71,200", job: "Awaiting part", tech: "L. Ortega", eta: "Hold" },
{ no: "Bay 7", util: 0, status: "free", veh: "—", plate: "—", vin: "—", odo: "—", job: "Open", tech: "—", eta: "—" },
{ no: "Bay 8", util: 84, status: "inprogress", veh: "2023 Toyota RAV4", plate: "RAV 401", vin: "2T3P1RFV0PC", odo: "9,840", job: "Alignment", tech: "P. Nair", eta: "30m" }
];
var bayList = document.getElementById("bayList");
var bayDetail = document.getElementById("bayDetail");
var bayDetailTitle = document.getElementById("bayDetailTitle");
var bayDetailBody = document.getElementById("bayDetailBody");
var selectedBay = null;
function renderBays() {
bayList.innerHTML = "";
BAYS.forEach(function (b, i) {
var btn = document.createElement("button");
btn.className = "bay";
btn.type = "button";
btn.setAttribute("aria-label", b.no + ", " + b.util + " percent utilized");
btn.innerHTML =
'<div class="bay-top"><span class="bay-no">' + b.no + '</span>' +
'<span class="bay-status ' + b.status + '" title="' + b.status + '"></span></div>' +
'<div class="bay-meter"><span style="width:0%"></span></div>' +
'<span class="bay-pct">' + b.util + '% · ' + statusLabel(b.status) + '</span>';
btn.addEventListener("click", function () { selectBay(i, btn); });
bayList.appendChild(btn);
var fill = btn.querySelector(".bay-meter span");
requestAnimationFrame(function () { fill.style.width = b.util + "%"; });
});
}
function statusLabel(s) {
return { inprogress: "In Progress", waiting: "Waiting", done: "Done", hold: "On Hold", free: "Open" }[s] || s;
}
function selectBay(i, btn) {
var b = BAYS[i];
Array.prototype.forEach.call(bayList.children, function (c) { c.classList.remove("is-sel"); });
btn.classList.add("is-sel");
selectedBay = i;
bayDetailTitle.textContent = b.no + " — " + statusLabel(b.status);
bayDetailBody.innerHTML =
'<div class="bd-grid">' +
cell("Vehicle", b.veh) +
cell("Plate", b.plate, true) +
cell("VIN", b.vin, true) +
cell("Odometer", b.odo + " mi", true) +
cell("Current Job", b.job) +
cell("Technician", b.tech) +
cell("ETA", b.eta) +
cell("Utilization", b.util + "%", true) +
'</div>';
bayDetail.hidden = false;
bayDetail.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
function cell(k, v, mono) {
return '<div class="bd-cell"><div class="k">' + k + '</div><div class="v' + (mono ? " mono" : "") + '">' + v + '</div></div>';
}
document.getElementById("bayClose").addEventListener("click", function () {
bayDetail.hidden = true;
if (selectedBay != null && bayList.children[selectedBay]) bayList.children[selectedBay].classList.remove("is-sel");
selectedBay = null;
});
/* ---------- Approvals ---------- */
var APPROVALS = [
{ veh: "2016 Jeep Wrangler", plate: "JEP 220", code: "P0420", desc: "Catalytic converter + labor", amt: 1480, cust: "R. Mendez" },
{ veh: "2019 Audi Q5", plate: "AUD 731", code: "Brakes", desc: "Front pads, rotors, fluid", amt: 920, cust: "T. Wallace" },
{ veh: "2014 Chevy Silverado", plate: "SLV 045", code: "P0301", desc: "Coil pack + plugs, cyl 1 misfire", amt: 640, cust: "K. Boone" }
];
var apprList = document.getElementById("apprList");
var apprSub = document.getElementById("apprSub");
var apprEmpty = document.getElementById("apprEmpty");
function renderApprovals() {
apprList.innerHTML = "";
APPROVALS.forEach(function (a, i) {
var li = document.createElement("li");
li.className = "appr";
li.innerHTML =
'<div class="appr-thumb" aria-hidden="true"></div>' +
'<div class="appr-main">' +
'<div class="appr-veh">' + a.veh + '</div>' +
'<div class="appr-meta"><span class="plate">' + a.plate + '</span>' +
'<span class="code">' + a.code + '</span><span>' + a.desc + '</span></div>' +
'</div>' +
'<div class="appr-amt">' + fmtMoney(a.amt) + '</div>' +
'<div class="appr-acts">' +
'<button class="mini-btn decline" type="button">Decline</button>' +
'<button class="mini-btn approve" type="button">Approve</button>' +
'</div>';
var approveBtn = li.querySelector(".approve");
var declineBtn = li.querySelector(".decline");
approveBtn.addEventListener("click", function () { resolveAppr(li, a, "approved"); });
declineBtn.addEventListener("click", function () { resolveAppr(li, a, "declined"); });
apprList.appendChild(li);
});
updateApprCount();
}
function resolveAppr(li, a, action) {
li.classList.add("is-leaving");
if (action === "approved") toast("Estimate approved · " + a.veh + " · " + fmtMoney(a.amt), "ok");
else toast("Estimate declined · " + a.veh, "danger");
setTimeout(function () {
li.remove();
updateApprCount();
}, 320);
}
function updateApprCount() {
var n = apprList.querySelectorAll(".appr:not(.is-leaving)").length;
apprSub.textContent = n + (n === 1 ? " estimate" : " estimates") + " awaiting authorization";
apprEmpty.hidden = n !== 0;
}
/* ---------- Top techs ---------- */
var TECHS = [
{ name: "Lena Ortega", role: "Master Tech · 4 ROs", eff: 118, color: "#ff6a13" },
{ name: "Priya Nair", role: "A-Tech · 5 ROs", eff: 109, color: "#2b7fff" },
{ name: "Dwayne Cole", role: "Diagnostics · 3 ROs", eff: 102, color: "#2f9e6f" },
{ name: "Sam Kim", role: "Lube Tech · 6 ROs", eff: 96, color: "#e0962a" }
];
var techList = document.getElementById("techList");
function renderTechs() {
var max = Math.max.apply(null, TECHS.map(function (t) { return t.eff; }));
techList.innerHTML = "";
TECHS.forEach(function (t, i) {
var initials = t.name.split(" ").map(function (w) { return w[0]; }).join("");
var li = document.createElement("li");
li.className = "tech";
li.innerHTML =
'<span class="tech-rank">' + (i + 1) + '</span>' +
'<span class="tech-av" style="background:' + t.color + '">' + initials + '</span>' +
'<div class="tech-main"><div class="tech-name">' + t.name + '</div>' +
'<div class="tech-sub">' + t.role + '</div>' +
'<div class="tech-bar"><span style="width:0%"></span></div></div>' +
'<span class="tech-eff">' + t.eff + '%</span>';
techList.appendChild(li);
var bar = li.querySelector(".tech-bar span");
requestAnimationFrame(function () { bar.style.width = (t.eff / max * 100) + "%"; });
});
}
/* ---------- Timeframe toggle ---------- */
var segBtns = document.querySelectorAll(".seg-btn");
Array.prototype.forEach.call(segBtns, function (b) {
b.addEventListener("click", function () {
Array.prototype.forEach.call(segBtns, function (x) {
x.classList.remove("is-active");
x.setAttribute("aria-selected", "false");
});
b.classList.add("is-active");
b.setAttribute("aria-selected", "true");
var range = b.getAttribute("data-range");
renderChart(range);
updateKpis(range);
toast("Showing " + b.textContent.toLowerCase() + " metrics", "");
});
});
/* ---------- Export + menu ---------- */
document.getElementById("exportBtn").addEventListener("click", function () {
toast("Shop report queued — PDF will email to you", "ok");
});
var rail = document.querySelector(".rail");
document.getElementById("menuBtn").addEventListener("click", function () {
rail.classList.toggle("is-open");
});
document.addEventListener("click", function (e) {
if (window.innerWidth <= 880 && rail.classList.contains("is-open") &&
!rail.contains(e.target) && !e.target.closest("#menuBtn")) {
rail.classList.remove("is-open");
}
});
/* ---------- Init ---------- */
renderChart("today");
updateKpis("today");
renderMix();
renderBays();
renderApprovals();
renderTechs();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Torque & Co. — Shop Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Sidebar -->
<aside class="rail" aria-label="Primary">
<div class="rail-brand">
<span class="rail-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</span>
<span class="rail-name">Torque & Co.</span>
</div>
<nav class="rail-nav">
<a href="#" class="rail-link is-active"><span class="dot" aria-hidden="true"></span>Dashboard</a>
<a href="#" class="rail-link"><span class="dot" aria-hidden="true"></span>Work Orders</a>
<a href="#" class="rail-link"><span class="dot" aria-hidden="true"></span>Bays</a>
<a href="#" class="rail-link"><span class="dot" aria-hidden="true"></span>Parts & Inventory</a>
<a href="#" class="rail-link"><span class="dot" aria-hidden="true"></span>Customers</a>
<a href="#" class="rail-link"><span class="dot" aria-hidden="true"></span>Reports</a>
</nav>
<div class="rail-foot">
<div class="rail-shop">
<span class="rail-shop-ico" aria-hidden="true">TC</span>
<div>
<strong>Lot 7 — Northgate</strong>
<small>Open · 7:00–19:00</small>
</div>
</div>
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div class="topbar-l">
<button class="icon-btn menu-btn" id="menuBtn" aria-label="Toggle navigation">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
</button>
<div>
<h1 class="topbar-title">Shop Dashboard</h1>
<p class="topbar-sub">Tuesday, 16 June 2026 · Live</p>
</div>
</div>
<div class="topbar-r">
<div class="seg" role="tablist" aria-label="Timeframe">
<button class="seg-btn is-active" role="tab" aria-selected="true" data-range="today">Today</button>
<button class="seg-btn" role="tab" aria-selected="false" data-range="week">Week</button>
<button class="seg-btn" role="tab" aria-selected="false" data-range="month">Month</button>
</div>
<button class="ghost-btn" id="exportBtn">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
Export
</button>
<div class="avatar" title="Marco Devlin — Service Manager">MD</div>
</div>
</header>
<div class="content">
<!-- KPIs -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Revenue</span>
<span class="kpi-chip up">▲ <span data-kpi-delta="revenue">8.4%</span></span>
</div>
<strong class="kpi-val tnum"><span class="kpi-pre">$</span><span data-kpi="revenue">18,420</span></strong>
<span class="kpi-foot">vs <span data-kpi-prev="revenue">$16,990</span> prior</span>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Bay Utilization</span>
<span class="kpi-chip up">▲ <span data-kpi-delta="util">3.1%</span></span>
</div>
<strong class="kpi-val tnum"><span data-kpi="util">82</span><span class="kpi-pre">%</span></strong>
<span class="kpi-foot"><span data-kpi-prev="util">79%</span> last period</span>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Avg Ticket</span>
<span class="kpi-chip down">▼ <span data-kpi-delta="ticket">1.9%</span></span>
</div>
<strong class="kpi-val tnum"><span class="kpi-pre">$</span><span data-kpi="ticket">486</span></strong>
<span class="kpi-foot">per repair order</span>
</article>
<article class="kpi">
<div class="kpi-top">
<span class="kpi-label">Cars Serviced</span>
<span class="kpi-chip up">▲ <span data-kpi-delta="cars">12</span></span>
</div>
<strong class="kpi-val tnum"><span data-kpi="cars">38</span></strong>
<span class="kpi-foot"><span data-kpi-prev="cars">7</span> awaiting pickup</span>
</article>
</section>
<div class="grid">
<!-- Revenue chart -->
<section class="card chart-card" aria-label="Revenue trend">
<div class="card-head">
<div>
<h2 class="card-title">Revenue & Labor Hours</h2>
<p class="card-sub" id="chartSub">Hourly intake — today</p>
</div>
<div class="legend">
<span class="lg"><i class="sw sw-rev"></i>Revenue</span>
<span class="lg"><i class="sw sw-hrs"></i>Labor hrs</span>
</div>
</div>
<div class="chart-wrap">
<svg class="chart" id="revChart" viewBox="0 0 720 260" preserveAspectRatio="none" role="img" aria-label="Revenue trend chart"></svg>
</div>
<div class="chart-axis" id="chartAxis"></div>
</section>
<!-- Service mix -->
<section class="card mix-card" aria-label="Service type mix">
<div class="card-head">
<h2 class="card-title">Service Mix</h2>
<span class="card-tag" id="mixTotal">38 ROs</span>
</div>
<div class="donut-wrap">
<svg class="donut" id="donut" viewBox="0 0 120 120" role="img" aria-label="Service mix donut"></svg>
<div class="donut-center">
<strong id="donutVal" class="tnum">42%</strong>
<small id="donutLabel">Maintenance</small>
</div>
</div>
<ul class="mix-list" id="mixList"></ul>
</section>
<!-- Bay utilization -->
<section class="card bays-card" aria-label="Bay utilization">
<div class="card-head">
<div>
<h2 class="card-title">Bay Utilization</h2>
<p class="card-sub">Select a bay to drill in</p>
</div>
<span class="card-tag">8 bays</span>
</div>
<div class="bays" id="bayList"></div>
<div class="bay-detail" id="bayDetail" hidden>
<div class="bay-detail-head">
<strong id="bayDetailTitle">Bay 3</strong>
<button class="link-btn" id="bayClose">Close</button>
</div>
<div class="bay-detail-body" id="bayDetailBody"></div>
</div>
</section>
<!-- Pending approvals -->
<section class="card appr-card" aria-label="Pending approvals">
<div class="card-head">
<div>
<h2 class="card-title">Pending Approvals</h2>
<p class="card-sub" id="apprSub">3 estimates awaiting authorization</p>
</div>
</div>
<ul class="appr-list" id="apprList"></ul>
<p class="appr-empty" id="apprEmpty" hidden>All clear — no estimates waiting on sign-off.</p>
</section>
<!-- Top techs -->
<section class="card techs-card" aria-label="Top technicians">
<div class="card-head">
<h2 class="card-title">Top Technicians</h2>
<span class="card-tag">Efficiency</span>
</div>
<ol class="techs" id="techList"></ol>
</section>
</div>
</div>
</main>
</div>
<div class="toast-host" id="toastHost" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Shop Dashboard
A control-room view for the front-of-house service manager at a fictional repair shop. The layout pairs a dark garage-black rail with a light work area, leading on four KPI cards — revenue, bay utilization, average ticket and cars serviced — each with a trend chip versus the prior period. Below them, an SVG chart animates revenue bars against a labor-hours line, while a donut breaks down the service mix by repair-order type. All figures use tabular numerals so prices, percentages and odometer readings stay column-aligned.
The dashboard is built to be driven. The Today / Week / Month segmented toggle redraws the chart and rewrites every KPI in place, flipping the up and down trend chips to match. Bay tiles show a live utilization meter and a colored status dot (Waiting, In Progress, Done, On Hold, Open); clicking one opens a drill-down panel with the vehicle, plate, VIN, odometer, current job, assigned tech and ETA. The pending-approvals queue lets you approve or decline each estimate — rows animate out, the count updates, and an all-clear state appears when the queue empties.
Everything is vanilla HTML, CSS and JavaScript with no build step or external libraries. Charts
are hand-drawn SVG, micro-interactions are CSS transitions, and a small toast() helper confirms
each action. The interface is keyboard-usable, AA-contrast, and collapses to a mobile layout with
a slide-in rail down to roughly 360px.
Illustrative UI only — fictional shop/dealership, not a real service system.