Salon — Revenue per Stylist + Service Mix
An owner-grade salon analytics dashboard with KPI cards for revenue, bookings, average ticket and retail share, each carrying trend deltas against the prior period. A pure-CSS bar chart ranks revenue per stylist, a conic donut breaks down the Hair, Color, Nails and Spa service mix, and a top-performers list sits beside a recent-transactions table. A Today, Week and Month segmented control swaps precomputed datasets with animated counters, all wrapped in a luxe rose-gold and cream editorial aesthetic.
MCP
Kod
:root {
--serif: "Cormorant Garamond", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
--gold: #b08d57;
--gold-d: #8c6d3f;
--gold-soft: #efe2cf;
--rose: #c9a78f;
--rose-soft: #f3e6dc;
--ink: #1c1814;
--ink-2: #3d362f;
--muted: #8a7d70;
--cream: #f7f1e8;
--bg: #faf6ef;
--white: #ffffff;
--line: rgba(28, 24, 20, 0.1);
--line-2: rgba(28, 24, 20, 0.18);
--ok: #5f8a6b;
--warn: #c08a3e;
--danger: #b3503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-1: 0 1px 2px rgba(28, 24, 20, 0.05), 0 1px 0 rgba(255, 255, 255, 0.6) inset;
--sh-2: 0 18px 44px -28px rgba(28, 24, 20, 0.4), 0 2px 8px rgba(28, 24, 20, 0.04);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background:
radial-gradient(1200px 600px at 90% -10%, rgba(176, 141, 87, 0.08), transparent 60%),
var(--bg);
color: var(--ink);
font-family: var(--sans);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
h1,
h2,
h3 {
font-family: var(--serif);
font-weight: 600;
margin: 0;
letter-spacing: 0.01em;
}
.shell {
max-width: 1240px;
margin: 0 auto;
padding: 0 28px 64px;
}
/* ===== Topbar ===== */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
padding: 18px 0 16px;
margin-bottom: 8px;
background: linear-gradient(var(--bg) 70%, rgba(250, 246, 239, 0));
border-bottom: 1px solid var(--line);
}
.brand {
display: flex;
align-items: center;
gap: 13px;
}
.brand__mark {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 50%;
font-family: var(--serif);
font-weight: 700;
font-size: 1.05rem;
color: var(--white);
background:
radial-gradient(120% 120% at 30% 20%, #c9a667, var(--gold-d));
box-shadow: 0 6px 18px -8px rgba(140, 109, 63, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.4) inset;
letter-spacing: 0.04em;
}
.brand__text {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand__name {
font-family: var(--serif);
font-size: 1.32rem;
font-weight: 600;
}
.brand__sub {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--muted);
}
.topbar__actions {
display: flex;
align-items: center;
gap: 12px;
}
/* Segmented control */
.seg {
display: inline-flex;
padding: 4px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.seg__btn {
appearance: none;
border: 0;
background: transparent;
font: inherit;
font-size: 0.85rem;
font-weight: 500;
color: var(--muted);
padding: 7px 18px;
border-radius: 999px;
cursor: pointer;
transition: color 0.2s, background 0.2s, box-shadow 0.2s;
}
.seg__btn:hover {
color: var(--ink-2);
}
.seg__btn.is-active {
color: var(--ink);
background: linear-gradient(180deg, var(--gold-soft), var(--rose-soft));
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.7) inset, 0 2px 6px -3px rgba(140, 109, 63, 0.5);
}
.seg__btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
.btn {
appearance: none;
font: inherit;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
border-radius: 999px;
padding: 8px 16px;
display: inline-flex;
align-items: center;
gap: 7px;
transition: transform 0.15s, box-shadow 0.2s, background 0.2s, border-color 0.2s;
}
.btn:active {
transform: translateY(1px);
}
.btn--ghost {
background: var(--white);
border: 1px solid var(--line);
color: var(--ink-2);
box-shadow: var(--sh-1);
}
.btn--ghost:hover {
border-color: var(--gold);
color: var(--ink);
}
.btn:focus-visible {
outline: 2px solid var(--gold);
outline-offset: 2px;
}
/* ===== Page head ===== */
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin: 22px 0 24px;
}
.eyebrow {
margin: 0 0 4px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--gold-d);
font-weight: 600;
}
.page-title {
font-size: clamp(1.9rem, 4vw, 2.6rem);
line-height: 1.05;
}
.page-meta {
margin: 0;
font-size: 0.88rem;
color: var(--muted);
}
.page-meta strong {
color: var(--ink-2);
text-transform: capitalize;
}
/* ===== Cards ===== */
.card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 22px;
}
.card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 18px;
}
.card__title {
font-size: 1.4rem;
}
.card__sub {
margin: 2px 0 0;
font-size: 0.8rem;
color: var(--muted);
}
.hint {
font-size: 0.78rem;
font-weight: 600;
color: var(--gold-d);
white-space: nowrap;
}
/* ===== KPI ===== */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.kpi {
padding: 20px 22px;
position: relative;
overflow: hidden;
}
.kpi::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: linear-gradient(var(--gold), var(--rose));
opacity: 0.85;
}
.kpi__label {
margin: 0 0 8px;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
font-weight: 600;
}
.kpi__value {
margin: 0;
font-family: var(--serif);
font-size: 2.1rem;
font-weight: 700;
letter-spacing: 0.01em;
line-height: 1;
}
.kpi__delta {
margin: 12px 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.chip {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 8px;
border-radius: 999px;
font-weight: 600;
font-size: 0.72rem;
margin-right: 4px;
}
.chip--up {
color: var(--ok);
background: rgba(95, 138, 107, 0.12);
}
.chip--down {
color: var(--danger);
background: rgba(179, 80, 62, 0.12);
}
/* ===== Grid ===== */
.grid-2 {
display: grid;
grid-template-columns: 1.15fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.grid-2--lower {
grid-template-columns: 1fr 1.25fr;
}
/* ===== Bar chart ===== */
.bars {
display: flex;
flex-direction: column;
gap: 14px;
}
.bar-row {
display: grid;
grid-template-columns: 96px 1fr auto;
align-items: center;
gap: 12px;
}
.bar-row__name {
font-size: 0.83rem;
font-weight: 500;
color: var(--ink-2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bar-track {
height: 12px;
border-radius: 999px;
background: var(--cream);
overflow: hidden;
}
.bar-fill {
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--rose), var(--gold));
transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.bar-row:nth-child(1) .bar-fill {
background: linear-gradient(90deg, var(--gold), var(--gold-d));
}
.bar-row__val {
font-size: 0.82rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--ink);
min-width: 56px;
text-align: right;
}
/* ===== Donut / mix ===== */
.mix {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
}
.donut {
--c1: var(--gold);
position: relative;
width: 168px;
height: 168px;
border-radius: 50%;
flex: 0 0 auto;
display: grid;
place-items: center;
background: conic-gradient(var(--cream) 0 100%);
transition: background 0.8s ease;
}
.donut__hole {
width: 104px;
height: 104px;
border-radius: 50%;
background: var(--white);
box-shadow: inset 0 0 0 1px var(--line);
display: grid;
place-content: center;
text-align: center;
gap: 2px;
}
.donut__num {
font-family: var(--serif);
font-size: 1.5rem;
font-weight: 700;
line-height: 1;
}
.donut__cap {
font-size: 0.64rem;
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--muted);
}
.legend {
list-style: none;
margin: 0;
padding: 0;
flex: 1 1 160px;
min-width: 160px;
display: flex;
flex-direction: column;
gap: 11px;
}
.legend li {
display: grid;
grid-template-columns: 12px 1fr auto;
align-items: center;
gap: 10px;
font-size: 0.85rem;
}
.legend .dot {
width: 11px;
height: 11px;
border-radius: 3px;
}
.legend .lg-name {
color: var(--ink-2);
}
.legend .lg-val {
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--ink);
}
/* ===== Performers ===== */
.performers {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.performer {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 13px;
padding: 13px 0;
border-top: 1px solid var(--line);
}
.performer:first-child {
border-top: 0;
}
.performer__rank {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 50%;
font-size: 0.78rem;
font-weight: 700;
color: var(--gold-d);
background: var(--gold-soft);
font-variant-numeric: tabular-nums;
}
.performer:first-child .performer__rank {
color: var(--white);
background: linear-gradient(140deg, #c9a667, var(--gold-d));
}
.performer__name {
font-weight: 600;
font-size: 0.92rem;
}
.performer__role {
display: block;
font-size: 0.74rem;
font-weight: 400;
color: var(--muted);
}
.performer__amt {
text-align: right;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.performer__amt small {
display: block;
font-weight: 500;
font-size: 0.72rem;
color: var(--ok);
}
/* ===== Table ===== */
.table-wrap {
overflow-x: auto;
}
.txn {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
}
.txn th {
text-align: left;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--muted);
font-weight: 600;
padding: 0 0 10px;
border-bottom: 1px solid var(--line-2);
}
.txn td {
padding: 12px 0;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.txn tr:last-child td {
border-bottom: 0;
}
.txn .ta-r {
text-align: right;
}
.txn .client {
font-weight: 600;
}
.txn .svc {
display: inline-flex;
align-items: center;
gap: 6px;
}
.svc-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.txn .amt {
text-align: right;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
background: var(--ink);
color: var(--cream);
padding: 12px 20px;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s, transform 0.3s;
z-index: 60;
}
.toast.is-on {
opacity: 1;
transform: translate(-50%, 0);
}
.toast::before {
content: "✦";
color: var(--gold);
margin-right: 8px;
}
/* ===== Responsive ===== */
@media (max-width: 900px) {
.kpis {
grid-template-columns: repeat(2, 1fr);
}
.grid-2,
.grid-2--lower {
grid-template-columns: 1fr;
}
}
@media (max-width: 520px) {
.shell {
padding: 0 16px 48px;
}
.topbar {
padding-top: 14px;
}
.topbar__actions {
width: 100%;
justify-content: space-between;
}
.seg__btn {
padding: 7px 13px;
font-size: 0.8rem;
}
.kpis {
grid-template-columns: 1fr;
}
.kpi__value {
font-size: 1.85rem;
}
.bar-row {
grid-template-columns: 80px 1fr auto;
gap: 9px;
}
.mix {
justify-content: center;
}
.card {
padding: 18px;
}
}(function () {
"use strict";
/* ---------- Service categories ---------- */
var CATS = {
Hair: "#b08d57",
Color: "#c9a78f",
Nails: "#8c6d3f",
Spa: "#5f8a6b",
};
/* ---------- Precomputed datasets per range ---------- */
var DATA = {
today: {
label: "today",
crew: "12 stylists on the floor",
kpis: {
revenue: { value: 4820, prev: 4310, fmt: "money" },
bookings: { value: 38, prev: 35, fmt: "int" },
ticket: { value: 127, prev: 123, fmt: "money" },
retail: { value: 18, prev: 16, fmt: "pct" },
},
stylists: [
{ name: "Aria Vance", role: "Master Stylist", rev: 1180, svc: 9 },
{ name: "Noor Haddad", role: "Color Director", rev: 980, svc: 7 },
{ name: "Léa Moreau", role: "Senior Stylist", rev: 760, svc: 8 },
{ name: "Theo Brandt", role: "Stylist", rev: 640, svc: 6 },
{ name: "Mira Solis", role: "Nail Artist", rev: 540, svc: 11 },
{ name: "Jun Park", role: "Spa Therapist", rev: 480, svc: 5 },
],
mix: [
{ cat: "Hair", val: 41 },
{ cat: "Color", val: 32 },
{ cat: "Nails", val: 15 },
{ cat: "Spa", val: 12 },
],
txns: [
{ client: "Eleanor Pryce", stylist: "Aria Vance", svc: "Cut & Style", cat: "Hair", amt: 165 },
{ client: "Sasha Lindqvist", stylist: "Noor Haddad", svc: "Full Balayage", cat: "Color", amt: 285 },
{ client: "Priya Nair", stylist: "Mira Solis", svc: "Gel Manicure", cat: "Nails", amt: 72 },
{ client: "Daniel Voss", stylist: "Theo Brandt", svc: "Skin Fade", cat: "Hair", amt: 58 },
{ client: "Isabel Romero", stylist: "Jun Park", svc: "Aroma Facial", cat: "Spa", amt: 140 },
{ client: "Hana Okafor", stylist: "Léa Moreau", svc: "Gloss & Blowout", cat: "Color", amt: 118 },
],
},
week: {
label: "this week",
crew: "12 stylists · 6-day rota",
kpis: {
revenue: { value: 31640, prev: 29870, fmt: "money" },
bookings: { value: 252, prev: 244, fmt: "int" },
ticket: { value: 125, prev: 122, fmt: "money" },
retail: { value: 21, prev: 22, fmt: "pct" },
},
stylists: [
{ name: "Noor Haddad", role: "Color Director", rev: 7240, svc: 49 },
{ name: "Aria Vance", role: "Master Stylist", rev: 6980, svc: 56 },
{ name: "Léa Moreau", role: "Senior Stylist", rev: 5120, svc: 52 },
{ name: "Mira Solis", role: "Nail Artist", rev: 4380, svc: 71 },
{ name: "Theo Brandt", role: "Stylist", rev: 4010, svc: 44 },
{ name: "Jun Park", role: "Spa Therapist", rev: 3910, svc: 38 },
],
mix: [
{ cat: "Hair", val: 38 },
{ cat: "Color", val: 35 },
{ cat: "Nails", val: 16 },
{ cat: "Spa", val: 11 },
],
txns: [
{ client: "Margot Avery", stylist: "Noor Haddad", svc: "Color Correction", cat: "Color", amt: 340 },
{ client: "Owen Fairlie", stylist: "Theo Brandt", svc: "Cut & Beard", cat: "Hair", amt: 74 },
{ client: "Yuki Tanaka", stylist: "Jun Park", svc: "Hot Stone Massage", cat: "Spa", amt: 175 },
{ client: "Renée Dubois", stylist: "Aria Vance", svc: "Bridal Updo", cat: "Hair", amt: 210 },
{ client: "Carla Esposito", stylist: "Mira Solis", svc: "Luxe Pedicure", cat: "Nails", amt: 95 },
{ client: "Thomas Reed", stylist: "Léa Moreau", svc: "Highlights", cat: "Color", amt: 188 },
],
},
month: {
label: "this month",
crew: "12 stylists · 26 trading days",
kpis: {
revenue: { value: 134920, prev: 128400, fmt: "money" },
bookings: { value: 1086, prev: 1052, fmt: "int" },
ticket: { value: 124, prev: 122, fmt: "money" },
retail: { value: 23, prev: 20, fmt: "pct" },
},
stylists: [
{ name: "Aria Vance", role: "Master Stylist", rev: 29800, svc: 238 },
{ name: "Noor Haddad", role: "Color Director", rev: 28640, svc: 201 },
{ name: "Léa Moreau", role: "Senior Stylist", rev: 21450, svc: 224 },
{ name: "Mira Solis", role: "Nail Artist", rev: 18900, svc: 306 },
{ name: "Theo Brandt", role: "Stylist", rev: 17220, svc: 189 },
{ name: "Jun Park", role: "Spa Therapist", rev: 16910, svc: 162 },
],
mix: [
{ cat: "Hair", val: 36 },
{ cat: "Color", val: 34 },
{ cat: "Nails", val: 18 },
{ cat: "Spa", val: 12 },
],
txns: [
{ client: "Vivienne Clark", stylist: "Aria Vance", svc: "Signature Package", cat: "Hair", amt: 420 },
{ client: "Marcus Bell", stylist: "Theo Brandt", svc: "Executive Cut", cat: "Hair", amt: 88 },
{ client: "Anouk Vermeer", stylist: "Noor Haddad", svc: "Dimensional Blonde", cat: "Color", amt: 365 },
{ client: "Sofia Marchetti", stylist: "Jun Park", svc: "Spa Day", cat: "Spa", amt: 260 },
{ client: "Greta Lindholm", stylist: "Mira Solis", svc: "Nail Art Set", cat: "Nails", amt: 110 },
{ client: "Idris Quaye", stylist: "Léa Moreau", svc: "Tonal Gloss", cat: "Color", amt: 96 },
],
},
};
/* ---------- Helpers ---------- */
var $ = function (sel, root) {
return (root || document).querySelector(sel);
};
var $$ = function (sel, root) {
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
};
function money(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function fmtVal(v) {
if (v.fmt === "money") return money(v.value);
if (v.fmt === "pct") return v.value + "%";
return v.value.toLocaleString("en-US");
}
function pctDelta(cur, prev) {
if (!prev) return 0;
return ((cur - prev) / prev) * 100;
}
function compactMoney(n) {
if (n >= 1000) return "$" + (n / 1000).toFixed(n >= 10000 ? 0 : 1) + "k";
return "$" + n;
}
var toastTimer;
function toast(msg) {
var el = $("#toast");
el.textContent = msg;
el.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
el.classList.remove("is-on");
}, 2400);
}
/* ---------- Counter animation ---------- */
function animateNumber(el, to, fmt) {
var from = Number(el.getAttribute("data-cur") || 0);
var start = performance.now();
var dur = 650;
function tick(now) {
var t = Math.min(1, (now - start) / dur);
var eased = 1 - Math.pow(1 - t, 3);
var cur = from + (to - from) * eased;
if (fmt === "money") el.textContent = money(cur);
else if (fmt === "pct") el.textContent = Math.round(cur) + "%";
else el.textContent = Math.round(cur).toLocaleString("en-US");
if (t < 1) requestAnimationFrame(tick);
else el.setAttribute("data-cur", String(to));
}
requestAnimationFrame(tick);
}
/* ---------- Renderers ---------- */
function renderKpis(d) {
Object.keys(d.kpis).forEach(function (key) {
var k = d.kpis[key];
var valEl = $('[data-kpi="' + key + '"]');
if (valEl) animateNumber(valEl, k.value, k.fmt);
var deltaEl = $('[data-delta="' + key + '"]');
if (deltaEl) {
var p = pctDelta(k.value, k.prev);
var up = p >= 0;
var cls = up ? "chip--up" : "chip--down";
var arrow = up ? "▲" : "▼";
deltaEl.innerHTML =
'<span class="chip ' +
cls +
'">' +
arrow +
" " +
Math.abs(p).toFixed(1) +
"%</span> vs prior";
}
});
}
function renderBars(d) {
var wrap = $("#bars");
wrap.innerHTML = "";
var sorted = d.stylists.slice().sort(function (a, b) {
return b.rev - a.rev;
});
var max = sorted[0].rev;
var total = 0;
sorted.forEach(function (s) {
total += s.rev;
var row = document.createElement("div");
row.className = "bar-row";
row.innerHTML =
'<span class="bar-row__name" title="' +
s.name +
'">' +
s.name +
"</span>" +
'<span class="bar-track"><span class="bar-fill"></span></span>' +
'<span class="bar-row__val">' +
compactMoney(s.rev) +
"</span>";
wrap.appendChild(row);
var fill = $(".bar-fill", row);
requestAnimationFrame(function () {
fill.style.width = Math.max(6, (s.rev / max) * 100) + "%";
});
});
$("#barTotal").textContent = money(total) + " total";
}
function renderMix(d) {
var donut = $("#donut");
var legend = $("#legend");
legend.innerHTML = "";
var stops = [];
var acc = 0;
var top = d.mix
.slice()
.sort(function (a, b) {
return b.val - a.val;
})[0];
d.mix.forEach(function (m) {
var color = CATS[m.cat];
stops.push(color + " " + acc + "% " + (acc + m.val) + "%");
acc += m.val;
var li = document.createElement("li");
li.innerHTML =
'<span class="dot" style="background:' +
color +
'"></span>' +
'<span class="lg-name">' +
m.cat +
"</span>" +
'<span class="lg-val">' +
m.val +
"%</span>";
legend.appendChild(li);
});
donut.style.background = "conic-gradient(" + stops.join(",") + ")";
$("#donutTop").textContent = top.cat;
}
function renderPerformers(d) {
var list = $("#performers");
list.innerHTML = "";
var sorted = d.stylists.slice().sort(function (a, b) {
return b.rev - a.rev;
});
sorted.forEach(function (s, i) {
var li = document.createElement("li");
li.className = "performer";
var avg = s.svc ? Math.round(s.rev / s.svc) : 0;
li.innerHTML =
'<span class="performer__rank">' +
(i + 1) +
"</span>" +
'<span class="performer__name">' +
s.name +
'<span class="performer__role">' +
s.role +
" · " +
s.svc +
" services</span></span>" +
'<span class="performer__amt">' +
money(s.rev) +
"<small>" +
money(avg) +
" avg</small></span>";
li.tabIndex = 0;
li.setAttribute("role", "button");
li.addEventListener("click", function () {
toast(s.name + " — " + money(s.rev) + " from " + s.svc + " services");
});
li.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
li.click();
}
});
list.appendChild(li);
});
}
function renderTxns(d) {
var body = $("#txnBody");
body.innerHTML = "";
d.txns.forEach(function (t) {
var tr = document.createElement("tr");
tr.innerHTML =
'<td class="client">' +
t.client +
"</td>" +
"<td>" +
t.stylist +
"</td>" +
'<td><span class="svc"><span class="svc-dot" style="background:' +
CATS[t.cat] +
'"></span>' +
t.svc +
"</span></td>" +
'<td class="amt">' +
money(t.amt) +
"</td>";
body.appendChild(tr);
});
$("#txnCount").textContent = d.txns.length + " shown";
}
function render(rangeKey) {
var d = DATA[rangeKey];
if (!d) return;
renderKpis(d);
renderBars(d);
renderMix(d);
renderPerformers(d);
renderTxns(d);
var lbl = $("#rangeLabel");
lbl.innerHTML =
"Showing figures for <strong>" + d.label + "</strong> · " + d.crew;
}
/* ---------- Wire up ---------- */
var currentRange = "today";
$$("#range .seg__btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var range = btn.getAttribute("data-range");
if (range === currentRange) return;
currentRange = range;
$$("#range .seg__btn").forEach(function (b) {
var on = b === btn;
b.classList.toggle("is-active", on);
b.setAttribute("aria-selected", on ? "true" : "false");
});
render(range);
toast("Switched to " + DATA[range].label);
});
});
$("#exportBtn").addEventListener("click", function () {
toast("Exporting " + DATA[currentRange].label + " report as CSV…");
});
render(currentRange);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Owner Dashboard · Maison Lumière Salon</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=Cormorant+Garamond:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="shell">
<!-- ===== Header ===== -->
<header class="topbar">
<div class="brand">
<span class="brand__mark" aria-hidden="true">ML</span>
<span class="brand__text">
<span class="brand__name">Maison Lumière</span>
<span class="brand__sub">Owner Dashboard</span>
</span>
</div>
<div class="topbar__actions">
<div
class="seg"
role="tablist"
aria-label="Reporting period"
id="range"
>
<button class="seg__btn is-active" role="tab" aria-selected="true" data-range="today">Today</button>
<button class="seg__btn" role="tab" aria-selected="false" data-range="week">Week</button>
<button class="seg__btn" role="tab" aria-selected="false" data-range="month">Month</button>
</div>
<button class="btn btn--ghost" id="exportBtn" type="button">
<span aria-hidden="true">↧</span> Export
</button>
</div>
</header>
<main class="main">
<div class="page-head">
<div>
<p class="eyebrow">Performance overview</p>
<h1 class="page-title">Good morning, Camille</h1>
</div>
<p class="page-meta" id="rangeLabel">Showing figures for <strong>today</strong> · 12 stylists on the floor</p>
</div>
<!-- ===== KPI cards ===== -->
<section class="kpis" aria-label="Key performance indicators">
<article class="card kpi">
<p class="kpi__label">Revenue</p>
<p class="kpi__value" data-kpi="revenue">$0</p>
<p class="kpi__delta" data-delta="revenue"><span class="chip chip--up">▲ 0%</span> vs prior</p>
</article>
<article class="card kpi">
<p class="kpi__label">Bookings</p>
<p class="kpi__value" data-kpi="bookings">0</p>
<p class="kpi__delta" data-delta="bookings"><span class="chip chip--up">▲ 0%</span> vs prior</p>
</article>
<article class="card kpi">
<p class="kpi__label">Avg ticket</p>
<p class="kpi__value" data-kpi="ticket">$0</p>
<p class="kpi__delta" data-delta="ticket"><span class="chip chip--up">▲ 0%</span> vs prior</p>
</article>
<article class="card kpi">
<p class="kpi__label">Retail share</p>
<p class="kpi__value" data-kpi="retail">0%</p>
<p class="kpi__delta" data-delta="retail"><span class="chip chip--up">▲ 0%</span> vs prior</p>
</article>
</section>
<!-- ===== Charts row ===== -->
<section class="grid-2">
<article class="card chart-card">
<header class="card__head">
<div>
<h2 class="card__title">Revenue per stylist</h2>
<p class="card__sub">Service + retail, net of discounts</p>
</div>
<span class="hint" id="barTotal">—</span>
</header>
<div class="bars" id="bars" role="img" aria-label="Bar chart of revenue per stylist"></div>
</article>
<article class="card chart-card">
<header class="card__head">
<div>
<h2 class="card__title">Service mix</h2>
<p class="card__sub">Share of revenue by category</p>
</div>
</header>
<div class="mix">
<div class="donut" id="donut" role="img" aria-label="Donut chart of service mix">
<div class="donut__hole">
<span class="donut__num" id="donutTop">—</span>
<span class="donut__cap">top mix</span>
</div>
</div>
<ul class="legend" id="legend"></ul>
</div>
</article>
</section>
<!-- ===== Lower row ===== -->
<section class="grid-2 grid-2--lower">
<article class="card">
<header class="card__head">
<div>
<h2 class="card__title">Top performers</h2>
<p class="card__sub">Ranked by net revenue</p>
</div>
</header>
<ol class="performers" id="performers"></ol>
</article>
<article class="card">
<header class="card__head">
<div>
<h2 class="card__title">Recent transactions</h2>
<p class="card__sub">Last settled tickets</p>
</div>
<span class="hint" id="txnCount">—</span>
</header>
<div class="table-wrap">
<table class="txn">
<thead>
<tr>
<th scope="col">Client</th>
<th scope="col">Stylist</th>
<th scope="col">Service</th>
<th scope="col" class="ta-r">Total</th>
</tr>
</thead>
<tbody id="txnBody"></tbody>
</table>
</div>
</article>
</section>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Revenue per Stylist + Service Mix
The morning view a boutique owner actually opens. A four-card KPI band leads with today’s revenue, bookings, average ticket and retail share — each number rolls up with an eased counter and carries a coloured delta chip measured against the prior period. Beneath it, a pure-CSS bar chart ranks revenue per stylist with animated gold-to-rose fills, while a conic-gradient donut and matching legend break the business down by Hair, Color, Nails and Spa.
A top-performers list ranks the floor by net revenue with per-service averages, and a recent-transactions table threads each settled ticket to its stylist and category via a small colour dot. The whole screen pivots on a single segmented control: switching between Today, Week and Month swaps precomputed datasets and re-renders every card, chart and row in place, with a toast confirming the change.
It is vanilla JS end to end — no chart library, no build step. The charts are CSS gradients, the counters are requestAnimationFrame, and the styling leans on the shared salon tokens: Cormorant Garamond display over Inter body, matte-black ink, cream surfaces and thin gold hairlines. Keyboard-operable rows, AA-contrast text and a dedicated sub-520px layout round it out.