Auto — Technician Productivity
A service-bay technician productivity report for an auto repair shop. Shows a sortable scorecard of billed versus available hours, efficiency percentages, jobs closed and labor revenue per tech, plus an efficiency leaderboard and a billed-vs-available hours bar chart. Day, week and month timeframe toggles recompute every metric, and clicking any technician opens a drill-in drawer with an efficiency ring, open work orders and certifications. Pure HTML, CSS and vanilla JavaScript with no dependencies.
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.08);
--sh-md: 0 4px 14px rgba(20, 21, 24, 0.08), 0 1px 3px rgba(20, 21, 24, 0.06);
--sh-lg: 0 18px 50px rgba(20, 21, 24, 0.22);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "tnum" 0;
}
.tnum, .kpi-value, .ds-v, .dring-num, .ttable td.num, .b-bill, .ch-val, .bd-eff {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum" 1;
}
button { font-family: inherit; }
/* ---------- Top bar ---------- */
.topbar {
display: flex;
align-items: center;
gap: 22px;
padding: 0 24px;
height: 60px;
background: var(--garage);
color: #eef0f3;
position: sticky;
top: 0;
z-index: 20;
}
.brand { display: flex; align-items: center; gap: 11px; }
.brand-mark {
width: 36px; height: 36px;
display: grid; place-items: center;
border-radius: 10px;
background: var(--orange);
color: #fff;
box-shadow: 0 4px 12px rgba(255, 106, 19, 0.4);
}
.brand-txt { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name { font-weight: 800; font-size: 15px; letter-spacing: -0.01em; }
.brand-sub { font-size: 11px; color: var(--steel-l); font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; }
.topnav { display: flex; gap: 4px; margin-left: 8px; }
.topnav-link {
font-size: 13.5px; font-weight: 600;
color: var(--steel-l);
text-decoration: none;
padding: 7px 12px;
border-radius: var(--r-sm);
transition: background .15s, color .15s;
}
.topnav-link:hover { color: #fff; background: rgba(255, 255, 255, 0.06); }
.topnav-link.is-active { color: #fff; background: rgba(255, 255, 255, 0.1); }
.topbar-right { margin-left: auto; display: flex; align-items: center; gap: 16px; }
.seg {
display: inline-flex;
background: var(--garage-2);
border-radius: 10px;
padding: 3px;
gap: 2px;
}
.seg-btn {
border: 0; background: transparent;
color: var(--steel-l);
font-size: 13px; font-weight: 600;
padding: 6px 14px;
border-radius: 7px;
cursor: pointer;
transition: background .15s, color .15s;
}
.seg-btn:hover { color: #fff; }
.seg-btn.is-active { background: var(--orange); color: #fff; box-shadow: var(--sh-sm); }
.seg-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.avatar {
width: 34px; height: 34px;
border-radius: 50%;
display: grid; place-items: center;
background: linear-gradient(135deg, #3a4150, #232730);
color: #fff; font-weight: 700; font-size: 12.5px;
border: 1px solid rgba(255, 255, 255, 0.12);
}
/* ---------- Main ---------- */
.main { max-width: 1240px; margin: 0 auto; padding: 26px 24px 48px; }
.page-head {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 16px; margin-bottom: 20px;
}
.page-title { margin: 0; font-size: 23px; font-weight: 800; letter-spacing: -0.02em; }
.page-meta { margin: 4px 0 0; font-size: 13px; color: var(--muted); }
.page-meta span { color: var(--ink-2); font-weight: 600; }
.btn {
display: inline-flex; align-items: center; gap: 7px;
font-size: 13.5px; font-weight: 600;
border-radius: var(--r-sm);
padding: 9px 14px;
cursor: pointer;
border: 1px solid var(--line-2);
transition: background .15s, border-color .15s, transform .05s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.btn-ghost { background: var(--surface); color: var(--ink-2); }
.btn-ghost:hover { background: #fafafb; border-color: var(--steel); }
/* ---------- KPIs ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 18px;
}
.kpi {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 16px 14px;
box-shadow: var(--sh-sm);
display: flex; flex-direction: column; gap: 6px;
position: relative;
overflow: hidden;
}
.kpi::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: var(--steel);
}
.kpi-rev::before { background: var(--orange); }
.kpi-label { font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); }
.kpi-value { font-size: 28px; font-weight: 800; letter-spacing: -0.02em; line-height: 1; color: var(--ink); }
.kpi-foot { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; }
.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex: none; }
.dot-ok { background: var(--ok); }
.dot-warn { background: var(--warn); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.col-right { display: flex; flex-direction: column; gap: 18px; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-sm);
overflow: hidden;
}
.panel-head {
display: flex; align-items: center; justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px solid var(--line);
}
.panel-title { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.hint { font-size: 12px; color: var(--muted); }
/* ---------- Table ---------- */
.table-wrap { overflow-x: auto; }
.table-wrap:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
.ttable { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.ttable thead th {
text-align: right;
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
color: var(--muted);
padding: 11px 14px;
background: #fafafb;
border-bottom: 1px solid var(--line);
cursor: pointer;
white-space: nowrap;
user-select: none;
position: sticky; top: 0;
}
.ttable thead th.th-name { text-align: left; }
.ttable thead th:hover { color: var(--ink-2); }
.ttable thead th.is-sorted { color: var(--orange); }
.caret {
display: inline-block; width: 0; height: 0; margin-left: 4px;
vertical-align: middle; opacity: 0;
border-left: 4px solid transparent; border-right: 4px solid transparent;
}
.ttable thead th.is-sorted .caret { opacity: 1; }
.ttable thead th.is-sorted.asc .caret { border-bottom: 5px solid var(--orange); }
.ttable thead th.is-sorted.desc .caret { border-top: 5px solid var(--orange); }
.ttable tbody tr {
cursor: pointer;
transition: background .12s;
}
.ttable tbody tr:hover { background: var(--orange-50); }
.ttable tbody tr:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
.ttable td {
padding: 12px 14px;
border-bottom: 1px solid var(--line);
text-align: right;
white-space: nowrap;
color: var(--ink-2);
}
.ttable tbody tr:last-child td { border-bottom: 0; }
.td-name { text-align: left; }
.tech-cell { display: flex; align-items: center; gap: 11px; }
.tech-av {
width: 32px; height: 32px; border-radius: 9px; flex: none;
display: grid; place-items: center;
font-size: 12px; font-weight: 700; color: #fff;
}
.tech-meta { display: flex; flex-direction: column; line-height: 1.25; }
.tech-name { font-weight: 600; color: var(--ink); }
.tech-bay { font-size: 11.5px; color: var(--muted); }
.eff-cell { display: flex; align-items: center; justify-content: flex-end; gap: 9px; }
.eff-bar {
width: 64px; height: 7px; border-radius: 99px;
background: var(--line);
overflow: hidden; flex: none;
}
.eff-fill { height: 100%; border-radius: 99px; transition: width .6s cubic-bezier(.2,.8,.2,1); }
.eff-num { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 38px; }
.rev-num { font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
/* ---------- Leaderboard ---------- */
.board { list-style: none; margin: 0; padding: 8px; }
.bd-item {
display: flex; align-items: center; gap: 12px;
padding: 9px 10px;
border-radius: var(--r-md);
transition: background .12s;
cursor: pointer;
}
.bd-item:hover { background: #fafafb; }
.bd-rank {
width: 24px; height: 24px; flex: none;
border-radius: 7px;
display: grid; place-items: center;
font-size: 12px; font-weight: 800;
background: var(--garage-2); color: var(--steel-l);
}
.bd-item:nth-child(1) .bd-rank { background: var(--orange); color: #fff; }
.bd-item:nth-child(2) .bd-rank { background: #e8edf2; color: var(--steel); }
.bd-item:nth-child(3) .bd-rank { background: #f0e6d8; color: var(--orange-d); }
.bd-body { flex: 1; min-width: 0; }
.bd-name { font-size: 13.5px; font-weight: 600; }
.bd-track { height: 6px; border-radius: 99px; background: var(--line); margin-top: 5px; overflow: hidden; }
.bd-prog { height: 100%; border-radius: 99px; background: var(--orange); transition: width .6s cubic-bezier(.2,.8,.2,1); }
.bd-eff { font-size: 13.5px; font-weight: 800; color: var(--ink); min-width: 42px; text-align: right; }
/* ---------- Chart ---------- */
.legend { display: flex; gap: 14px; }
.lg { font-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; font-weight: 600; }
.sw { width: 11px; height: 11px; border-radius: 3px; display: inline-block; }
.sw-billed { background: var(--orange); }
.sw-avail { background: var(--line-2); }
.chart {
display: flex; align-items: flex-end; justify-content: space-around;
gap: 6px;
height: 188px;
padding: 16px 16px 12px;
}
.ch-col { display: flex; flex-direction: column; align-items: center; gap: 7px; flex: 1; height: 100%; justify-content: flex-end; }
.ch-bars { display: flex; align-items: flex-end; gap: 4px; height: 100%; width: 100%; justify-content: center; }
.ch-bar {
width: 14px; border-radius: 5px 5px 0 0;
transition: height .7s cubic-bezier(.2,.8,.2,1);
position: relative;
}
.ch-bar.avail { background: var(--line-2); }
.ch-bar.billed { background: var(--orange); cursor: pointer; }
.ch-bar.billed:hover { background: var(--orange-d); }
.ch-bar .ch-val {
position: absolute; top: -17px; left: 50%; transform: translateX(-50%);
font-size: 10.5px; font-weight: 700; color: var(--ink); white-space: nowrap;
opacity: 0; transition: opacity .15s;
}
.ch-bar:hover .ch-val { opacity: 1; }
.ch-lab { font-size: 11px; font-weight: 600; color: var(--muted); }
/* ---------- Drawer ---------- */
.drawer-scrim {
position: fixed; inset: 0;
background: rgba(20, 21, 24, 0.42);
z-index: 40;
opacity: 0; animation: fade .2s forwards;
}
@keyframes fade { to { opacity: 1; } }
.drawer {
position: fixed; top: 0; right: 0; bottom: 0;
width: 400px; max-width: 92vw;
background: var(--surface);
z-index: 50;
box-shadow: var(--sh-lg);
transform: translateX(102%);
transition: transform .3s cubic-bezier(.2,.8,.2,1);
display: flex; flex-direction: column;
}
.drawer.is-open { transform: translateX(0); }
.drawer-head {
display: flex; align-items: center; justify-content: space-between;
padding: 18px 20px;
background: var(--garage);
color: #fff;
}
.dh-id { display: flex; align-items: center; gap: 13px; }
.dh-avatar {
width: 46px; height: 46px; border-radius: 12px; flex: none;
display: grid; place-items: center;
font-size: 16px; font-weight: 800; color: #fff;
}
.dh-name { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.01em; }
.dh-role { margin: 2px 0 0; font-size: 12.5px; color: var(--steel-l); }
.icon-btn {
border: 0; background: rgba(255, 255, 255, 0.08); color: #fff;
width: 34px; height: 34px; border-radius: 9px;
display: grid; place-items: center; cursor: pointer;
transition: background .15s;
}
.icon-btn:hover { background: rgba(255, 255, 255, 0.18); }
.icon-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.drawer-body { padding: 20px; overflow-y: auto; }
.dring-wrap { display: flex; align-items: center; gap: 22px; margin-bottom: 22px; }
.dring {
width: 116px; height: 116px; flex: none; border-radius: 50%;
display: grid; place-content: center; text-align: center;
position: relative;
}
.dring::after {
content: ""; position: absolute; inset: 11px;
border-radius: 50%; background: var(--surface);
}
.dring-num { position: relative; z-index: 1; font-size: 26px; font-weight: 800; letter-spacing: -0.02em; }
.dring-cap { position: relative; z-index: 1; font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
.dstats { list-style: none; margin: 0; padding: 0; flex: 1; display: flex; flex-direction: column; gap: 9px; }
.dstats li { display: flex; align-items: center; justify-content: space-between; }
.ds-l { font-size: 12.5px; color: var(--muted); }
.ds-v { font-size: 14px; font-weight: 700; color: var(--ink); }
.dsub { margin: 18px 0 10px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }
.dsub:first-of-type { margin-top: 4px; }
.wo-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.wo {
display: flex; align-items: center; gap: 11px;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 10px 12px;
}
.wo-status {
width: 8px; height: 8px; border-radius: 50%; flex: none;
}
.wo-main { flex: 1; min-width: 0; }
.wo-veh { font-size: 13px; font-weight: 600; color: var(--ink); }
.wo-sub { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.wo-badge {
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
padding: 3px 8px; border-radius: 99px; white-space: nowrap;
}
.st-inprogress { background: rgba(43, 127, 255, 0.12); color: var(--inprogress); }
.st-waiting { background: rgba(224, 150, 42, 0.14); color: var(--waiting); }
.st-done { background: rgba(47, 158, 111, 0.13); color: var(--done); }
.st-hold { background: rgba(212, 73, 62, 0.13); color: var(--hold); }
.wo-badge.wo-status, .wo .wo-status.st-inprogress { background: var(--inprogress); }
.wo .wo-status.st-waiting { background: var(--waiting); }
.wo .wo-status.st-done { background: var(--done); }
.wo .wo-status.st-hold { background: var(--hold); }
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
font-size: 11.5px; font-weight: 600;
padding: 5px 11px;
border-radius: 99px;
background: var(--garage-2); color: #d7dce2;
letter-spacing: 0.01em;
}
/* ---------- Toast ---------- */
.toast {
position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(20px);
background: var(--garage); color: #fff;
font-size: 13.5px; font-weight: 600;
padding: 11px 18px; border-radius: 99px;
box-shadow: var(--sh-lg);
opacity: 0; pointer-events: none;
transition: opacity .22s, transform .22s;
z-index: 60;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.grid { grid-template-columns: 1fr; }
}
@media (max-width: 760px) {
.kpis { grid-template-columns: repeat(2, 1fr); }
.topnav { display: none; }
}
@media (max-width: 520px) {
.topbar { gap: 12px; padding: 0 14px; height: 56px; }
.brand-sub { display: none; }
.main { padding: 18px 14px 40px; }
.page-head { flex-direction: column; align-items: flex-start; gap: 12px; }
.seg-btn { padding: 6px 11px; }
.ttable td.col-rev, .ttable th[data-sort="revenue"],
.ttable td.col-avail, .ttable th[data-sort="available"] { display: none; }
.drawer { width: 100%; max-width: 100%; }
.dring-wrap { gap: 16px; }
}(function () {
"use strict";
var ALR = 145; // average labor rate $/hr
// --- Data: per-tech, per-timeframe (billed hours). Available derived. ---
var TECHS = [
{
id: "mreyes", name: "Marisol Reyes", bay: "Bay 1 · Master Tech", role: "ASE Master · Diagnostics",
color: "#ff6a13",
hours: { day: 8.4, week: 41.2, month: 168.5 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 5, week: 24, month: 96 },
certs: ["ASE Master", "L1 Adv. Engine", "EV/HEV", "A/C 609"],
orders: [
{ veh: "'21 RAM 1500", plate: "GHK-4471", code: "P0301 · Misfire", status: "inprogress", label: "In Progress", hrs: 2.4 },
{ veh: "'19 Civic Si", plate: "8TRV029", code: "Brake job", status: "waiting", label: "Waiting", hrs: 1.6 },
{ veh: "'22 F-150", plate: "TCK-9920", code: "Diagnostic", status: "done", label: "Done", hrs: 1.1 }
]
},
{
id: "dpham", name: "Davis Pham", bay: "Bay 2 · A-Tech",
role: "ASE A-Tech · Driveability", color: "#2b7fff",
hours: { day: 7.9, week: 39.6, month: 159.0 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 6, week: 27, month: 104 },
certs: ["ASE A6", "ASE A8", "TPMS"],
orders: [
{ veh: "'18 Camry LE", plate: "RDX-1188", code: "Timing chain", status: "inprogress", label: "In Progress", hrs: 4.2 },
{ veh: "'20 CX-5", plate: "MZD-5521", code: "Oil + filter", status: "done", label: "Done", hrs: 0.6 }
]
},
{
id: "tbright", name: "Tony Brightwater", bay: "Bay 3 · B-Tech",
role: "ASE B-Tech · General Service", color: "#2f9e6f",
hours: { day: 6.8, week: 35.1, month: 142.3 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 7, week: 31, month: 121 },
certs: ["ASE A4", "ASE A5", "Alignment"],
orders: [
{ veh: "'17 Altima", plate: "9PLM034", code: "Tie rod + align", status: "inprogress", label: "In Progress", hrs: 2.0 },
{ veh: "'23 Telluride", plate: "KIA-7783", code: "Rotation", status: "waiting", label: "Waiting", hrs: 0.5 }
]
},
{
id: "klindqv", name: "Karin Lindqvist", bay: "Bay 4 · A-Tech",
role: "ASE A-Tech · Electrical", color: "#e0962a",
hours: { day: 6.1, week: 33.8, month: 137.0 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 4, week: 19, month: 78 },
certs: ["ASE A6", "EV/HEV", "ADAS"],
orders: [
{ veh: "'22 Model 3", plate: "TSL-0042", code: "ADAS calib.", status: "hold", label: "On Hold", hrs: 3.0 },
{ veh: "'16 Wrangler", plate: "JP-3340", code: "Parasitic draw", status: "inprogress", label: "In Progress", hrs: 1.8 }
]
},
{
id: "oadeyemi", name: "Obi Adeyemi", bay: "Bay 5 · B-Tech",
role: "ASE B-Tech · Tires & Brakes", color: "#5b6470",
hours: { day: 5.4, week: 30.2, month: 124.6 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 8, week: 34, month: 138 },
certs: ["ASE A5", "TPMS", "Road Force"],
orders: [
{ veh: "'19 Outback", plate: "SUB-2210", code: "4 tires + bal.", status: "inprogress", label: "In Progress", hrs: 1.4 },
{ veh: "'21 Sienna", plate: "VAN-6655", code: "Brake pads", status: "done", label: "Done", hrs: 1.0 }
]
},
{
id: "jcastle", name: "Jess Castellano", bay: "Bay 6 · Lube Tech",
role: "Express Lube · Maintenance", color: "#8a929d",
hours: { day: 4.6, week: 24.9, month: 101.2 },
avail: { day: 8, week: 40, month: 172 },
jobs: { day: 9, week: 41, month: 162 },
certs: ["Maint. Light", "Fluid Exch."],
orders: [
{ veh: "'20 Corolla", plate: "ECO-1102", code: "LOF service", status: "done", label: "Done", hrs: 0.5 },
{ veh: "'18 Escape", plate: "FRD-8841", code: "Coolant flush", status: "waiting", label: "Waiting", hrs: 0.7 }
]
}
];
var state = { range: "week", sortKey: "eff", sortDir: -1 };
var RANGE_LABEL = { day: "today", week: "this week", month: "this month" };
// --- Helpers ---
function $(s, c) { return (c || document).querySelector(s); }
function eff(t) { return t.avail[state.range] ? (t.hours[state.range] / t.avail[state.range]) * 100 : 0; }
function rev(t) { return t.hours[state.range] * ALR; }
function money(n) { return "$" + Math.round(n).toLocaleString("en-US"); }
function effColor(e) {
if (e >= 95) return "#2f9e6f";
if (e >= 82) return "#ff6a13";
if (e >= 70) return "#e0962a";
return "#d4493e";
}
function initials(name) {
return name.split(" ").map(function (w) { return w[0]; }).slice(0, 2).join("").toUpperCase();
}
var toastTimer;
function toast(msg) {
var el = $("#toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove("show"); }, 2200);
}
// --- Sorting ---
function sortedTechs() {
var arr = TECHS.slice();
var k = state.sortKey, dir = state.sortDir;
arr.sort(function (a, b) {
var av, bv;
if (k === "name") { av = a.name.toLowerCase(); bv = b.name.toLowerCase(); }
else if (k === "eff") { av = eff(a); bv = eff(b); }
else if (k === "revenue") { av = rev(a); bv = rev(b); }
else if (k === "billed") { av = a.hours[state.range]; bv = b.hours[state.range]; }
else if (k === "available") { av = a.avail[state.range]; bv = b.avail[state.range]; }
else if (k === "jobs") { av = a.jobs[state.range]; bv = b.jobs[state.range]; }
if (av < bv) return -1 * dir;
if (av > bv) return 1 * dir;
return 0;
});
return arr;
}
// --- Render: KPI strip ---
function renderKpis() {
var billed = 0, avail = 0, jobs = 0, revenue = 0, hold = 0;
TECHS.forEach(function (t) {
billed += t.hours[state.range];
avail += t.avail[state.range];
jobs += t.jobs[state.range];
revenue += rev(t);
t.orders.forEach(function (o) { if (o.status === "hold") hold++; });
});
var shopEff = avail ? (billed / avail) * 100 : 0;
$("#kpiBilled").textContent = billed.toFixed(1) + "h";
$("#kpiEff").textContent = shopEff.toFixed(0) + "%";
$("#kpiJobs").textContent = jobs;
$("#kpiRev").textContent = money(revenue);
$("#kpiHold").textContent = hold;
$("#kpiBilledDelta").textContent = shopEff >= 90 ? "+4.1%" : "+1.6%";
$("#rangeLabel").textContent = RANGE_LABEL[state.range];
}
// --- Render: table ---
function renderTable() {
var body = $("#techBody");
body.innerHTML = "";
sortedTechs().forEach(function (t) {
var e = eff(t);
var tr = document.createElement("tr");
tr.tabIndex = 0;
tr.setAttribute("role", "button");
tr.setAttribute("aria-label", "Open detail for " + t.name);
tr.innerHTML =
'<td class="td-name">' +
'<div class="tech-cell">' +
'<span class="tech-av" style="background:' + t.color + '">' + initials(t.name) + '</span>' +
'<span class="tech-meta"><span class="tech-name">' + t.name + '</span>' +
'<span class="tech-bay">' + t.bay + '</span></span>' +
'</div>' +
'</td>' +
'<td class="num">' + t.hours[state.range].toFixed(1) + 'h</td>' +
'<td class="num col-avail">' + t.avail[state.range].toFixed(0) + 'h</td>' +
'<td>' +
'<div class="eff-cell">' +
'<span class="eff-bar"><span class="eff-fill" style="width:' + Math.min(e, 100) + '%;background:' + effColor(e) + '"></span></span>' +
'<span class="eff-num" style="color:' + effColor(e) + '">' + e.toFixed(0) + '%</span>' +
'</div>' +
'</td>' +
'<td class="num">' + t.jobs[state.range] + '</td>' +
'<td class="num col-rev"><span class="rev-num">' + money(rev(t)) + '</span></td>';
tr.addEventListener("click", function () { openDrawer(t.id); });
tr.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); openDrawer(t.id); }
});
body.appendChild(tr);
});
// header indicators
var ths = document.querySelectorAll("#techTable thead th");
ths.forEach(function (th) {
th.classList.remove("is-sorted", "asc", "desc");
th.removeAttribute("aria-sort");
if (th.dataset.sort === state.sortKey) {
th.classList.add("is-sorted", state.sortDir === 1 ? "asc" : "desc");
th.setAttribute("aria-sort", state.sortDir === 1 ? "ascending" : "descending");
}
});
}
// --- Render: leaderboard ---
function renderBoard() {
var ol = $("#board");
ol.innerHTML = "";
var ranked = TECHS.slice().sort(function (a, b) { return eff(b) - eff(a); });
var max = eff(ranked[0]) || 1;
ranked.forEach(function (t, i) {
var e = eff(t);
var li = document.createElement("li");
li.className = "bd-item";
li.tabIndex = 0;
li.innerHTML =
'<span class="bd-rank">' + (i + 1) + '</span>' +
'<span class="bd-body">' +
'<span class="bd-name">' + t.name + '</span>' +
'<span class="bd-track"><span class="bd-prog" style="width:' + (e / max * 100) + '%"></span></span>' +
'</span>' +
'<span class="bd-eff">' + e.toFixed(0) + '%</span>';
li.addEventListener("click", function () { openDrawer(t.id); });
li.addEventListener("keydown", function (ev) {
if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); openDrawer(t.id); }
});
ol.appendChild(li);
});
}
// --- Render: chart ---
function renderChart() {
var wrap = $("#chart");
wrap.innerHTML = "";
var maxAvail = 0;
TECHS.forEach(function (t) {
maxAvail = Math.max(maxAvail, t.avail[state.range], t.hours[state.range]);
});
TECHS.forEach(function (t) {
var col = document.createElement("div");
col.className = "ch-col";
var bH = (t.hours[state.range] / maxAvail) * 100;
var aH = (t.avail[state.range] / maxAvail) * 100;
col.innerHTML =
'<div class="ch-bars">' +
'<div class="ch-bar billed" style="height:0%" data-h="' + bH + '" title="' + t.name + ' billed ' + t.hours[state.range].toFixed(1) + 'h">' +
'<span class="ch-val">' + t.hours[state.range].toFixed(0) + 'h</span>' +
'</div>' +
'<div class="ch-bar avail" style="height:0%" data-h="' + aH + '"></div>' +
'</div>' +
'<span class="ch-lab">' + t.name.split(" ")[0] + '</span>';
col.querySelector(".ch-bar.billed").addEventListener("click", function () { openDrawer(t.id); });
wrap.appendChild(col);
});
// animate next frame
requestAnimationFrame(function () {
wrap.querySelectorAll(".ch-bar").forEach(function (b) {
b.style.height = b.dataset.h + "%";
});
});
}
// --- Drawer ---
function openDrawer(id) {
var t = TECHS.find(function (x) { return x.id === id; });
if (!t) return;
var e = eff(t);
$("#dAvatar").textContent = initials(t.name);
$("#dAvatar").style.background = t.color;
$("#dName").textContent = t.name;
$("#dRole").textContent = t.role;
$("#dEff").textContent = e.toFixed(0) + "%";
$("#dBilled").textContent = t.hours[state.range].toFixed(1) + "h";
$("#dAvail").textContent = t.avail[state.range].toFixed(0) + "h";
$("#dJobs").textContent = t.jobs[state.range];
$("#dRev").textContent = money(rev(t));
var ring = $("#dRing");
var c = effColor(e);
ring.style.background = "conic-gradient(" + c + " " + (Math.min(e, 100) * 3.6) + "deg, var(--line) 0)";
$("#dEff").style.color = c;
var wo = $("#dWorkOrders");
wo.innerHTML = "";
t.orders.forEach(function (o) {
var li = document.createElement("li");
li.className = "wo";
li.innerHTML =
'<span class="wo-status st-' + o.status + '"></span>' +
'<span class="wo-main">' +
'<span class="wo-veh">' + o.veh + '</span>' +
'<span class="wo-sub">' + o.plate + ' · ' + o.code + ' · ' + o.hrs.toFixed(1) + 'h</span>' +
'</span>' +
'<span class="wo-badge st-' + o.status + '">' + o.label + '</span>';
wo.appendChild(li);
});
var chips = $("#dCerts");
chips.innerHTML = "";
t.certs.forEach(function (cn) {
var s = document.createElement("span");
s.className = "chip";
s.textContent = cn;
chips.appendChild(s);
});
$("#scrim").hidden = false;
var drawer = $("#drawer");
drawer.classList.add("is-open");
drawer.setAttribute("aria-hidden", "false");
$("#drawerClose").focus();
}
function closeDrawer() {
var drawer = $("#drawer");
drawer.classList.remove("is-open");
drawer.setAttribute("aria-hidden", "true");
setTimeout(function () { $("#scrim").hidden = true; }, 280);
}
// --- Render all ---
function renderAll() {
renderKpis();
renderTable();
renderBoard();
renderChart();
}
// --- Events ---
document.querySelectorAll(".seg-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
if (btn.classList.contains("is-active")) return;
document.querySelectorAll(".seg-btn").forEach(function (b) {
b.classList.remove("is-active");
b.removeAttribute("aria-pressed");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
state.range = btn.dataset.range;
renderAll();
toast("Showing " + RANGE_LABEL[state.range]);
});
});
document.querySelectorAll("#techTable thead th").forEach(function (th) {
th.addEventListener("click", function () {
var k = th.dataset.sort;
if (state.sortKey === k) {
state.sortDir *= -1;
} else {
state.sortKey = k;
state.sortDir = k === "name" ? 1 : -1;
}
renderTable();
});
});
$("#drawerClose").addEventListener("click", closeDrawer);
$("#scrim").addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") closeDrawer();
});
$("#exportBtn").addEventListener("click", function () {
toast("Productivity report (" + RANGE_LABEL[state.range] + ") queued for export");
});
// init
renderAll();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Torque & Tread — Technician Productivity</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">
<!-- Top bar -->
<header class="topbar">
<div class="brand">
<div class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a4 4 0 0 1-5.4 5.4L4 17v3h3l5.3-5.3a4 4 0 0 1 5.4-5.4l-2.6 2.6-1.4-.3-.3-1.4 2.6-2.6Z"/>
</svg>
</div>
<div class="brand-txt">
<span class="brand-name">Torque & Tread</span>
<span class="brand-sub">Service Bay Ops</span>
</div>
</div>
<nav class="topnav" aria-label="Sections">
<a href="#" class="topnav-link">Dashboard</a>
<a href="#" class="topnav-link">Work Orders</a>
<a href="#" class="topnav-link is-active" aria-current="page">Technicians</a>
<a href="#" class="topnav-link">Inventory</a>
</nav>
<div class="topbar-right">
<div class="seg" role="group" aria-label="Timeframe">
<button class="seg-btn" data-range="day">Day</button>
<button class="seg-btn is-active" data-range="week" aria-pressed="true">Week</button>
<button class="seg-btn" data-range="month">Month</button>
</div>
<div class="avatar" title="Shop Foreman — D. Okafor">DO</div>
</div>
</header>
<main class="main">
<div class="page-head">
<div>
<h1 class="page-title">Technician Productivity</h1>
<p class="page-meta">Bay performance for <span id="rangeLabel">this week</span> · 6 techs on shift · updated 9:42 AM</p>
</div>
<button class="btn btn-ghost" id="exportBtn" type="button">
<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="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"/></svg>
Export
</button>
</div>
<!-- KPI strip -->
<section class="kpis" aria-label="Shop totals">
<div class="kpi">
<span class="kpi-label">Billed Hours</span>
<span class="kpi-value" id="kpiBilled">0</span>
<span class="kpi-foot"><i class="dot dot-ok"></i><span id="kpiBilledDelta">+0%</span> vs prior</span>
</div>
<div class="kpi">
<span class="kpi-label">Shop Efficiency</span>
<span class="kpi-value" id="kpiEff">0%</span>
<span class="kpi-foot"><i class="dot dot-ok"></i>Target 90%</span>
</div>
<div class="kpi">
<span class="kpi-label">Jobs Closed</span>
<span class="kpi-value" id="kpiJobs">0</span>
<span class="kpi-foot"><i class="dot dot-warn"></i><span id="kpiHold">0</span> on hold</span>
</div>
<div class="kpi kpi-rev">
<span class="kpi-label">Labor Revenue</span>
<span class="kpi-value" id="kpiRev">$0</span>
<span class="kpi-foot"><i class="dot dot-ok"></i>@ $145/hr ALR</span>
</div>
</section>
<div class="grid">
<!-- Main table -->
<section class="panel panel-table">
<div class="panel-head">
<h2 class="panel-title">Technician Scorecard</h2>
<div class="table-tools">
<span class="hint">Click a row to drill in</span>
</div>
</div>
<div class="table-wrap" role="region" aria-label="Technician scorecard" tabindex="0">
<table class="ttable" id="techTable">
<thead>
<tr>
<th class="th-name" data-sort="name">Technician <span class="caret"></span></th>
<th data-sort="billed">Billed <span class="caret"></span></th>
<th data-sort="available">Avail <span class="caret"></span></th>
<th data-sort="eff" class="is-sorted" aria-sort="descending">Eff % <span class="caret"></span></th>
<th data-sort="jobs">Jobs <span class="caret"></span></th>
<th data-sort="revenue">Revenue <span class="caret"></span></th>
</tr>
</thead>
<tbody id="techBody"><!-- JS --></tbody>
</table>
</div>
</section>
<!-- Right column -->
<div class="col-right">
<!-- Leaderboard -->
<section class="panel panel-board">
<div class="panel-head">
<h2 class="panel-title">Efficiency Leaderboard</h2>
</div>
<ol class="board" id="board"><!-- JS --></ol>
</section>
<!-- Hours chart -->
<section class="panel panel-chart">
<div class="panel-head">
<h2 class="panel-title">Billed vs Available Hours</h2>
<div class="legend">
<span class="lg"><i class="sw sw-billed"></i>Billed</span>
<span class="lg"><i class="sw sw-avail"></i>Available</span>
</div>
</div>
<div class="chart" id="chart" role="img" aria-label="Bar chart of billed versus available hours per technician"><!-- JS --></div>
</section>
</div>
</div>
</main>
</div>
<!-- Drill-in drawer -->
<div class="drawer-scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" aria-label="Technician detail" aria-hidden="true">
<div class="drawer-head">
<div class="dh-id">
<div class="dh-avatar" id="dAvatar">--</div>
<div>
<h3 class="dh-name" id="dName">—</h3>
<p class="dh-role" id="dRole">—</p>
</div>
</div>
<button class="icon-btn" id="drawerClose" type="button" aria-label="Close detail">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m6 6 12 12M18 6 6 18"/></svg>
</button>
</div>
<div class="drawer-body">
<div class="dring-wrap">
<div class="dring" id="dRing">
<span class="dring-num" id="dEff">0%</span>
<span class="dring-cap">efficiency</span>
</div>
<ul class="dstats">
<li><span class="ds-l">Billed</span><span class="ds-v" id="dBilled">0h</span></li>
<li><span class="ds-l">Available</span><span class="ds-v" id="dAvail">0h</span></li>
<li><span class="ds-l">Jobs</span><span class="ds-v" id="dJobs">0</span></li>
<li><span class="ds-l">Revenue</span><span class="ds-v" id="dRev">$0</span></li>
</ul>
</div>
<h4 class="dsub">Open work orders</h4>
<ul class="wo-list" id="dWorkOrders"><!-- JS --></ul>
<h4 class="dsub">Certifications</h4>
<div class="chips" id="dCerts"><!-- JS --></div>
</div>
</aside>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Technician Productivity
An industrial, status-forward productivity dashboard for the service bays at a fictional shop, Torque & Tread. A KPI strip rolls up shop-wide billed hours, efficiency, jobs closed and labor revenue, then a technician scorecard lists each tech with billed and available hours, an inline efficiency bar, jobs and revenue. The right column pairs an efficiency leaderboard with a billed-versus-available hours bar chart so the foreman can spot capacity at a glance.
Every column header is a sort toggle that flips ascending and descending, with caret indicators and aria-sort for screen readers. The Day / Week / Month segmented control recomputes efficiency, revenue (at a $145/hr labor rate) and the chart in place, and a toast confirms the active timeframe. Tabular figures keep hours, percentages and money aligned across rows.
Clicking any scorecard row, leaderboard entry or billed bar opens a drill-in drawer. It renders a conic-gradient efficiency ring colored by performance band, the tech’s billed/available/jobs/revenue stats, a list of open work orders with status pills (In Progress, Waiting, Done, On Hold), plate, diagnostic code and hours, plus certification chips. The drawer is keyboard dismissible with Escape and the whole UI degrades cleanly to a single column down to ~360px.
Illustrative UI only — fictional shop/dealership, not a real service system.