Auto — Technician Mobile Job View
A mobile-first technician job view for an auto repair shop, built with vanilla HTML, CSS, and JavaScript. It shows an assigned work-order card with vehicle, VIN, plate, odometer and a P0301 diagnostic code, plus a tappable task checklist with live progress, a clock-on/off labor timer, photo and note capture, a parts request flow that updates the running estimate, and an animated mark-complete confirmation. Fully responsive down to small phone screens.
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;
--shadow: 0 1px 2px rgba(20, 21, 24, 0.06), 0 6px 18px rgba(20, 21, 24, 0.07);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
}
body {
font-family: "Inter", system-ui, -apple-system, sans-serif;
background:
radial-gradient(1100px 520px at 50% -8%, #21242b 0%, transparent 60%),
var(--garage);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
display: flex;
justify-content: center;
padding: 24px 16px;
}
.tab { font-variant-numeric: tabular-nums; font-feature-settings: "tnum"; }
/* ---- Phone shell ---- */
.phone {
width: 100%;
max-width: 412px;
background: var(--bg);
border-radius: 28px;
overflow: hidden;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.04);
display: flex;
flex-direction: column;
height: min(860px, calc(100vh - 48px));
position: relative;
}
/* ---- App bar ---- */
.appbar {
flex: 0 0 auto;
background: var(--garage);
color: #fff;
display: flex;
align-items: center;
gap: 12px;
padding: 16px 16px 14px;
}
.icon-btn {
background: rgba(255, 255, 255, 0.08);
border: none;
color: #fff;
width: 36px;
height: 36px;
border-radius: 10px;
display: grid;
place-items: center;
cursor: pointer;
transition: background 0.15s;
}
.icon-btn:hover { background: rgba(255, 255, 255, 0.16); }
.icon-btn:active { transform: scale(0.94); }
.appbar-title { display: flex; flex-direction: column; line-height: 1.2; flex: 1; }
.appbar-shop { font-weight: 700; font-size: 0.95rem; letter-spacing: -0.01em; }
.appbar-wo { font-size: 0.74rem; color: var(--steel-l); font-weight: 600; }
.avatar {
width: 36px; height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--orange), var(--orange-d));
color: #fff;
display: grid; place-items: center;
font-size: 0.78rem; font-weight: 700;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.18);
}
/* ---- Scroll area ---- */
.scroll {
flex: 1 1 auto;
overflow-y: auto;
padding: 14px 14px 0;
display: flex;
flex-direction: column;
gap: 12px;
-webkit-overflow-scrolling: touch;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow);
padding: 14px;
}
.sec-h { font-size: 0.92rem; font-weight: 700; margin: 0; letter-spacing: -0.01em; }
.sec-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
/* ---- Job card ---- */
.job { padding: 0; overflow: hidden; }
.job-photo {
height: 132px;
background:
linear-gradient(135deg, #2a2d34 0%, #14151a 100%),
var(--garage-2);
position: relative;
display: grid;
place-items: center;
}
.job-photo::after {
content: "";
position: absolute; inset: 0;
background:
repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.04) 0 2px, transparent 2px 22px),
radial-gradient(220px 120px at 70% 120%, rgba(255, 106, 19, 0.28), transparent 70%);
}
.vehicle-glyph { font-size: 2.6rem; filter: grayscale(0.1); position: relative; z-index: 1; }
.badge.bay {
position: absolute; top: 12px; left: 12px;
background: var(--orange);
color: #fff;
font-size: 0.72rem; font-weight: 700;
padding: 4px 9px; border-radius: 999px;
z-index: 2; letter-spacing: 0.02em;
box-shadow: 0 4px 12px rgba(255, 106, 19, 0.4);
}
.job-body { padding: 14px; }
.job-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.vehicle { font-size: 1.12rem; font-weight: 800; margin: 0; letter-spacing: -0.02em; }
.cust { margin: 2px 0 0; color: var(--muted); font-size: 0.84rem; font-weight: 500; }
.status-pill {
flex: 0 0 auto;
font-size: 0.72rem; font-weight: 700;
padding: 5px 10px; border-radius: 999px;
white-space: nowrap;
}
.status-pill[data-status="inprogress"] { background: rgba(43, 127, 255, 0.12); color: var(--inprogress); }
.status-pill[data-status="done"] { background: rgba(47, 158, 111, 0.14); color: var(--done); }
.status-pill[data-status="waiting"] { background: rgba(224, 150, 42, 0.14); color: var(--waiting); }
.specs {
margin: 14px 0 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 14px;
}
.specs div { display: flex; flex-direction: column; }
.specs dt { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--steel); font-weight: 700; }
.specs dd { margin: 2px 0 0; font-size: 0.86rem; font-weight: 600; color: var(--ink-2); }
.dtc {
margin-top: 14px;
display: flex; align-items: center; gap: 8px;
background: rgba(212, 73, 62, 0.07);
border: 1px solid rgba(212, 73, 62, 0.2);
border-radius: var(--r-sm);
padding: 8px 10px;
font-size: 0.82rem;
}
.dtc-tag {
background: var(--danger); color: #fff;
font-size: 0.64rem; font-weight: 800;
padding: 2px 6px; border-radius: 5px; letter-spacing: 0.04em;
}
.dtc .tab { font-weight: 800; color: var(--danger); }
.dtc-desc { color: var(--ink-2); font-weight: 500; }
/* ---- Timer ---- */
.timer-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.timer-sub { margin: 2px 0 0; font-size: 0.8rem; color: var(--muted); font-weight: 500; }
.timer-sub.live { color: var(--ok); font-weight: 600; }
.timer-display {
font-size: 1.5rem; font-weight: 800;
letter-spacing: -0.02em;
color: var(--garage);
}
.timer-display.live { color: var(--orange-d); }
.btn {
width: 100%;
border: none;
border-radius: var(--r-md);
font-family: inherit;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
padding: 13px;
display: flex; align-items: center; justify-content: center; gap: 9px;
transition: transform 0.12s, background 0.18s, box-shadow 0.18s;
}
.btn:active { transform: scale(0.985); }
.btn.clock {
background: var(--garage);
color: #fff;
}
.btn.clock:hover { background: #23262e; }
.btn.clock .dot {
width: 9px; height: 9px; border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5);
}
.btn.clock.on { background: var(--orange); }
.btn.clock.on:hover { background: var(--orange-d); }
.btn.clock.on .dot {
background: #fff;
animation: pulse 1.4s ease-out infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.55); }
100% { box-shadow: 0 0 0 9px rgba(255, 255, 255, 0); }
}
/* ---- Tasks ---- */
.count {
font-size: 0.78rem; font-weight: 700; color: var(--steel);
background: var(--bg); padding: 3px 9px; border-radius: 999px;
}
.progress {
height: 6px; border-radius: 999px;
background: var(--bg);
overflow: hidden; margin-bottom: 12px;
}
.progress span {
display: block; height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--orange), var(--orange-d));
border-radius: 999px;
transition: width 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.tasks { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.task label {
display: flex; align-items: center; gap: 12px;
padding: 9px 4px;
cursor: pointer;
border-radius: var(--r-sm);
transition: background 0.12s;
}
.task label:hover { background: var(--bg); }
.task input { position: absolute; opacity: 0; pointer-events: none; }
.check {
flex: 0 0 auto;
width: 24px; height: 24px;
border-radius: 7px;
border: 2px solid var(--line-2);
display: grid; place-items: center;
transition: background 0.18s, border-color 0.18s, transform 0.18s;
}
.check::after {
content: "";
width: 11px; height: 6px;
border-left: 2.5px solid #fff; border-bottom: 2.5px solid #fff;
transform: rotate(-45deg) scale(0);
margin-top: -2px;
transition: transform 0.18s cubic-bezier(0.2, 1.4, 0.4, 1);
}
.task input:focus-visible + .check { outline: 2px solid var(--orange); outline-offset: 2px; }
.task input:checked + .check {
background: var(--ok); border-color: var(--ok);
transform: scale(1.04);
}
.task input:checked + .check::after { transform: rotate(-45deg) scale(1); }
.task-txt { font-size: 0.88rem; font-weight: 500; transition: color 0.15s; }
.task input:checked ~ .task-txt { color: var(--muted); text-decoration: line-through; }
/* ---- Parts ---- */
.parts { list-style: none; margin: 0; padding: 0; }
.part {
display: flex; justify-content: space-between; align-items: center; gap: 10px;
padding: 9px 0;
border-bottom: 1px solid var(--line);
}
.part:last-child { border-bottom: none; }
.part-name { font-size: 0.86rem; font-weight: 600; }
.part-name em { color: var(--muted); font-style: normal; font-weight: 500; font-size: 0.8rem; }
.part-meta { display: flex; align-items: center; gap: 12px; }
.qty { font-size: 0.78rem; color: var(--steel); font-weight: 700; }
.money { font-weight: 700; font-size: 0.86rem; }
.part.labor { background: var(--orange-50); margin: 4px -6px 0; padding: 9px 6px; border-radius: var(--r-sm); border-bottom: none; }
.part.labor .part-name { color: var(--orange-d); }
.part.new { animation: slidein 0.35s ease; }
@keyframes slidein { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: none; } }
.total-row {
display: flex; justify-content: space-between; align-items: center;
margin-top: 12px; padding-top: 12px;
border-top: 2px solid var(--line-2);
font-weight: 800; font-size: 0.95rem;
}
.total-row .money { font-size: 1.05rem; color: var(--garage); }
.btn-ghost {
background: var(--orange-50);
color: var(--orange-d);
border: 1px solid rgba(255, 106, 19, 0.25);
border-radius: var(--r-sm);
font-family: inherit;
font-size: 0.78rem; font-weight: 700;
padding: 6px 11px;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
}
.btn-ghost:hover { background: #ffe3d2; }
.btn-ghost:active { transform: scale(0.96); }
.btn-ghost.wide { width: 100%; padding: 11px; font-size: 0.9rem; margin-top: 6px; }
/* ---- Capture ---- */
.capture-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.cap-add {
aspect-ratio: 1;
background: var(--bg);
border: 2px dashed var(--line-2);
border-radius: var(--r-sm);
color: var(--steel);
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
font-size: 0.66rem; font-weight: 700;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background 0.15s;
}
.cap-add:hover { border-color: var(--orange); color: var(--orange-d); background: var(--orange-50); }
.cap-thumb {
aspect-ratio: 1;
border-radius: var(--r-sm);
position: relative;
overflow: hidden;
animation: slidein 0.3s ease;
box-shadow: inset 0 0 0 1px var(--line);
}
.cap-thumb span {
position: absolute; bottom: 4px; left: 4px;
font-size: 0.6rem; font-weight: 700; color: #fff;
background: rgba(0, 0, 0, 0.5); padding: 1px 5px; border-radius: 5px;
}
.note-wrap { display: flex; gap: 8px; align-items: flex-end; }
.note-wrap textarea {
flex: 1;
font-family: inherit; font-size: 0.85rem;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 10px;
resize: none;
color: var(--ink);
transition: border-color 0.15s, box-shadow 0.15s;
}
.note-wrap textarea:focus { outline: none; border-color: var(--orange); box-shadow: 0 0 0 3px rgba(255, 106, 19, 0.15); }
.notes { list-style: none; margin: 12px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.note {
background: var(--bg);
border-left: 3px solid var(--orange);
border-radius: var(--r-sm);
padding: 8px 10px;
font-size: 0.82rem;
animation: slidein 0.3s ease;
}
.note time { display: block; font-size: 0.68rem; color: var(--muted); font-weight: 600; margin-top: 3px; }
.footer-space { height: 6px; flex: 0 0 auto; }
/* ---- Action bar ---- */
.action-bar {
flex: 0 0 auto;
padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
background: linear-gradient(0deg, var(--surface) 70%, transparent);
border-top: 1px solid var(--line);
}
.btn.complete {
background: var(--ok);
color: #fff;
box-shadow: 0 6px 18px rgba(47, 158, 111, 0.32);
}
.btn.complete:hover { background: #29885f; }
.btn.complete:disabled { background: var(--steel); box-shadow: none; cursor: not-allowed; opacity: 0.7; }
.btn.complete.ready { animation: nudge 0.5s ease; }
@keyframes nudge { 0%,100% { transform: none; } 30% { transform: scale(1.02); } }
/* ---- Overlay ---- */
.overlay {
position: absolute; inset: 0;
background: rgba(20, 21, 24, 0.6);
backdrop-filter: blur(3px);
display: grid; place-items: center;
padding: 24px;
z-index: 40;
animation: fade 0.2s ease;
}
.overlay[hidden] { display: none; }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }
.done-card {
background: var(--surface);
border-radius: var(--r-lg);
padding: 26px 22px 20px;
text-align: center;
width: 100%;
max-width: 300px;
animation: pop 0.3s cubic-bezier(0.2, 1.2, 0.3, 1);
}
@keyframes pop { from { opacity: 0; transform: scale(0.9) translateY(8px); } to { opacity: 1; transform: none; } }
.done-card h3 { margin: 14px 0 0; font-size: 1.25rem; font-weight: 800; letter-spacing: -0.02em; }
.done-sub { margin: 4px 0 0; color: var(--muted); font-size: 0.86rem; }
.done-time { margin: 10px 0 0; font-weight: 700; font-size: 0.9rem; color: var(--orange-d); }
.done-ring { width: 80px; height: 80px; margin: 0 auto; }
.done-ring svg { width: 80px; height: 80px; }
.ring-bg { fill: none; stroke: rgba(47, 158, 111, 0.15); stroke-width: 4; }
.ring-fg {
fill: none; stroke: var(--ok); stroke-width: 4; stroke-linecap: round;
stroke-dasharray: 138; stroke-dashoffset: 138;
transform: rotate(-90deg); transform-origin: center;
animation: draw 0.6s 0.1s ease forwards;
}
.ring-check {
stroke: var(--ok); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round;
stroke-dasharray: 40; stroke-dashoffset: 40;
animation: draw 0.35s 0.6s ease forwards;
}
@keyframes draw { to { stroke-dashoffset: 0; } }
/* ---- Toast ---- */
.toast {
position: absolute;
left: 50%; bottom: 88px;
transform: translate(-50%, 16px);
background: var(--garage);
color: #fff;
font-size: 0.82rem; font-weight: 600;
padding: 10px 16px;
border-radius: 999px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
max-width: 80%;
text-align: center;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---- Responsive ---- */
@media (max-width: 520px) {
body { padding: 0; }
.phone {
max-width: 100%;
height: 100vh;
height: 100dvh;
border-radius: 0;
box-shadow: none;
}
}(function () {
"use strict";
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
/* ---------- Task checklist ---------- */
var taskList = document.getElementById("taskList");
var checkboxes = Array.prototype.slice.call(taskList.querySelectorAll('input[type="checkbox"]'));
var taskCount = document.getElementById("taskCount");
var progBar = document.getElementById("progBar");
var completeBtn = document.getElementById("completeBtn");
function refreshTasks() {
var done = checkboxes.filter(function (c) { return c.checked; }).length;
var total = checkboxes.length;
taskCount.textContent = done + " / " + total;
progBar.style.width = (done / total) * 100 + "%";
var allDone = done === total;
completeBtn.disabled = !allDone;
if (allDone) {
completeBtn.classList.add("ready");
setTimeout(function () { completeBtn.classList.remove("ready"); }, 500);
}
}
checkboxes.forEach(function (cb) {
cb.addEventListener("change", function () {
refreshTasks();
if (cb.checked) {
var txt = cb.closest(".task").querySelector(".task-txt").textContent;
toast("✓ " + txt);
}
});
});
refreshTasks();
/* ---------- Clock / labor timer ---------- */
var clockBtn = document.getElementById("clockBtn");
var clockLabel = document.getElementById("clockLabel");
var clockState = document.getElementById("clockState");
var timerDisplay = document.getElementById("timerDisplay");
var elapsed = 0; // seconds
var running = false;
var tickId = null;
function fmt(s) {
var h = Math.floor(s / 3600);
var m = Math.floor((s % 3600) / 60);
var sec = s % 60;
function pad(n) { return n < 10 ? "0" + n : "" + n; }
return pad(h) + ":" + pad(m) + ":" + pad(sec);
}
function render() {
timerDisplay.textContent = fmt(elapsed);
}
function tick() {
elapsed += 1;
render();
}
clockBtn.addEventListener("click", function () {
running = !running;
if (running) {
tickId = setInterval(tick, 1000);
clockBtn.classList.add("on");
clockBtn.setAttribute("aria-pressed", "true");
clockLabel.textContent = "Clock Off";
clockState.textContent = "Clocked on · live";
clockState.classList.add("live");
timerDisplay.classList.add("live");
toast("Clocked on — timer running");
} else {
clearInterval(tickId);
clockBtn.classList.remove("on");
clockBtn.setAttribute("aria-pressed", "false");
clockLabel.textContent = "Clock On";
clockState.textContent = "Paused · " + fmt(elapsed);
clockState.classList.remove("live");
timerDisplay.classList.remove("live");
toast("Clocked off — " + fmt(elapsed) + " logged");
}
});
render();
/* ---------- Parts request ---------- */
var partsList = document.getElementById("partsList");
var partsTotal = document.getElementById("partsTotal");
var reqPartBtn = document.getElementById("reqPartBtn");
var laborRow = partsList.querySelector(".part.labor");
var catalog = [
{ name: "Brake Fluid <em>(DOT 4, 1L)</em>", price: 14.5 },
{ name: "Cabin Air Filter <em>(OEM)</em>", price: 22.0 },
{ name: "Wheel Bearing <em>(front)</em>", price: 89.0 },
{ name: "Serpentine Belt <em>(Gates)</em>", price: 31.75 },
{ name: "Spark Plug Set <em>(×4)</em>", price: 36.0 }
];
var catIdx = 0;
var total = 556.4;
reqPartBtn.addEventListener("click", function () {
var p = catalog[catIdx % catalog.length];
catIdx += 1;
total += p.price;
var li = document.createElement("li");
li.className = "part new";
li.innerHTML =
'<span class="part-name">' + p.name + "</span>" +
'<span class="part-meta"><span class="qty">×1</span>' +
'<span class="tab money">$' + p.price.toFixed(2) + "</span></span>";
partsList.insertBefore(li, laborRow);
partsTotal.textContent = "$" + total.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
var plain = p.name.replace(/<[^>]+>/g, "").trim();
toast("Part requested: " + plain);
});
/* ---------- Photo capture ---------- */
var captureGrid = document.getElementById("captureGrid");
var addPhoto = document.getElementById("addPhoto");
var gradients = [
"linear-gradient(135deg,#3a3f47,#1a1c21)",
"linear-gradient(135deg,#5b6470,#2a2d34)",
"linear-gradient(135deg,#ff6a13,#e2540a)",
"linear-gradient(135deg,#2b7fff,#1d5fd6)",
"linear-gradient(135deg,#2f9e6f,#1f6f4c)"
];
var labels = ["Pads", "Rotor", "Coil", "VIN", "Odo", "Tire"];
var photoN = 0;
addPhoto.addEventListener("click", function () {
photoN += 1;
var thumb = document.createElement("div");
thumb.className = "cap-thumb";
thumb.style.background = gradients[(photoN - 1) % gradients.length];
thumb.innerHTML = "<span>" + labels[(photoN - 1) % labels.length] + "</span>";
captureGrid.insertBefore(thumb, addPhoto);
toast("Photo captured (" + photoN + ")");
});
/* ---------- Notes ---------- */
var noteInput = document.getElementById("noteInput");
var addNote = document.getElementById("addNote");
var notesList = document.getElementById("notesList");
function nowLabel() {
var d = new Date();
var h = d.getHours();
var m = d.getMinutes();
var ap = h >= 12 ? "PM" : "AM";
h = h % 12; if (h === 0) h = 12;
return h + ":" + (m < 10 ? "0" + m : m) + " " + ap;
}
function saveNote() {
var val = noteInput.value.trim();
if (!val) { noteInput.focus(); return; }
var li = document.createElement("li");
li.className = "note";
var span = document.createElement("span");
span.textContent = val;
var time = document.createElement("time");
time.textContent = "Tech DM · " + nowLabel();
li.appendChild(span);
li.appendChild(time);
notesList.insertBefore(li, notesList.firstChild);
noteInput.value = "";
toast("Note saved");
}
addNote.addEventListener("click", saveNote);
noteInput.addEventListener("keydown", function (e) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") saveNote();
});
/* ---------- Complete job ---------- */
var overlay = document.getElementById("overlay");
var overlayClose = document.getElementById("overlayClose");
var doneTime = document.getElementById("doneTime");
var jobStatus = document.getElementById("jobStatus");
var completeLabel = document.getElementById("completeLabel");
completeBtn.addEventListener("click", function () {
if (completeBtn.disabled) return;
if (running) clockBtn.click(); // clock off automatically
doneTime.textContent = "Logged " + fmt(elapsed);
jobStatus.textContent = "Done";
jobStatus.setAttribute("data-status", "done");
completeLabel.textContent = "Completed";
overlay.hidden = false;
});
overlayClose.addEventListener("click", function () {
overlay.hidden = true;
toast("Returning to job list…");
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Technician Mobile Job View</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="phone" role="application" aria-label="Technician mobile job view">
<!-- App bar -->
<header class="appbar">
<button class="icon-btn" aria-label="Back to job list">
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true"><path d="M15 18l-6-6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<div class="appbar-title">
<span class="appbar-shop">Cedar Ridge Auto</span>
<span class="appbar-wo">WO #4821</span>
</div>
<div class="appbar-tech" aria-label="Logged in technician">
<span class="avatar">DM</span>
</div>
</header>
<main class="scroll" id="scroll">
<!-- Job card -->
<section class="card job" aria-labelledby="job-h">
<div class="job-photo" aria-hidden="true">
<span class="badge bay">Bay 3</span>
<span class="vehicle-glyph">🚗</span>
</div>
<div class="job-body">
<div class="job-top">
<div>
<h1 class="vehicle" id="job-h">2019 Subaru Outback</h1>
<p class="cust">Marisol Vega · Silver</p>
</div>
<span class="status-pill" id="jobStatus" data-status="inprogress">In Progress</span>
</div>
<dl class="specs">
<div><dt>VIN</dt><dd class="tab">4S4BSANC1K3•••421</dd></div>
<div><dt>Plate</dt><dd class="tab">HRT-2914</dd></div>
<div><dt>Odometer</dt><dd class="tab">68,420 mi</dd></div>
<div><dt>Service</dt><dd>Brake job + diagnostic</dd></div>
</dl>
<div class="dtc" title="Diagnostic trouble code">
<span class="dtc-tag">DTC</span>
<span class="tab">P0301</span>
<span class="dtc-desc">Cylinder 1 Misfire Detected</span>
</div>
</div>
</section>
<!-- Clock / timer -->
<section class="card timer" aria-labelledby="timer-h">
<div class="timer-row">
<div>
<h2 id="timer-h" class="sec-h">Labor Timer</h2>
<p class="timer-sub" id="clockState">Not clocked on</p>
</div>
<div class="timer-display tab" id="timerDisplay" aria-live="polite">00:00:00</div>
</div>
<button class="btn clock" id="clockBtn" aria-pressed="false">
<span class="dot" aria-hidden="true"></span>
<span id="clockLabel">Clock On</span>
</button>
</section>
<!-- Task checklist -->
<section class="card" aria-labelledby="tasks-h">
<div class="sec-head">
<h2 class="sec-h" id="tasks-h">Task Checklist</h2>
<span class="count" id="taskCount">0 / 5</span>
</div>
<div class="progress" aria-hidden="true"><span id="progBar"></span></div>
<ul class="tasks" id="taskList">
<li class="task"><label><input type="checkbox" /><span class="check" aria-hidden="true"></span><span class="task-txt">Road test & confirm misfire</span></label></li>
<li class="task"><label><input type="checkbox" /><span class="check" aria-hidden="true"></span><span class="task-txt">Inspect front & rear brake pads</span></label></li>
<li class="task"><label><input type="checkbox" /><span class="check" aria-hidden="true"></span><span class="task-txt">Replace ignition coil — Cyl 1</span></label></li>
<li class="task"><label><input type="checkbox" /><span class="check" aria-hidden="true"></span><span class="task-txt">Replace front rotors & pads</span></label></li>
<li class="task"><label><input type="checkbox" /><span class="check" aria-hidden="true"></span><span class="task-txt">Clear codes & final road test</span></label></li>
</ul>
</section>
<!-- Parts -->
<section class="card" aria-labelledby="parts-h">
<div class="sec-head">
<h2 class="sec-h" id="parts-h">Parts & Labor</h2>
<button class="btn-ghost" id="reqPartBtn">+ Request Part</button>
</div>
<ul class="parts" id="partsList">
<li class="part"><span class="part-name">Ignition Coil <em>(NGK U5168)</em></span><span class="part-meta"><span class="qty">×1</span><span class="tab money">$58.40</span></span></li>
<li class="part"><span class="part-name">Front Brake Pads <em>(Akebono)</em></span><span class="part-meta"><span class="qty">×1</span><span class="tab money">$74.00</span></span></li>
<li class="part"><span class="part-name">Front Rotors <em>(pair)</em></span><span class="part-meta"><span class="qty">×2</span><span class="tab money">$112.00</span></span></li>
<li class="part labor"><span class="part-name">Labor <em>(2.4 hrs)</em></span><span class="part-meta"><span class="tab money">$312.00</span></span></li>
</ul>
<div class="total-row"><span>Estimate total</span><span class="tab money" id="partsTotal">$556.40</span></div>
</section>
<!-- Capture -->
<section class="card" aria-labelledby="cap-h">
<h2 class="sec-h" id="cap-h">Photos & Notes</h2>
<div class="capture-grid" id="captureGrid">
<button class="cap-add" id="addPhoto" aria-label="Add photo">
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M12 8v8M8 12h8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><rect x="3" y="6" width="18" height="13" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><path d="M9 6l1.2-2h3.6L15 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
<span>Photo</span>
</button>
</div>
<div class="note-wrap">
<textarea id="noteInput" rows="2" placeholder="Add a note for the service advisor…" aria-label="Job note"></textarea>
<button class="btn-ghost" id="addNote">Save</button>
</div>
<ul class="notes" id="notesList" aria-live="polite"></ul>
</section>
<div class="footer-space" aria-hidden="true"></div>
</main>
<!-- Sticky action -->
<footer class="action-bar">
<button class="btn complete" id="completeBtn">
<span id="completeLabel">Mark Job Complete</span>
</button>
</footer>
<!-- Complete overlay -->
<div class="overlay" id="overlay" hidden>
<div class="done-card" role="status">
<div class="done-ring"><svg viewBox="0 0 52 52" aria-hidden="true"><circle class="ring-bg" cx="26" cy="26" r="22"/><circle class="ring-fg" cx="26" cy="26" r="22"/><path class="ring-check" d="M16 27l7 7 13-14" fill="none"/></svg></div>
<h3>Job Complete</h3>
<p class="done-sub">WO #4821 sent to service advisor</p>
<p class="done-time tab" id="doneTime">Logged 00:00:00</p>
<button class="btn-ghost wide" id="overlayClose">Back to jobs</button>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Technician Mobile Job View
A phone-sized job view a technician carries into the bay. The header pins the shop name and work-order number, and the job card stacks the vehicle photo, customer, VIN, plate, odometer and the active diagnostic trouble code (P0301) so the whole context is readable at a glance. Status is shown as a colored pill that flips to “Done” once the job is closed out.
The screen is built to be tapped one-handed. Checking off a task animates its checkbox, strikes the label, and advances a progress bar; the “Mark Job Complete” button stays disabled until every task is checked. A clock-on/off button drives a live labor timer with a pulsing indicator, the parts list grows when you request a part and rolls the new cost into the running estimate, and the capture area adds gradient photo thumbnails and timestamped notes for the service advisor.
Marking the job complete auto-clocks-off the timer, updates the status pill, and plays an animated check-ring overlay summarizing the logged time. Everything is vanilla JS with a small toast() helper for inline feedback — no frameworks, no build step — and the layout collapses to a full-screen mobile shell under 520px.
Illustrative UI only — fictional shop/dealership, not a real service system.