Auto — Customer & Vehicle Database
An admin customer and vehicle database for a repair shop, built as one self-contained page. A searchable table matches across names, plates, VINs and phone numbers with quick filters for VIP, open repair orders and overdue service. Clicking a row slides open a customer record showing lifetime spend, visit count, vehicle cards with VIN and odometer, a service-history timeline with diagnostic codes, and an inline note composer that writes back instantly.
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-1: 0 1px 2px rgba(20, 21, 24, 0.06), 0 1px 3px rgba(20, 21, 24, 0.08);
--sh-2: 0 8px 24px rgba(20, 21, 24, 0.12);
--sh-3: 0 24px 60px rgba(20, 21, 24, 0.28);
}
* { box-sizing: border-box; }
html, body { margin: 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;
}
.num, .tnum { font-variant-numeric: tabular-nums; }
button { font-family: inherit; }
.app {
max-width: 1100px;
margin: 0 auto;
padding: 20px 18px 40px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
background: var(--garage);
background: linear-gradient(180deg, var(--garage-2), var(--garage));
color: #fff;
border-radius: var(--r-lg);
padding: 14px 18px;
box-shadow: var(--sh-2);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 38px; height: 38px;
border-radius: 10px;
background: var(--orange);
color: var(--garage);
font-size: 20px;
font-weight: 800;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.2; }
.brand-text strong { font-size: 16px; font-weight: 700; letter-spacing: .2px; }
.brand-text span { font-size: 12px; color: var(--steel-l); }
.topbar-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.chip {
font-size: 12px;
font-weight: 600;
color: #fff;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.14);
padding: 5px 10px;
border-radius: 999px;
font-variant-numeric: tabular-nums;
}
/* Buttons */
.btn {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink);
font-size: 13px;
font-weight: 600;
padding: 8px 13px;
border-radius: var(--r-sm);
cursor: pointer;
transition: transform .08s ease, background .15s ease, border-color .15s ease, box-shadow .15s ease;
}
.btn:hover { border-color: var(--steel); }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.btn-primary {
background: var(--orange);
border-color: var(--orange-d);
color: #fff;
box-shadow: 0 2px 0 var(--orange-d);
}
.btn-primary:hover { background: var(--orange-d); }
.btn-ghost { background: transparent; border-color: transparent; color: var(--steel); }
.btn-ghost:hover { background: rgba(20, 21, 24, 0.05); color: var(--ink); }
/* Layout / panel */
.layout { margin-top: 16px; }
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-1);
overflow: hidden;
}
.list-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
padding: 14px 16px;
border-bottom: 1px solid var(--line);
}
.search {
position: relative;
flex: 1 1 280px;
min-width: 0;
}
.search-ico {
position: absolute;
left: 12px; top: 50%;
transform: translateY(-50%);
color: var(--muted);
font-size: 16px;
}
.search input {
width: 100%;
font: inherit;
font-size: 14px;
padding: 10px 12px 10px 34px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: #fbfbfc;
color: var(--ink);
}
.search input:focus { outline: 2px solid var(--orange); outline-offset: 1px; border-color: var(--orange); }
.filters { display: flex; gap: 6px; flex-wrap: wrap; }
.filter {
border: 1px solid var(--line);
background: var(--surface);
color: var(--steel);
font-size: 12px;
font-weight: 600;
padding: 7px 12px;
border-radius: 999px;
cursor: pointer;
transition: all .14s ease;
}
.filter:hover { border-color: var(--steel); color: var(--ink); }
.filter.active {
background: var(--garage);
border-color: var(--garage);
color: #fff;
}
.filter:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
/* Table */
.table-wrap { overflow-x: auto; }
.cust-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.cust-table thead th {
text-align: left;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .6px;
color: var(--muted);
font-weight: 700;
padding: 11px 16px;
border-bottom: 1px solid var(--line);
background: #fafbfc;
position: sticky;
top: 0;
}
.cust-table th.num, .cust-table td.num { text-align: right; }
.cust-table tbody tr {
cursor: pointer;
transition: background .12s ease;
}
.cust-table tbody tr:hover { background: var(--orange-50); }
.cust-table tbody tr:focus-visible { outline: 2px solid var(--orange); outline-offset: -2px; }
.cust-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.cust-cell { display: flex; align-items: center; gap: 11px; }
.avatar {
flex: none;
width: 34px; height: 34px;
border-radius: 9px;
display: grid;
place-items: center;
font-size: 13px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, var(--steel), var(--garage));
}
.cust-name { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.cust-name strong { font-weight: 600; }
.cust-name span { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.veh-mini { display: flex; flex-direction: column; line-height: 1.3; }
.veh-mini b { font-weight: 600; font-size: 13px; }
.veh-mini small { color: var(--muted); font-size: 11.5px; }
.lifetime { font-weight: 700; font-variant-numeric: tabular-nums; }
.tag {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
white-space: nowrap;
}
.tag::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.tag.vip { color: var(--orange-d); background: var(--orange-50); }
.tag.open { color: #1f63d8; background: #e7effd; }
.tag.overdue { color: var(--danger); background: #fbe9e7; }
.tag.ok { color: var(--ok); background: #e6f4ee; }
.empty { padding: 34px 16px; text-align: center; color: var(--muted); font-size: 14px; }
.list-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 11px 16px;
font-size: 12.5px;
color: var(--ink-2);
border-top: 1px solid var(--line);
background: #fafbfc;
}
.list-foot .dim { color: var(--muted); }
#resultCount { font-weight: 600; }
/* Drawer */
.scrim {
position: fixed;
inset: 0;
background: rgba(20, 21, 24, 0.5);
backdrop-filter: blur(2px);
z-index: 40;
opacity: 0;
animation: fade .2s ease forwards;
}
@keyframes fade { to { opacity: 1; } }
.drawer {
position: fixed;
top: 0; right: 0;
height: 100dvh;
width: min(480px, 100%);
background: var(--bg);
z-index: 50;
box-shadow: var(--sh-3);
transform: translateX(100%);
transition: transform .26s cubic-bezier(.22, 1, .36, 1);
display: flex;
flex-direction: column;
}
.drawer.open { transform: translateX(0); }
.drawer-inner { overflow-y: auto; height: 100%; }
.dr-head {
position: sticky;
top: 0;
background: linear-gradient(180deg, var(--garage-2), var(--garage));
color: #fff;
padding: 18px 20px 16px;
z-index: 2;
}
.dr-head-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.dr-close {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.16);
color: #fff;
width: 32px; height: 32px;
border-radius: 8px;
cursor: pointer;
font-size: 18px;
line-height: 1;
flex: none;
}
.dr-close:hover { background: rgba(255, 255, 255, 0.2); }
.dr-close:focus-visible { outline: 2px solid var(--orange); outline-offset: 2px; }
.dr-cust { display: flex; align-items: center; gap: 13px; margin-top: 4px; }
.dr-cust .avatar { width: 48px; height: 48px; font-size: 17px; border-radius: 12px; }
.dr-cust h2 { margin: 0; font-size: 19px; font-weight: 700; }
.dr-cust .sub { font-size: 12.5px; color: var(--steel-l); font-variant-numeric: tabular-nums; }
.dr-tags { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
.dr-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 14px;
}
.dr-stat {
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--r-sm);
padding: 9px 10px;
}
.dr-stat .k { font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; color: var(--steel-l); font-weight: 700; }
.dr-stat .v { font-size: 17px; font-weight: 800; font-variant-numeric: tabular-nums; margin-top: 2px; }
.dr-body { padding: 16px 20px 28px; }
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20px 0 10px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: .6px;
font-weight: 800;
color: var(--ink-2);
}
.section-title:first-child { margin-top: 4px; }
.section-title .count { color: var(--muted); font-weight: 700; }
/* Vehicle cards */
.veh-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: var(--sh-1);
margin-bottom: 12px;
}
.veh-photo {
height: 88px;
position: relative;
display: flex;
align-items: flex-end;
padding: 10px 12px;
color: #fff;
}
.veh-photo::after {
content: "";
position: absolute;
inset: 0;
background: rgba(20, 21, 24, 0.28);
}
.veh-photo > * { position: relative; z-index: 1; }
.veh-photo .yr {
font-weight: 800;
font-size: 13px;
background: rgba(20, 21, 24, 0.45);
padding: 3px 8px;
border-radius: 6px;
font-variant-numeric: tabular-nums;
}
.veh-meta { padding: 11px 13px 13px; }
.veh-meta h4 { margin: 0; font-size: 15px; font-weight: 700; }
.veh-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 12px;
margin-top: 9px;
}
.veh-grid div { font-size: 12px; }
.veh-grid .k { color: var(--muted); font-weight: 600; }
.veh-grid .vv { font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: .2px; }
.veh-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 11px;
padding-top: 10px;
border-top: 1px solid var(--line);
}
.svc-due { font-size: 12px; font-weight: 600; }
.svc-due.warn { color: var(--warn); }
.svc-due.ok { color: var(--ok); }
/* Service history */
.history { list-style: none; margin: 0; padding: 0; position: relative; }
.history::before {
content: "";
position: absolute;
left: 7px; top: 4px; bottom: 4px;
width: 2px;
background: var(--line-2);
}
.hist-item {
position: relative;
padding: 0 0 14px 26px;
}
.hist-item::before {
content: "";
position: absolute;
left: 2px; top: 3px;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--surface);
border: 2px solid var(--steel);
}
.hist-item.done::before { border-color: var(--done); background: var(--done); }
.hist-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.hist-head b { font-size: 13.5px; font-weight: 700; }
.hist-head .date { font-size: 11.5px; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.hist-sub { font-size: 12px; color: var(--ink-2); margin-top: 2px; }
.hist-sub .ro { color: var(--muted); font-variant-numeric: tabular-nums; }
.hist-sub .dtc {
display: inline-block;
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 11px;
font-weight: 700;
color: var(--danger);
background: #fbe9e7;
padding: 1px 6px;
border-radius: 5px;
margin-left: 4px;
}
.hist-amt { font-weight: 700; font-variant-numeric: tabular-nums; font-size: 13px; margin-top: 3px; }
/* Notes */
.notes { display: flex; flex-direction: column; gap: 8px; }
.note {
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--orange);
border-radius: var(--r-sm);
padding: 9px 11px;
font-size: 12.5px;
color: var(--ink-2);
animation: pop .25s ease;
}
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
.note .meta { display: block; font-size: 10.5px; color: var(--muted); font-weight: 700; margin-top: 4px; text-transform: uppercase; letter-spacing: .4px; }
.note-add { display: flex; gap: 8px; margin-top: 10px; }
.note-add input {
flex: 1;
font: inherit;
font-size: 13px;
padding: 9px 11px;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
background: var(--surface);
}
.note-add input:focus { outline: 2px solid var(--orange); outline-offset: 1px; border-color: var(--orange); }
/* Toast */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
z-index: 70;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
background: var(--garage);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 16px;
border-radius: 999px;
box-shadow: var(--sh-2);
display: flex;
align-items: center;
gap: 8px;
animation: toastIn .25s ease;
}
.toast::before { content: "✓"; color: var(--orange); font-weight: 800; }
.toast.leaving { animation: toastOut .25s ease forwards; }
@keyframes toastIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(12px); } }
/* Responsive */
@media (max-width: 720px) {
.dr-stats { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.app { padding: 12px 10px 32px; }
.topbar { padding: 12px 14px; }
.hide-sm { display: none; }
.cust-table thead th { padding: 10px 12px; }
.cust-table td { padding: 11px 12px; }
.veh-grid { grid-template-columns: 1fr; }
.dr-stats { grid-template-columns: repeat(3, 1fr); }
.dr-stat .v { font-size: 14px; }
.topbar-actions .chip { display: none; }
}(function () {
"use strict";
var GRADIENTS = [
"linear-gradient(135deg,#3b4049,#141518)",
"linear-gradient(135deg,#5b6470,#1f2127)",
"linear-gradient(135deg,#7a4a2a,#2a1d12)",
"linear-gradient(135deg,#2a4a55,#10242b)",
"linear-gradient(135deg,#4a3a55,#1a1024)",
"linear-gradient(135deg,#55482a,#241d10)"
];
var customers = [
{
id: "c1", name: "Marisol Vega", since: "2019", phone: "(503) 555-0142",
email: "[email protected]", advisor: "T. Okafor",
flags: ["vip"], lastVisit: "2026-06-04", lifetime: 14820, visits: 31, openRO: false, due: false,
vehicles: [
{ yr: 2021, make: "Toyota", model: "Tacoma TRD", plate: "JKR-4471", vin: "5TFCZ5AN1MX012834", odo: 61240, color: "Cement", nextSvc: "2026-09", dueWarn: false, g: 2 },
{ yr: 2017, make: "Subaru", model: "Outback", plate: "BNV-2203", vin: "4S4BSANC3H3210945", odo: 118930, color: "Slate", nextSvc: "2026-07", dueWarn: true, g: 3 }
],
history: [
{ date: "2026-06-04", ro: "RO-20418", title: "Brake pads + rotors, front", veh: "21 Tacoma", amt: 642.18, dtc: "", done: true },
{ date: "2026-03-11", ro: "RO-19880", title: "60k major service", veh: "21 Tacoma", amt: 1184.5, dtc: "", done: true },
{ date: "2025-12-02", ro: "RO-19112", title: "Misfire diagnosis cyl 3", veh: "17 Outback", amt: 388.0, dtc: "P0303", done: true }
],
notes: [{ t: "Prefers OEM parts only. Texts before noon.", who: "T. Okafor", when: "Mar 2026" }]
},
{
id: "c2", name: "Dwayne Pruitt", since: "2022", phone: "(503) 555-0188",
email: "[email protected]", advisor: "L. Hassan",
flags: ["open"], lastVisit: "2026-06-16", lifetime: 5210, visits: 9, openRO: true, due: false,
vehicles: [
{ yr: 2019, make: "Ford", model: "F-250 Super Duty", plate: "HWL-9920", vin: "1FT7W2BT4KEC55120", odo: 88410, color: "Oxford White", nextSvc: "2026-06", dueWarn: false, g: 0 }
],
history: [
{ date: "2026-06-16", ro: "RO-20502", title: "Turbo boost leak — in progress", veh: "19 F-250", amt: 0, dtc: "P0299", done: false },
{ date: "2026-01-20", ro: "RO-19340", title: "Oil + fuel filter service", veh: "19 F-250", amt: 410.7, dtc: "", done: true }
],
notes: []
},
{
id: "c3", name: "Priya Anand", since: "2020", phone: "(503) 555-0119",
email: "[email protected]", advisor: "T. Okafor",
flags: ["vip", "overdue"], lastVisit: "2025-11-28", lifetime: 9640, visits: 18, openRO: false, due: true,
vehicles: [
{ yr: 2023, make: "Tesla", model: "Model Y LR", plate: "EVX-1188", vin: "7SAYGDEE0PA110233", odo: 29870, color: "Midnight", nextSvc: "2026-05", dueWarn: true, g: 4 },
{ yr: 2015, make: "Honda", model: "CR-V EX", plate: "MTR-6604", vin: "5J6RM4H53FL045761", odo: 142005, color: "Silver", nextSvc: "2026-04", dueWarn: true, g: 1 }
],
history: [
{ date: "2025-11-28", ro: "RO-18990", title: "Tire rotation + alignment", veh: "23 Model Y", amt: 215.0, dtc: "", done: true },
{ date: "2025-08-09", ro: "RO-18221", title: "Timing belt + water pump", veh: "15 CR-V", amt: 1320.0, dtc: "", done: true }
],
notes: [{ t: "Both vehicles overdue for service — send reminder.", who: "System", when: "Jun 2026" }]
},
{
id: "c4", name: "Hollow Creek Plumbing", since: "2018", phone: "(503) 555-0301",
email: "[email protected]", advisor: "L. Hassan",
flags: ["vip", "open"], lastVisit: "2026-06-12", lifetime: 41250, visits: 64, openRO: true, due: false,
vehicles: [
{ yr: 2020, make: "Ram", model: "ProMaster 2500", plate: "FLT-0021", vin: "3C6TRVDG1LE110456", odo: 102330, color: "Bright White", nextSvc: "2026-08", dueWarn: false, g: 5 },
{ yr: 2020, make: "Ram", model: "ProMaster 2500", plate: "FLT-0022", vin: "3C6TRVDG8LE110489", odo: 97640, color: "Bright White", nextSvc: "2026-07", dueWarn: true, g: 5 },
{ yr: 2018, make: "Chevrolet", model: "Express 3500", plate: "FLT-0014", vin: "1GCWGAFG2J1234567", odo: 161200, color: "Summit White", nextSvc: "2026-09", dueWarn: false, g: 1 }
],
history: [
{ date: "2026-06-12", ro: "RO-20470", title: "Fleet brake inspection (3 units)", veh: "Fleet", amt: 0, dtc: "", done: false },
{ date: "2026-02-28", ro: "RO-19590", title: "Transmission service — FLT-0014", veh: "18 Express", amt: 980.4, dtc: "P0700", done: true }
],
notes: [{ t: "Net-30 billing. PO required on every RO.", who: "L. Hassan", when: "2024" }]
},
{
id: "c5", name: "Beto Salcedo", since: "2024", phone: "(503) 555-0177",
email: "[email protected]", advisor: "T. Okafor",
flags: [], lastVisit: "2026-05-22", lifetime: 1180, visits: 3, openRO: false, due: false,
vehicles: [
{ yr: 2016, make: "Mazda", model: "MX-5 Miata", plate: "ZIP-0101", vin: "JM1NDAB75G0123980", odo: 44120, color: "Soul Red", nextSvc: "2026-10", dueWarn: false, g: 2 }
],
history: [
{ date: "2026-05-22", ro: "RO-20210", title: "Synthetic oil change", veh: "16 MX-5", amt: 96.0, dtc: "", done: true }
],
notes: []
},
{
id: "c6", name: "Greer & Sons Landscaping", since: "2021", phone: "(503) 555-0260",
email: "[email protected]", advisor: "L. Hassan",
flags: ["overdue"], lastVisit: "2025-10-04", lifetime: 18760, visits: 27, openRO: false, due: true,
vehicles: [
{ yr: 2014, make: "GMC", model: "Sierra 2500HD", plate: "DRT-7788", vin: "1GT220CG4EZ334120", odo: 198450, color: "Onyx", nextSvc: "2026-03", dueWarn: true, g: 0 },
{ yr: 2019, make: "Isuzu", model: "NPR-HD", plate: "BOX-4502", vin: "JALC4W164K7000981", odo: 76900, color: "White", nextSvc: "2026-05", dueWarn: true, g: 3 }
],
history: [
{ date: "2025-10-04", ro: "RO-18540", title: "Glow plug replacement", veh: "14 Sierra", amt: 540.0, dtc: "P0671", done: true },
{ date: "2025-06-17", ro: "RO-17720", title: "DPF regen + diagnostics", veh: "19 NPR-HD", amt: 720.0, dtc: "P2463", done: true }
],
notes: [{ t: "Seasonal — busiest spring/summer. Off-hours drop-off OK.", who: "L. Hassan", when: "May 2025" }]
}
];
// ---- helpers ----
function $(s, r) { return (r || document).querySelector(s); }
function el(tag, cls, html) {
var e = document.createElement(tag);
if (cls) e.className = cls;
if (html != null) e.innerHTML = html;
return e;
}
function initials(name) {
var p = name.replace(/[&]/g, "").trim().split(/\s+/);
return ((p[0] || "")[0] + (p[1] || "")[0] || (p[0] || "?")[0]).toUpperCase();
}
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function money0(n) { return "$" + Math.round(n).toLocaleString("en-US"); }
function fmtDate(s) {
var d = new Date(s + "T00:00:00");
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
}
function relDays(s) {
var diff = Math.round((Date.now() - new Date(s + "T00:00:00")) / 86400000);
if (diff <= 0) return "today";
if (diff === 1) return "yesterday";
if (diff < 45) return diff + "d ago";
return Math.round(diff / 30) + "mo ago";
}
var toastWrap = $("#toastWrap");
function toast(msg) {
var t = el("div", "toast", msg);
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("leaving");
setTimeout(function () { t.remove(); }, 260);
}, 2200);
}
// ---- list rendering ----
var custBody = $("#custBody");
var resultCount = $("#resultCount");
var emptyState = $("#emptyState");
var emptyTerm = $("#emptyTerm");
var searchInput = $("#searchInput");
var activeFilter = "all";
var term = "";
function matchesFilter(c) {
if (activeFilter === "all") return true;
if (activeFilter === "vip") return c.flags.indexOf("vip") > -1;
if (activeFilter === "open") return c.openRO;
if (activeFilter === "overdue") return c.due;
return true;
}
function matchesTerm(c) {
if (!term) return true;
var hay = [c.name, c.phone, c.email];
c.vehicles.forEach(function (v) {
hay.push(v.plate, v.vin, v.make, v.model, String(v.yr));
});
return hay.join(" ").toLowerCase().indexOf(term) > -1;
}
function statusTag(c) {
if (c.openRO) return '<span class="tag open">Open RO</span>';
if (c.due) return '<span class="tag overdue">Service due</span>';
if (c.flags.indexOf("vip") > -1) return '<span class="tag vip">VIP</span>';
return '<span class="tag ok">Active</span>';
}
function render() {
var list = customers.filter(function (c) { return matchesFilter(c) && matchesTerm(c); });
custBody.innerHTML = "";
list.forEach(function (c) {
var primary = c.vehicles[0];
var tr = el("tr");
tr.tabIndex = 0;
tr.setAttribute("role", "button");
tr.setAttribute("aria-label", "Open record for " + c.name);
tr.innerHTML =
'<td><div class="cust-cell">' +
'<span class="avatar" style="background:' + GRADIENTS[initials(c.name).charCodeAt(0) % GRADIENTS.length] + '">' + initials(c.name) + '</span>' +
'<span class="cust-name"><strong>' + c.name + '</strong><span>' + c.phone + ' · cust. since ' + c.since + '</span></span>' +
'</div></td>' +
'<td class="hide-sm"><div class="veh-mini"><b>' + primary.yr + ' ' + primary.make + ' ' + primary.model + '</b>' +
'<small>' + primary.plate + (c.vehicles.length > 1 ? ' · +' + (c.vehicles.length - 1) + ' more' : '') + '</small></div></td>' +
'<td class="hide-sm tnum">' + relDays(c.lastVisit) + '</td>' +
'<td class="num"><span class="lifetime">' + money0(c.lifetime) + '</span></td>' +
'<td class="num">' + statusTag(c) + '</td>';
tr.addEventListener("click", function () { openDrawer(c.id); });
tr.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openDrawer(c.id); }
});
custBody.appendChild(tr);
});
var n = list.length;
resultCount.textContent = n + (n === 1 ? " customer" : " customers");
if (n === 0 && term) {
emptyState.hidden = false;
emptyTerm.textContent = term;
} else {
emptyState.hidden = true;
}
}
searchInput.addEventListener("input", function () {
term = searchInput.value.trim().toLowerCase();
render();
});
Array.prototype.forEach.call(document.querySelectorAll(".filter"), function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".filter").forEach(function (b) {
b.classList.remove("active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("active");
btn.setAttribute("aria-selected", "true");
activeFilter = btn.dataset.filter;
render();
});
});
// ---- drawer ----
var drawer = $("#drawer");
var drawerInner = $("#drawerInner");
var scrim = $("#scrim");
var lastFocus = null;
function byId(id) {
for (var i = 0; i < customers.length; i++) if (customers[i].id === id) return customers[i];
return null;
}
function drawerHTML(c) {
var vehCount = c.vehicles.length;
var tags = "";
if (c.flags.indexOf("vip") > -1) tags += '<span class="tag vip">VIP</span>';
if (c.openRO) tags += '<span class="tag open">Open RO</span>';
if (c.due) tags += '<span class="tag overdue">Service due</span>';
if (!tags) tags = '<span class="tag ok">Active</span>';
var vehicles = c.vehicles.map(function (v) {
return '' +
'<div class="veh-card">' +
'<div class="veh-photo" style="background:' + GRADIENTS[v.g % GRADIENTS.length] + '">' +
'<span class="yr">' + v.yr + '</span>' +
'</div>' +
'<div class="veh-meta">' +
'<h4>' + v.make + ' ' + v.model + '</h4>' +
'<div class="veh-grid">' +
'<div><span class="k">Plate</span><br><span class="vv">' + v.plate + '</span></div>' +
'<div><span class="k">Odometer</span><br><span class="vv">' + v.odo.toLocaleString("en-US") + ' mi</span></div>' +
'<div style="grid-column:1/-1"><span class="k">VIN</span><br><span class="vv">' + v.vin + '</span></div>' +
'<div><span class="k">Color</span><br><span class="vv">' + v.color + '</span></div>' +
'<div><span class="k">Next service</span><br><span class="vv">' + v.nextSvc + '</span></div>' +
'</div>' +
'<div class="veh-foot">' +
'<span class="svc-due ' + (v.dueWarn ? 'warn' : 'ok') + '">' + (v.dueWarn ? '⚠ Service due' : '✓ Up to date') + '</span>' +
'<button class="btn btn-ghost btn-hist" data-veh="' + v.plate + '" type="button">View history</button>' +
'</div>' +
'</div>' +
'</div>';
}).join("");
var history = c.history.map(function (h) {
return '' +
'<li class="hist-item ' + (h.done ? 'done' : '') + '" data-veh="' + h.veh + '">' +
'<div class="hist-head"><b>' + h.title + '</b><span class="date">' + fmtDate(h.date) + '</span></div>' +
'<div class="hist-sub"><span class="ro">' + h.ro + '</span> · ' + h.veh +
(h.dtc ? '<span class="dtc">' + h.dtc + '</span>' : '') + '</div>' +
(h.done ? '<div class="hist-amt">' + money(h.amt) + '</div>' : '<div class="hist-amt" style="color:var(--inprogress)">In progress</div>') +
'</li>';
}).join("");
var notes = c.notes.length
? c.notes.map(function (nt) {
return '<div class="note">' + nt.t + '<span class="meta">' + nt.who + ' · ' + nt.when + '</span></div>';
}).join("")
: '<div class="note" style="border-left-color:var(--steel);color:var(--muted)">No notes yet.</div>';
return '' +
'<div class="dr-head">' +
'<div class="dr-head-top">' +
'<div class="dr-cust">' +
'<span class="avatar" style="background:' + GRADIENTS[initials(c.name).charCodeAt(0) % GRADIENTS.length] + '">' + initials(c.name) + '</span>' +
'<div><h2>' + c.name + '</h2><div class="sub">' + c.phone + ' · ' + c.email + '</div></div>' +
'</div>' +
'<button class="dr-close" id="drClose" aria-label="Close record" type="button">×</button>' +
'</div>' +
'<div class="dr-tags">' + tags + '</div>' +
'<div class="dr-stats">' +
'<div class="dr-stat"><div class="k">Lifetime</div><div class="v">' + money0(c.lifetime) + '</div></div>' +
'<div class="dr-stat"><div class="k">Visits</div><div class="v">' + c.visits + '</div></div>' +
'<div class="dr-stat"><div class="k">Vehicles</div><div class="v">' + vehCount + '</div></div>' +
'</div>' +
'</div>' +
'<div class="dr-body">' +
'<div class="section-title">Vehicles <span class="count">' + vehCount + '</span></div>' +
vehicles +
'<div class="section-title">Service history <span class="count">' + c.history.length + '</span></div>' +
'<ul class="history">' + history + '</ul>' +
'<div class="section-title">Notes <span class="count">' + c.notes.length + '</span></div>' +
'<div class="notes" id="noteList">' + notes + '</div>' +
'<div class="note-add">' +
'<input id="noteInput" type="text" placeholder="Add a note (e.g. prefers OEM parts)…" aria-label="Add a note" />' +
'<button class="btn btn-primary" id="noteSave" type="button">Add</button>' +
'</div>' +
'</div>';
}
function openDrawer(id) {
var c = byId(id);
if (!c) return;
lastFocus = document.activeElement;
drawerInner.innerHTML = drawerHTML(c);
scrim.hidden = false;
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
$("#drClose").addEventListener("click", closeDrawer);
// vehicle history -> scroll & highlight matching rows
Array.prototype.forEach.call(drawerInner.querySelectorAll(".btn-hist"), function (b) {
b.addEventListener("click", function () {
var plate = b.dataset.veh;
var v = c.vehicles.filter(function (x) { return x.plate === plate; })[0];
var label = v.yr + " " + v.make + " " + v.model;
var hist = drawerInner.querySelector(".history");
var matched = 0;
Array.prototype.forEach.call(drawerInner.querySelectorAll(".hist-item"), function (li) {
var hit = li.dataset.veh && (li.dataset.veh.indexOf(String(v.yr).slice(-2)) > -1 ||
li.dataset.veh.toLowerCase().indexOf(v.make.toLowerCase()) > -1 ||
li.dataset.veh.toLowerCase().indexOf("fleet") > -1);
li.style.opacity = hit ? "1" : "0.3";
if (hit) matched++;
});
if (hist) hist.scrollIntoView({ behavior: "smooth", block: "start" });
toast(matched + " record" + (matched === 1 ? "" : "s") + " for " + label);
});
});
// add note
var noteInput = $("#noteInput");
var noteSave = $("#noteSave");
function addNote() {
var val = noteInput.value.trim();
if (!val) { noteInput.focus(); return; }
c.notes.unshift({ t: val, who: c.advisor, when: "Just now" });
// refresh notes section in place
var list = $("#noteList");
var placeholder = list.querySelector(".note[style]");
if (placeholder) placeholder.remove();
var node = el("div", "note", val + '<span class="meta">' + c.advisor + ' · Just now</span>');
list.insertBefore(node, list.firstChild);
// bump count badge
var badges = drawerInner.querySelectorAll(".section-title .count");
if (badges[2]) badges[2].textContent = c.notes.length;
noteInput.value = "";
noteInput.focus();
toast("Note added to " + c.name.split(" ")[0] + "’s record");
}
noteSave.addEventListener("click", addNote);
noteInput.addEventListener("keydown", function (e) {
if (e.key === "Enter") { e.preventDefault(); addNote(); }
});
setTimeout(function () { drawer.focus(); }, 60);
}
function closeDrawer() {
drawer.classList.remove("open");
drawer.setAttribute("aria-hidden", "true");
scrim.hidden = true;
document.body.style.overflow = "";
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
scrim.addEventListener("click", closeDrawer);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && drawer.classList.contains("open")) closeDrawer();
});
$("#addCustomerBtn").addEventListener("click", function () {
toast("New customer intake — opens the registration form");
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Auto — Customer & Vehicle Database</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">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◧</span>
<div class="brand-text">
<strong>Ironwood Auto & Diesel</strong>
<span>Customer & Vehicle Database</span>
</div>
</div>
<div class="topbar-actions">
<span class="chip">2,184 customers</span>
<span class="chip">3,061 vehicles</span>
<button class="btn btn-primary" id="addCustomerBtn" type="button">+ New customer</button>
</div>
</header>
<main class="layout">
<section class="panel list-panel" aria-label="Customer list">
<div class="list-head">
<div class="search">
<span class="search-ico" aria-hidden="true">⌕</span>
<input id="searchInput" type="search" placeholder="Search name, plate, VIN, phone…" aria-label="Search customers and vehicles" autocomplete="off" />
</div>
<div class="filters" role="tablist" aria-label="Filter customers">
<button class="filter active" data-filter="all" role="tab" aria-selected="true" type="button">All</button>
<button class="filter" data-filter="vip" role="tab" aria-selected="false" type="button">VIP</button>
<button class="filter" data-filter="open" role="tab" aria-selected="false" type="button">Open RO</button>
<button class="filter" data-filter="overdue" role="tab" aria-selected="false" type="button">Service due</button>
</div>
</div>
<div class="table-wrap">
<table class="cust-table">
<thead>
<tr>
<th scope="col">Customer</th>
<th scope="col" class="hide-sm">Vehicles</th>
<th scope="col" class="hide-sm">Last visit</th>
<th scope="col" class="num">Lifetime</th>
<th scope="col" class="num">Status</th>
</tr>
</thead>
<tbody id="custBody"><!-- rows injected --></tbody>
</table>
<p class="empty" id="emptyState" hidden>No customers match “<span id="emptyTerm"></span>”.</p>
</div>
<footer class="list-foot">
<span id="resultCount">0 customers</span>
<span class="dim">Click a row to open the customer record</span>
</footer>
</section>
</main>
</div>
<!-- Customer drawer -->
<div class="scrim" id="scrim" hidden></div>
<aside class="drawer" id="drawer" aria-hidden="true" aria-label="Customer record" tabindex="-1">
<div class="drawer-inner" id="drawerInner"><!-- detail injected --></div>
</aside>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Customer & Vehicle Database
A service-advisor view of a shop’s customer book. The main panel is a searchable table where each row pairs a customer with their primary vehicle, last-visit recency, lifetime spend in tabular figures, and a status tag — VIP, open RO, service due, or active. A live search box matches across names, phone numbers, plates and full VINs, and a row of pill filters narrows the list to VIPs, open repair orders, or vehicles overdue for service.
Clicking any row slides in a customer record drawer over a dimmed scrim. The header carries the customer, contact details and a three-up stat block — lifetime spend, visit count, vehicle count — over the garage-black brand gradient. Below it, gradient-placeholder vehicle cards lay out plate, odometer, VIN, color and next-service date with up-to-date or service-due badges, followed by a service-history timeline whose line items show the RO number, vehicle, money totals and inline diagnostic codes such as P0303 or P0299.
Everything is self-contained vanilla HTML, CSS and JS — no frameworks, no build. Per-vehicle “View history” buttons focus the timeline and surface a count toast, an inline composer adds notes that pop into the record and bump the section badge, and the drawer is keyboard-usable with Escape-to-close and focus restoration. The layout collapses to a mobile-first single column under 520px and a small toast() helper confirms each action.
Illustrative UI only — fictional shop/dealership, not a real service system.