Pages Hard
Admin — Sales & KPI Dashboard
Restaurant manager dashboard — covers, average check, turn time, 7-day revenue SVG line chart, top items list, hourly heatmap, today's reservations preview.
Open in Lab
MCP
html css vanilla-js svg
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
.app {
height: 100vh;
display: grid;
grid-template-columns: 248px 1fr;
}
/* ─── Sidebar ─── */
.rail {
background: var(--forest);
color: var(--bone);
display: flex;
flex-direction: column;
border-right: 1px solid var(--forest-d);
overflow-y: auto;
}
.rail-brand {
padding: 22px 22px 18px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid rgba(250, 247, 241, 0.1);
}
.brand-mark {
width: 36px;
height: 36px;
background: var(--gold);
color: var(--ink);
font-family: var(--font-display);
font-weight: 800;
font-size: 0.92rem;
border-radius: 8px;
display: grid;
place-items: center;
letter-spacing: 0.04em;
}
.brand-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
}
.rail-nav {
flex: 1;
display: flex;
flex-direction: column;
padding: 12px 12px 18px;
gap: 2px;
}
.r-section {
margin: 16px 14px 6px;
font-size: 0.66rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--gold-light);
font-weight: 700;
}
.r-link {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: 8px;
text-decoration: none;
color: rgba(250, 247, 241, 0.78);
font-size: 0.92rem;
font-weight: 500;
transition: background 0.15s, color 0.15s;
}
.r-link:hover {
background: rgba(250, 247, 241, 0.06);
color: var(--bone);
}
.r-link.is-active {
background: var(--bone);
color: var(--forest-d);
font-weight: 700;
}
.r-link.is-active .r-icon {
filter: none;
}
.r-icon {
font-size: 1rem;
width: 22px;
text-align: center;
}
.r-badge {
margin-left: auto;
background: var(--gold);
color: var(--ink);
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
min-width: 24px;
text-align: center;
}
.r-badge-warn {
background: var(--warning);
color: var(--ink);
}
.r-link-quiet {
color: rgba(250, 247, 241, 0.55);
font-size: 0.84rem;
}
.r-dot {
margin-left: auto;
width: 8px;
height: 8px;
background: #6ec78a;
border-radius: 999px;
box-shadow: 0 0 0 4px rgba(110, 199, 138, 0.18);
}
.rail-foot {
padding: 14px 16px;
border-top: 1px solid rgba(250, 247, 241, 0.1);
}
.rail-user {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 38px;
height: 38px;
background: var(--gold);
color: var(--ink);
border-radius: 999px;
display: grid;
place-items: center;
font-family: var(--font-display);
font-weight: 800;
}
.user-name {
font-size: 0.92rem;
font-weight: 700;
color: var(--bone);
}
.user-role {
font-size: 0.74rem;
color: rgba(250, 247, 241, 0.5);
}
/* ─── Main ─── */
.main {
overflow-y: auto;
padding: 28px 36px 56px;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 14px;
}
.kicker {
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.top h1 {
font-family: var(--font-display);
font-weight: 800;
font-size: 2rem;
letter-spacing: -0.015em;
}
.top-tools {
display: flex;
align-items: center;
gap: 8px;
}
.seg {
display: inline-flex;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
padding: 3px;
border-radius: 999px;
}
.seg-btn {
background: transparent;
border: none;
color: var(--ink-2);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
padding: 7px 14px;
border-radius: 999px;
cursor: pointer;
}
.seg-btn.is-active {
background: var(--forest);
color: var(--bone);
}
.ghost,
.primary {
border-radius: 999px;
padding: 9px 16px;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
cursor: pointer;
border: 1px solid transparent;
}
.ghost {
background: var(--bone);
border-color: rgba(44, 26, 14, 0.12);
color: var(--ink-2);
}
.ghost:hover {
background: var(--cream-2);
color: var(--ink);
}
.primary {
background: var(--forest);
color: var(--bone);
}
.primary:hover {
background: var(--forest-d);
}
/* KPI grid */
.kpi-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
margin-bottom: 24px;
}
@media (max-width: 1180px) {
.kpi-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 720px) {
.kpi-grid {
grid-template-columns: 1fr 1fr;
}
}
.kpi {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-md);
padding: 16px 18px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.kpi-feat {
background: linear-gradient(180deg, var(--forest) 0%, var(--forest-d) 100%);
color: var(--bone);
border-color: var(--forest-d);
}
.kpi-label {
font-size: 0.74rem;
letter-spacing: 0.06em;
color: var(--warm-gray);
font-weight: 600;
}
.kpi-feat .kpi-label {
color: var(--gold-light);
}
.kpi-value {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.7rem;
letter-spacing: -0.01em;
}
.kpi-sub {
font-size: 0.92rem;
color: var(--warm-gray);
font-weight: 600;
}
.kpi-feat .kpi-sub {
color: rgba(250, 247, 241, 0.6);
}
.kpi-delta {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.04em;
color: var(--warm-gray);
}
.is-up {
color: var(--success);
}
.is-down {
color: var(--danger);
}
.is-flat {
color: var(--warm-gray);
}
.kpi-feat .kpi-delta {
color: var(--gold-light);
}
/* Rows */
.row {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 980px) {
.row {
grid-template-columns: 1fr;
}
}
.card {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: var(--r-lg);
padding: 20px 22px 18px;
display: flex;
flex-direction: column;
gap: 14px;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
}
.card-kicker {
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.card h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.4rem;
letter-spacing: -0.005em;
margin-top: 2px;
}
/* Chart */
.legend {
font-size: 0.78rem;
color: var(--warm-gray);
display: flex;
align-items: center;
gap: 8px;
}
.legend-pip {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 999px;
margin-right: 4px;
margin-left: 12px;
}
.legend-pip:first-of-type {
margin-left: 0;
}
.legend-this {
background: var(--terracotta);
}
.legend-prev {
background: rgba(44, 26, 14, 0.22);
}
.chart {
width: 100%;
height: auto;
display: block;
}
.grid line {
stroke: rgba(44, 26, 14, 0.08);
stroke-width: 1;
stroke-dasharray: 2 4;
}
.y-labels,
.x-labels {
font-family: var(--font-mono);
font-size: 10px;
fill: var(--warm-gray);
font-weight: 600;
}
.line {
fill: none;
stroke-width: 2.5;
stroke-linejoin: round;
stroke-linecap: round;
}
.line-curr {
stroke: var(--terracotta);
}
.line-prev {
stroke: rgba(44, 26, 14, 0.22);
stroke-dasharray: 4 4;
stroke-width: 2;
}
.area {
fill: rgba(193, 113, 74, 0.12);
stroke: none;
}
.dot {
fill: var(--terracotta);
stroke: var(--bone);
stroke-width: 2;
cursor: pointer;
}
/* Top items */
.ti-list {
list-style: none;
display: flex;
flex-direction: column;
}
.ti-list li {
display: grid;
grid-template-columns: 20px 1fr auto;
gap: 10px;
align-items: baseline;
padding: 10px 0;
border-bottom: 1px dashed rgba(44, 26, 14, 0.1);
font-size: 0.92rem;
}
.ti-list li:last-child {
border-bottom: none;
}
.ti-no {
font-family: var(--font-mono);
color: var(--warm-gray);
font-weight: 700;
font-size: 0.78rem;
}
.ti-name {
font-weight: 600;
}
.ti-name small {
display: block;
color: var(--warm-gray);
font-weight: 500;
font-size: 0.74rem;
margin-top: 2px;
}
.ti-amt {
font-family: var(--font-mono);
font-weight: 700;
color: var(--terracotta-d);
}
/* Heatmap */
.heatmap {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
gap: 4px;
}
.h-label {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--warm-gray);
display: grid;
place-items: center;
font-weight: 600;
}
.h-cell {
aspect-ratio: 1.6 / 1;
border-radius: 4px;
background: var(--cream-2);
}
.h-cell[data-h="0"] {
background: var(--cream-2);
}
.h-cell[data-h="1"] {
background: #e7d4b3;
}
.h-cell[data-h="2"] {
background: #d4a87b;
}
.h-cell[data-h="3"] {
background: var(--terracotta);
}
.h-cell[data-h="4"] {
background: var(--terracotta-d);
}
.h-cell[data-h="5"] {
background: var(--forest);
}
.heat-legend {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 12px;
font-size: 0.74rem;
color: var(--warm-gray);
font-weight: 600;
}
.h-scale {
display: inline-flex;
gap: 2px;
}
.h-s {
width: 18px;
height: 12px;
border-radius: 3px;
}
.s-1 {
background: var(--cream-2);
}
.s-2 {
background: #e7d4b3;
}
.s-3 {
background: #d4a87b;
}
.s-4 {
background: var(--terracotta);
}
.s-5 {
background: var(--forest);
}
/* Reservation preview */
.res-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.res-row {
display: grid;
grid-template-columns: 56px 1fr auto;
align-items: center;
gap: 12px;
padding: 10px 14px;
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 10px;
background: var(--cream);
}
.res-time {
font-family: var(--font-mono);
font-weight: 700;
color: var(--ink);
}
.res-name {
font-weight: 700;
font-size: 0.94rem;
}
.res-meta {
font-size: 0.76rem;
color: var(--warm-gray);
margin-top: 2px;
}
.res-tag {
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
}
.res-tag[data-t="vip"] {
background: var(--gold);
color: var(--ink);
}
.res-tag[data-t="party"] {
background: var(--terracotta);
color: var(--bone);
}
.res-tag[data-t="window"] {
background: var(--success);
color: var(--bone);
}
.card-cta {
margin-top: 2px;
text-decoration: none;
color: var(--terracotta-d);
font-weight: 700;
font-size: 0.86rem;
letter-spacing: 0.02em;
align-self: flex-start;
}
.card-cta:hover {
color: var(--ink);
}
/* ─── Section switching ─── */
.section[hidden] {
display: none;
}
/* ─── Section toolbar ─── */
.sec-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.sec-count {
font-size: 0.84rem;
color: var(--warm-gray);
font-weight: 600;
}
/* ─── Data table ─── */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.data-table th {
text-align: left;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--warm-gray);
font-weight: 700;
padding: 0 12px 12px 0;
border-bottom: 1px solid rgba(44, 26, 14, 0.1);
}
.data-table td {
padding: 12px 12px 12px 0;
border-bottom: 1px dashed rgba(44, 26, 14, 0.08);
vertical-align: middle;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table .mono {
font-family: var(--font-mono);
font-weight: 600;
}
.td-name {
font-weight: 700;
color: var(--ink);
}
.td-sub {
font-size: 0.74rem;
color: var(--warm-gray);
margin-top: 2px;
}
/* ─── Status chips ─── */
.chip {
display: inline-block;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
}
.chip-confirmed {
background: rgba(79, 122, 58, 0.14);
color: var(--success);
}
.chip-pending {
background: rgba(217, 144, 32, 0.14);
color: var(--warning);
}
.chip-waitlist {
background: rgba(44, 26, 14, 0.08);
color: var(--ink-2);
}
.chip-eightysixed {
background: rgba(179, 67, 42, 0.12);
color: var(--danger);
}
.chip-off {
background: rgba(44, 26, 14, 0.06);
color: var(--warm-gray);
}
/* ─── Ghost small button ─── */
.btn-ghost-sm {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.15);
border-radius: 6px;
padding: 5px 10px;
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
color: var(--ink-2);
cursor: pointer;
}
.btn-ghost-sm:hover {
background: var(--cream-2);
}
/* ─── Menu section layout ─── */
.menu-layout {
display: grid;
grid-template-columns: 200px 1fr;
gap: 16px;
}
@media (max-width: 760px) {
.menu-layout {
grid-template-columns: 1fr;
}
}
.menu-cat-head {
font-size: 0.7rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
margin-bottom: 14px;
}
.cat-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 2px;
}
.cat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 9px 12px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--ink-2);
cursor: pointer;
}
.cat-item:hover {
background: var(--cream);
}
.cat-item.is-active {
background: var(--forest);
color: var(--bone);
font-weight: 700;
}
.cat-count {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 700;
opacity: 0.6;
}
/* ── Responsive ── */
@media (max-width: 880px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.rail {
flex-direction: row;
overflow-x: auto;
padding: 8px 12px;
border-right: none;
border-bottom: 1px solid var(--forest-d);
}
.rail-brand,
.rail-foot,
.r-section {
display: none;
}
.rail-nav {
flex-direction: row;
padding: 0;
gap: 6px;
}
}
/* ─── Service section ─── */
.service-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
@media (max-width: 900px) { .service-grid { grid-template-columns: repeat(2, 1fr); } }
.svc-stat { padding: 16px 20px; }
.svc-label { font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--warm-gray); font-weight: 700; margin-bottom: 6px; }
.svc-value { font-size: 1.8rem; font-weight: 700; font-family: var(--font-display); color: var(--ink); line-height: 1; }
.svc-sub { font-size: 1rem; color: var(--warm-gray); font-weight: 400; }
.svc-floor { display: flex; flex-wrap: wrap; gap: 10px; padding: 4px 0; }
.svc-table { display: flex; flex-direction: column; align-items: center; gap: 4px; background: var(--cream-d, #f0ebe0); border-radius: 8px; padding: 12px 16px; min-width: 72px; }
.svc-tname { font-weight: 700; font-size: 0.92rem; }
.svc-status { font-size: 0.72rem; color: var(--forest); font-weight: 600; background: rgba(52,95,64,0.12); border-radius: 4px; padding: 2px 6px; }
.btn-danger { background: #c0392b; color: #fff; border: none; border-radius: 8px; padding: 8px 16px; font-weight: 600; font-size: 0.88rem; cursor: pointer; }
.btn-danger:hover { background: #a93226; }
/* ─── Alerts section ─── */
.alerts-list { list-style: none; display: flex; flex-direction: column; gap: 12px; }
.alert-item { display: flex; align-items: flex-start; gap: 14px; background: #fff; border-radius: 10px; padding: 16px 18px; border: 1px solid rgba(44,26,14,0.08); box-shadow: 0 1px 4px rgba(0,0,0,0.04); }
.alert-warn { border-left: 4px solid #e67e22; }
.alert-info { border-left: 4px solid #2980b9; }
.alert-icon { font-size: 1.4rem; flex-shrink: 0; margin-top: 2px; }
.alert-body { flex: 1; }
.alert-title { font-weight: 700; font-size: 0.94rem; margin-bottom: 4px; }
.alert-meta { font-size: 0.8rem; color: var(--warm-gray); }
.alert-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.btn-sm { padding: 5px 12px; font-size: 0.8rem; border-radius: 6px; font-weight: 600; cursor: pointer; white-space: nowrap; }
.btn-primary.btn-sm { background: var(--forest); color: #fff; border: none; }
.btn-primary.btn-sm:hover { background: var(--forest-d); }
.btn-ghost.btn-sm { background: transparent; border: 1px solid rgba(44,26,14,0.2); color: var(--ink); }
.btn-ghost.btn-sm:hover { background: rgba(44,26,14,0.05); }
.alerts-empty { color: var(--warm-gray); font-size: 0.92rem; padding: 24px 0; }// ─── Period data sets ───
const PERIODS = {
today: {
label: "Today",
chartKicker: "Revenue · today by hour",
days: ["12h", "13h", "14h", "15h", "16h", "17h", "18h", "19h", "20h", "21h", "22h", "23h"],
curr: [480, 620, 710, 540, 390, 680, 1240, 1980, 2340, 1860, 1420, 640],
prev: [410, 580, 660, 500, 360, 620, 1140, 1820, 2180, 1720, 1310, 590],
kCoversLabel: "Covers · today",
kCovers: "68",
kCoversDelta: "▲ 4.6% vs yesterday",
kCoversDeltaClass: "is-up",
kRevenue: "$5,820",
kRevenueDelta: "▲ 6.1%",
kRevenueDeltaClass: "is-up",
kAvg: "$85",
kAvgDelta: "▲ 1.4%",
kAvgDeltaClass: "is-up",
kTurn: "1h 38m",
kTurnDelta: "▲ 8 min faster",
kTurnDeltaClass: "is-up",
kCancel: "1",
kCancelDelta: "▼ 1 fewer than avg",
kCancelDeltaClass: "is-up",
kTonight: '38 <span class="kpi-sub">/ 42 seats</span>',
},
week: {
label: "This week",
chartKicker: "Revenue · last 7 days",
days: ["Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"],
curr: [3200, 3450, 3980, 5320, 6850, 5840, 3540],
prev: [2900, 3210, 3650, 4920, 6120, 5430, 3380],
kCoversLabel: "Covers · this week",
kCovers: "412",
kCoversDelta: "▲ 8.2% vs last week",
kCoversDeltaClass: "is-up",
kRevenue: "$32,184",
kRevenueDelta: "▲ 11.4% · target met",
kRevenueDeltaClass: "is-up",
kAvg: "$78",
kAvgDelta: "▼ 2.8%",
kAvgDeltaClass: "is-down",
kTurn: "1h 42m",
kTurnDelta: "▲ 6 min faster",
kTurnDeltaClass: "is-up",
kCancel: "4",
kCancelDelta: "— same as last week",
kCancelDeltaClass: "is-flat",
kTonight: '38 <span class="kpi-sub">/ 42 seats</span>',
},
month: {
label: "This month",
chartKicker: "Revenue · last 4 weeks",
days: ["Wk 1", "Wk 2", "Wk 3", "Wk 4"],
curr: [28400, 31200, 34800, 32184],
prev: [25600, 29800, 32100, 30560],
kCoversLabel: "Covers · this month",
kCovers: "1,648",
kCoversDelta: "▲ 9.4% vs last month",
kCoversDeltaClass: "is-up",
kRevenue: "$126,584",
kRevenueDelta: "▲ 13.2% · best month",
kRevenueDeltaClass: "is-up",
kAvg: "$76",
kAvgDelta: "▼ 1.4%",
kAvgDeltaClass: "is-down",
kTurn: "1h 45m",
kTurnDelta: "▲ 4 min faster",
kTurnDeltaClass: "is-up",
kCancel: "17",
kCancelDelta: "▲ 3 more than last month",
kCancelDeltaClass: "is-down",
kTonight: '38 <span class="kpi-sub">/ 42 seats</span>',
},
};
// ─── Chart rendering ───
const W = 600;
const H = 220;
const PX = 40;
const PY = 30;
const innerW = W - PX - 20;
const innerH = H - PY - 30;
const maxY = 8000;
function px(i, n) {
return PX + (i / (n - 1)) * innerW;
}
function py(v) {
return PY + innerH - (v / maxY) * innerH;
}
function pathFor(arr) {
return arr.map((v, i) => `${i === 0 ? "M" : "L"}${px(i, arr.length)} ${py(v)}`).join(" ");
}
function areaFor(arr) {
const top = pathFor(arr);
const last = arr.length - 1;
return `${top} L ${px(last, arr.length)} ${py(0)} L ${px(0, arr.length)} ${py(0)} Z`;
}
function drawChart(data) {
document.getElementById("currLine").setAttribute("d", pathFor(data.curr));
document.getElementById("prevLine").setAttribute("d", pathFor(data.prev));
document.getElementById("currArea").setAttribute("d", areaFor(data.curr));
document.getElementById("xLabels").innerHTML = data.days
.map(
(d, i) =>
`<text x="${px(i, data.days.length)}" y="${H - 8}" text-anchor="middle">${d}</text>`
)
.join("");
document.getElementById("dots").innerHTML = data.curr
.map(
(v, i) =>
`<circle class="dot" cx="${px(i, data.curr.length)}" cy="${py(v)}" r="4">
<title>${data.days[i]} · $${v.toLocaleString()}</title>
</circle>`
)
.join("");
document.getElementById("chartKicker").textContent = data.chartKicker;
}
function updateKPIs(data) {
const kCoversLabel = document.getElementById("kCoversLabel");
const kCovers = document.getElementById("kCovers");
const kCoversDelta = document.getElementById("kCoversDelta");
const kRevenue = document.getElementById("kRevenue");
const kRevenueDelta = document.getElementById("kRevenueDelta");
const kAvg = document.getElementById("kAvg");
const kAvgDelta = document.getElementById("kAvgDelta");
const kTurn = document.getElementById("kTurn");
const kTurnDelta = document.getElementById("kTurnDelta");
const kCancel = document.getElementById("kCancel");
const kCancelDelta = document.getElementById("kCancelDelta");
const kTonight = document.getElementById("kTonight");
if (kCoversLabel) kCoversLabel.textContent = data.kCoversLabel;
if (kCovers) kCovers.textContent = data.kCovers;
if (kCoversDelta) {
kCoversDelta.textContent = data.kCoversDelta;
kCoversDelta.className = "kpi-delta " + data.kCoversDeltaClass;
}
if (kRevenue) kRevenue.textContent = data.kRevenue;
if (kRevenueDelta) {
kRevenueDelta.textContent = data.kRevenueDelta;
kRevenueDelta.className = "kpi-delta " + data.kRevenueDeltaClass;
}
if (kAvg) kAvg.textContent = data.kAvg;
if (kAvgDelta) {
kAvgDelta.textContent = data.kAvgDelta;
kAvgDelta.className = "kpi-delta " + data.kAvgDeltaClass;
}
if (kTurn) kTurn.textContent = data.kTurn;
if (kTurnDelta) {
kTurnDelta.textContent = data.kTurnDelta;
kTurnDelta.className = "kpi-delta " + data.kTurnDeltaClass;
}
if (kCancel) kCancel.textContent = data.kCancel;
if (kCancelDelta) {
kCancelDelta.textContent = data.kCancelDelta;
kCancelDelta.className = "kpi-delta " + data.kCancelDeltaClass;
}
if (kTonight) kTonight.innerHTML = data.kTonight;
}
// ─── Period selector ───
let activePeriod = "week";
function applyPeriod(period) {
activePeriod = period;
const data = PERIODS[period];
drawChart(data);
updateKPIs(data);
document.querySelectorAll(".seg-btn").forEach((btn) => {
btn.classList.toggle("is-active", btn.dataset.period === period);
});
}
document.querySelectorAll(".seg-btn[data-period]").forEach((btn) => {
btn.addEventListener("click", () => applyPeriod(btn.dataset.period));
});
// Initial render
applyPeriod("week");
// ─── Top items ───
const TOP = [
{ name: "Ribeye 14oz", sub: "Mains · dry-aged 28 d", amt: 4264 },
{ name: "Burrata huerta", sub: "Apps", amt: 2880 },
{ name: "Pappardelle ragú", sub: "Pasta", amt: 2496 },
{ name: "Branzino entero", sub: "Mains · whole sea bass", amt: 2280 },
{ name: "Tarta de queso", sub: "Dessert · burnt cheesecake", amt: 1958 },
{ name: "Risotto hongos", sub: "Pasta · vegetarian", amt: 1768 },
{ name: "Pulpo brasa", sub: "Apps", amt: 1672 },
{ name: "Negroni sbagliato", sub: "Drinks", amt: 1568 },
];
document.getElementById("topItems").innerHTML = TOP.map(
(t, i) => `<li>
<span class="ti-no">${String(i + 1).padStart(2, "0")}</span>
<span class="ti-name">${t.name}<small>${t.sub}</small></span>
<span class="ti-amt">$${t.amt.toLocaleString()}</span>
</li>`
).join("");
// ─── Heatmap (rows = days, cols = service hours) ───
const HEAT_DAYS = ["Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"];
const HEAT_HOURS = ["18", "19", "20", "21", "22"];
// values 0..5 (low → high)
const HEAT = [
[1, 3, 4, 3, 1],
[1, 3, 4, 3, 1],
[2, 4, 5, 4, 2],
[2, 4, 5, 5, 3],
[3, 5, 5, 5, 3],
[2, 4, 4, 2, 0],
[0, 0, 0, 0, 0],
];
const heatmap = document.getElementById("heatmap");
let html = `<span class="h-label"></span>`;
HEAT_HOURS.forEach((h) => (html += `<span class="h-label">${h}h</span>`));
HEAT_DAYS.forEach((day, r) => {
html += `<span class="h-label">${day}</span>`;
HEAT[r].forEach((v) => {
html += `<span class="h-cell" data-h="${v}" title="${day} ${HEAT_HOURS[0]}h · ${v}/5"></span>`;
});
});
heatmap.innerHTML = html;
heatmap.style.gridTemplateColumns = `60px repeat(${HEAT_HOURS.length}, 1fr)`;
// ─── Reservation preview ───
const RES = [
{ time: "19:00", name: "Reyes", meta: "×2 · table 4", tag: "Window", t: "window" },
{
time: "19:15",
name: "García-Tan",
meta: "×4 · table 11 · birthday",
tag: "Birthday",
t: "party",
},
{ time: "19:30", name: "Khoury", meta: "×2 · bar · regular", tag: "VIP", t: "vip" },
{ time: "20:00", name: "Marquez", meta: "×2 · table 3", tag: "—", t: "" },
{ time: "20:15", name: "Loredo", meta: "×4 · table 5", tag: "—", t: "" },
{
time: "20:30",
name: "Mendoza",
meta: "×6 · long table · catering enquiry",
tag: "Large",
t: "party",
},
{ time: "20:45", name: "Tanaka", meta: "×3 · table 12", tag: "—", t: "" },
{ time: "21:00", name: "Singh", meta: "×2 · patio", tag: "—", t: "" },
];
document.getElementById("resList").innerHTML = RES.map(
(r) => `<li class="res-row">
<span class="res-time">${r.time}</span>
<span>
<p class="res-name">${r.name}</p>
<p class="res-meta">${r.meta}</p>
</span>
${r.t ? `<span class="res-tag" data-t="${r.t}">${r.tag}</span>` : ""}
</li>`
).join("");
// ─── Section switching ───
const SECTION_TITLES = {
dashboard: "Dashboard",
reservations: "Reservations",
menu: "Menu",
inventory: "Inventory",
staff: "Staff",
service: "Tonight · Service",
alerts: "Alerts",
};
function switchSection(sectionId) {
// Toggle sections
document.querySelectorAll(".section").forEach((sec) => {
sec.hidden = sec.id !== "s-" + sectionId;
});
// Update nav active state
document.querySelectorAll(".r-link[data-section]").forEach((link) => {
link.classList.toggle("is-active", link.dataset.section === sectionId);
});
// Update page title
const title = document.getElementById("pageTitle");
if (title) title.textContent = SECTION_TITLES[sectionId] || sectionId;
// Show/hide period selector (only on dashboard)
const seg = document.querySelector(".seg");
if (seg) seg.style.visibility = sectionId === "dashboard" ? "" : "hidden";
}
document.querySelectorAll(".r-link[data-section]").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const sec = link.dataset.section;
if (sec === "service") renderServiceFloor();
switchSection(sec);
});
});
// ─── Service section ───
let serviceOpen = true;
const TABLE_NAMES = ["T1", "T2", "T4", "T5", "T7", "T9", "T11"];
function renderServiceFloor() {
const floor = document.getElementById("svcFloor");
if (!floor) return;
floor.innerHTML = TABLE_NAMES.map(
(t) => `<div class="svc-table"><span class="svc-tname">${t}</span><span class="svc-status">Active</span></div>`
).join("");
}
document.getElementById("closeServiceBtn")?.addEventListener("click", () => {
serviceOpen = false;
const dot = document.getElementById("serviceDot");
if (dot) { dot.style.background = "var(--warm-gray, #999)"; }
const label = document.getElementById("serviceStatusLabel");
if (label) label.textContent = "Service closed";
const btn = document.getElementById("closeServiceBtn");
if (btn) { btn.textContent = "Reopen service"; btn.classList.replace("btn-danger", "btn-primary"); }
btn?.addEventListener("click", () => {
serviceOpen = true;
if (dot) dot.style.background = "";
if (label) label.textContent = "Service open since 19:00";
btn.textContent = "Close service";
btn.classList.replace("btn-primary", "btn-danger");
}, { once: true });
});
// ─── Alerts section ───
let alerts = ["pasta", "noshow"];
function updateAlertsCount() {
const countEl = document.getElementById("alertsCount");
const label = document.getElementById("alertsLabel");
const empty = document.getElementById("alertsEmpty");
const count = alerts.length;
if (countEl) countEl.textContent = count === 0 ? "No active alerts" : `${count} active alert${count > 1 ? "s" : ""}`;
if (label) label.textContent = count === 0 ? "No alerts" : `${count} alert${count > 1 ? "s" : ""}`;
if (empty) empty.hidden = count > 0;
}
document.getElementById("alertsList")?.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const alertId = btn.dataset.alert;
const action = btn.dataset.action;
const li = document.querySelector(`[data-alert="${alertId}"]`);
if (action === "reorder" && alertId === "pasta") {
li.querySelector(".alert-meta").textContent = "Reordered · arriving tomorrow";
li.querySelector("[data-action='reorder']").disabled = true;
li.querySelector("[data-action='reorder']").textContent = "Ordered ✓";
} else if (action === "release" && alertId === "noshow") {
li.querySelector(".alert-meta").textContent = "Table 4 released and made available";
btn.disabled = true;
btn.textContent = "Released ✓";
} else if (action === "dismiss") {
li.remove();
alerts = alerts.filter((a) => a !== alertId);
updateAlertsCount();
}
});
// Also wire the "Open reservation manager" card-cta link
document.querySelectorAll(".card-cta[data-section]").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
switchSection(link.dataset.section);
});
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Dashboard · Casa Olivar Admin</title>
</head>
<body>
<div class="app">
<!-- ─── Sidebar ─── -->
<aside class="rail">
<header class="rail-brand">
<span class="brand-mark">CO</span>
<span class="brand-name">Casa Olivar</span>
</header>
<nav class="rail-nav" aria-label="Admin sections">
<a class="r-link is-active" href="#" data-section="dashboard">
<span class="r-icon">📊</span><span>Dashboard</span>
</a>
<a class="r-link" href="#" data-section="reservations">
<span class="r-icon">📅</span><span>Reservations</span>
<span class="r-badge">14</span>
</a>
<a class="r-link" href="#" data-section="menu">
<span class="r-icon">🍽</span><span>Menu</span>
</a>
<a class="r-link" href="#" data-section="inventory">
<span class="r-icon">📦</span><span>Inventory</span>
<span class="r-badge r-badge-warn">3</span>
</a>
<a class="r-link" href="#" data-section="staff">
<span class="r-icon">👥</span><span>Staff</span>
</a>
<p class="r-section">Tonight</p>
<a class="r-link r-link-quiet" href="#" data-section="service">
<span class="r-icon">🔥</span><span>Service open</span>
<span class="r-dot" id="serviceDot"></span>
</a>
<a class="r-link r-link-quiet" href="#" data-section="alerts">
<span class="r-icon">🔔</span><span id="alertsLabel">2 alerts</span>
</a>
</nav>
<footer class="rail-foot">
<div class="rail-user">
<span class="user-avatar">M</span>
<div>
<p class="user-name">Marco Reyes</p>
<p class="user-role">Floor manager · Tue</p>
</div>
</div>
</footer>
</aside>
<!-- ─── Main ─── -->
<main class="main">
<header class="top">
<div>
<p class="kicker">Tuesday · 12 May</p>
<h1 id="pageTitle">Dashboard</h1>
</div>
<div class="top-tools">
<div class="seg" role="tablist" aria-label="Period">
<button class="seg-btn" type="button" data-period="today">Today</button>
<button class="seg-btn is-active" type="button" data-period="week">This week</button>
<button class="seg-btn" type="button" data-period="month">This month</button>
</div>
<button class="ghost" type="button">⤓ Export</button>
<button class="primary" type="button">+ New entry</button>
</div>
</header>
<!-- ─── Dashboard section ─── -->
<section class="section" id="s-dashboard">
<!-- KPI grid -->
<section class="kpi-grid">
<article class="kpi">
<p class="kpi-label" id="kCoversLabel">Covers · this week</p>
<p class="kpi-value" id="kCovers">412</p>
<p class="kpi-delta is-up" id="kCoversDelta">▲ 8.2% vs last week</p>
</article>
<article class="kpi">
<p class="kpi-label">Revenue</p>
<p class="kpi-value" id="kRevenue">$32,184</p>
<p class="kpi-delta is-up" id="kRevenueDelta">▲ 11.4% · target met</p>
</article>
<article class="kpi">
<p class="kpi-label">Avg check</p>
<p class="kpi-value" id="kAvg">$78</p>
<p class="kpi-delta is-down" id="kAvgDelta">▼ 2.8%</p>
</article>
<article class="kpi">
<p class="kpi-label">Avg turn time</p>
<p class="kpi-value" id="kTurn">1h 42m</p>
<p class="kpi-delta is-up" id="kTurnDelta">▲ 6 min faster</p>
</article>
<article class="kpi">
<p class="kpi-label">Cancellations</p>
<p class="kpi-value" id="kCancel">4</p>
<p class="kpi-delta is-flat" id="kCancelDelta">— same as last week</p>
</article>
<article class="kpi kpi-feat">
<p class="kpi-label">Tonight's bookings</p>
<p class="kpi-value" id="kTonight">38 <span class="kpi-sub">/ 42 seats</span></p>
<p class="kpi-delta">2 large parties · 1 birthday</p>
</article>
</section>
<!-- Chart + top items -->
<section class="row">
<article class="card chart-card">
<header class="card-head">
<div>
<p class="card-kicker" id="chartKicker">Revenue · last 7 days</p>
<h2>Trending up.</h2>
</div>
<div class="legend">
<span class="legend-pip legend-this"></span> This week
<span class="legend-pip legend-prev"></span> Last week
</div>
</header>
<svg viewBox="0 0 600 220" class="chart" id="chart">
<!-- Grid -->
<g class="grid">
<line x1="40" y1="40" x2="580" y2="40" />
<line x1="40" y1="90" x2="580" y2="90" />
<line x1="40" y1="140" x2="580" y2="140" />
<line x1="40" y1="190" x2="580" y2="190" />
</g>
<!-- Y axis labels -->
<g class="y-labels">
<text x="32" y="44" text-anchor="end">$8k</text>
<text x="32" y="94" text-anchor="end">$6k</text>
<text x="32" y="144" text-anchor="end">$4k</text>
<text x="32" y="194" text-anchor="end">$2k</text>
</g>
<!-- X labels -->
<g class="x-labels" id="xLabels"></g>
<!-- Previous week (faint) -->
<path id="prevLine" class="line line-prev" />
<!-- This week -->
<path id="currLine" class="line line-curr" />
<!-- Area fill -->
<path id="currArea" class="area" />
<!-- Hover dots -->
<g id="dots"></g>
</svg>
</article>
<article class="card top-items">
<header class="card-head">
<p class="card-kicker">Top dishes · this week</p>
<h2>Best sellers.</h2>
</header>
<ul class="ti-list" id="topItems"></ul>
</article>
</section>
<!-- Heatmap + reservations preview -->
<section class="row">
<article class="card heatmap-card">
<header class="card-head">
<p class="card-kicker">Occupancy · hourly · last 14 days</p>
<h2>When the room fills.</h2>
</header>
<div class="heatmap" id="heatmap"></div>
<p class="heat-legend">
<span>0%</span>
<span class="h-scale">
<span class="h-s s-1"></span>
<span class="h-s s-2"></span>
<span class="h-s s-3"></span>
<span class="h-s s-4"></span>
<span class="h-s s-5"></span>
</span>
<span>100%</span>
</p>
</article>
<article class="card res-card">
<header class="card-head">
<p class="card-kicker">Tonight · top bookings</p>
<h2>14 reservations.</h2>
</header>
<ul class="res-list" id="resList"></ul>
<a class="card-cta" href="#" data-section="reservations">Open reservation manager →</a>
</article>
</section>
</section>
<!-- ─── Reservations section ─── -->
<section class="section" id="s-reservations" hidden>
<div class="sec-toolbar">
<p class="sec-count">14 bookings tonight</p>
<button class="primary" type="button">+ New reservation</button>
</div>
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>Time</th>
<th>Name</th>
<th>Guests</th>
<th>Table</th>
<th>Notes</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="mono">19:00</td>
<td class="td-name">Reyes</td>
<td>2</td>
<td>T-04 · window</td>
<td>Regular</td>
<td><span class="chip chip-confirmed">Confirmed</span></td>
</tr>
<tr>
<td class="mono">19:15</td>
<td class="td-name">García-Tan</td>
<td>4</td>
<td>T-11</td>
<td>Birthday cake</td>
<td><span class="chip chip-confirmed">Confirmed</span></td>
</tr>
<tr>
<td class="mono">19:30</td>
<td class="td-name">Khoury</td>
<td>2</td>
<td>Bar</td>
<td>VIP · regular</td>
<td><span class="chip chip-confirmed">Confirmed</span></td>
</tr>
<tr>
<td class="mono">20:00</td>
<td class="td-name">Marquez</td>
<td>2</td>
<td>T-03</td>
<td>—</td>
<td><span class="chip chip-pending">Pending</span></td>
</tr>
<tr>
<td class="mono">20:30</td>
<td class="td-name">Mendoza</td>
<td>6</td>
<td>Long table</td>
<td>Catering enquiry</td>
<td><span class="chip chip-confirmed">Confirmed</span></td>
</tr>
<tr>
<td class="mono">21:00</td>
<td class="td-name">Singh</td>
<td>2</td>
<td>Patio</td>
<td>Allergy: nuts</td>
<td><span class="chip chip-waitlist">Waitlist</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- ─── Menu section ─── -->
<section class="section" id="s-menu" hidden>
<div class="sec-toolbar">
<p class="sec-count">32 active items</p>
<button class="primary" type="button">+ Add item</button>
</div>
<div class="menu-layout">
<div class="card menu-cats">
<p class="menu-cat-head">Categories</p>
<ul class="cat-list">
<li class="cat-item is-active">Appetizers <span class="cat-count">7</span></li>
<li class="cat-item">Mains <span class="cat-count">11</span></li>
<li class="cat-item">Pasta <span class="cat-count">6</span></li>
<li class="cat-item">Desserts <span class="cat-count">5</span></li>
<li class="cat-item">Drinks <span class="cat-count">14</span></li>
</ul>
</div>
<div class="card menu-items-panel">
<table class="data-table">
<thead>
<tr>
<th>Item</th>
<th>Price</th>
<th>Sold this week</th>
<th>Active</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>
<p class="td-name">Burrata huerta</p>
<p class="td-sub">Heirloom tomatoes, basil oil</p>
</td>
<td class="mono">$18</td>
<td class="mono">64</td>
<td><span class="chip chip-confirmed">On</span></td>
<td><button class="btn-ghost-sm" type="button">Edit</button></td>
</tr>
<tr>
<td>
<p class="td-name">Pulpo brasa</p>
<p class="td-sub">Smoked paprika, patatas</p>
</td>
<td class="mono">$24</td>
<td class="mono">41</td>
<td><span class="chip chip-confirmed">On</span></td>
<td><button class="btn-ghost-sm" type="button">Edit</button></td>
</tr>
<tr>
<td>
<p class="td-name">Croquetas jamón</p>
<p class="td-sub">Ibérico ham béchamel</p>
</td>
<td class="mono">$16</td>
<td class="mono">88</td>
<td><span class="chip chip-confirmed">On</span></td>
<td><button class="btn-ghost-sm" type="button">Edit</button></td>
</tr>
<tr>
<td>
<p class="td-name">Gazpacho verde</p>
<p class="td-sub">Cucumber, tomatillo, jalapeño</p>
</td>
<td class="mono">$14</td>
<td class="mono">0</td>
<td><span class="chip chip-off">Off</span></td>
<td><button class="btn-ghost-sm" type="button">Edit</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- ─── Inventory section ─── -->
<section class="section" id="s-inventory" hidden>
<div class="sec-toolbar">
<p class="sec-count">3 items need attention</p>
<button class="primary" type="button">+ Log delivery</button>
</div>
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>Item</th>
<th>Unit</th>
<th>In stock</th>
<th>Par level</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="td-name">Wagyu ribeye</td>
<td>kg</td>
<td class="mono">14.2</td>
<td class="mono">10.0</td>
<td><span class="chip chip-confirmed">OK</span></td>
</tr>
<tr>
<td class="td-name">Burrata (250 g)</td>
<td>units</td>
<td class="mono">8</td>
<td class="mono">12</td>
<td><span class="chip chip-pending">Low stock</span></td>
</tr>
<tr>
<td class="td-name">Dry pasta — pappardelle</td>
<td>kg</td>
<td class="mono">0</td>
<td class="mono">5.0</td>
<td><span class="chip chip-eightysixed">86'd</span></td>
</tr>
<tr>
<td class="td-name">Ibérico jamón</td>
<td>kg</td>
<td class="mono">3.8</td>
<td class="mono">4.0</td>
<td><span class="chip chip-pending">Low stock</span></td>
</tr>
<tr>
<td class="td-name">Olive oil EVOO 5 L</td>
<td>bottles</td>
<td class="mono">6</td>
<td class="mono">4</td>
<td><span class="chip chip-confirmed">OK</span></td>
</tr>
<tr>
<td class="td-name">Red wine — Ribera 2021</td>
<td>bottles</td>
<td class="mono">2</td>
<td class="mono">6</td>
<td><span class="chip chip-eightysixed">86'd</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- ─── Staff section ─── -->
<section class="section" id="s-staff" hidden>
<div class="sec-toolbar">
<p class="sec-count">7 on shift tonight</p>
<button class="primary" type="button">+ Add team member</button>
</div>
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Shift</th>
<th>Clock-in</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="td-name">Marco Reyes</td>
<td>Floor manager</td>
<td>17:00 – 23:30</td>
<td class="mono">17:02</td>
<td><span class="chip chip-confirmed">On shift</span></td>
</tr>
<tr>
<td class="td-name">Sofía Medina</td>
<td>Head chef</td>
<td>15:00 – 23:00</td>
<td class="mono">15:08</td>
<td><span class="chip chip-confirmed">On shift</span></td>
</tr>
<tr>
<td class="td-name">Diego Lara</td>
<td>Sous chef</td>
<td>15:00 – 23:00</td>
<td class="mono">15:11</td>
<td><span class="chip chip-confirmed">On shift</span></td>
</tr>
<tr>
<td class="td-name">Camila Torres</td>
<td>Waitstaff</td>
<td>17:30 – 23:30</td>
<td class="mono">—</td>
<td><span class="chip chip-pending">Not in</span></td>
</tr>
<tr>
<td class="td-name">Julián Ortiz</td>
<td>Bartender</td>
<td>18:00 – 01:00</td>
<td class="mono">18:03</td>
<td><span class="chip chip-confirmed">On shift</span></td>
</tr>
<tr>
<td class="td-name">Ana Petit</td>
<td>Waitstaff</td>
<td>Day off</td>
<td class="mono">—</td>
<td><span class="chip chip-off">Off</span></td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- ─── Service section ─── -->
<section class="section" id="s-service" hidden>
<div class="sec-toolbar">
<p class="sec-count" id="serviceStatusLabel">Service open since 19:00</p>
<button class="btn-danger" id="closeServiceBtn" type="button">Close service</button>
</div>
<div class="service-grid">
<div class="card svc-stat">
<p class="svc-label">Seats occupied</p>
<p class="svc-value">38 <span class="svc-sub">/ 42</span></p>
</div>
<div class="card svc-stat">
<p class="svc-label">Tables open</p>
<p class="svc-value" id="svcTablesOpen">7</p>
</div>
<div class="card svc-stat">
<p class="svc-label">Covers served tonight</p>
<p class="svc-value">68</p>
</div>
<div class="card svc-stat">
<p class="svc-label">Avg wait · kitchen</p>
<p class="svc-value">11 <span class="svc-sub">min</span></p>
</div>
</div>
<div class="card">
<p class="card-kicker">Active tables</p>
<div class="svc-floor" id="svcFloor"></div>
</div>
</section>
<!-- ─── Alerts section ─── -->
<section class="section" id="s-alerts" hidden>
<div class="sec-toolbar">
<p class="sec-count" id="alertsCount">2 active alerts</p>
</div>
<ul class="alerts-list" id="alertsList">
<li class="alert-item alert-warn" data-alert="pasta">
<span class="alert-icon">📦</span>
<div class="alert-body">
<p class="alert-title">Low stock — Pappardelle pasta</p>
<p class="alert-meta">0.4 kg remaining · par 2 kg · last ordered 6 days ago</p>
</div>
<div class="alert-actions">
<button class="btn-primary btn-sm" data-action="reorder" data-alert="pasta">Reorder</button>
<button class="btn-ghost btn-sm" data-action="dismiss" data-alert="pasta">Dismiss</button>
</div>
</li>
<li class="alert-item alert-info" data-alert="noshow">
<span class="alert-icon">📅</span>
<div class="alert-body">
<p class="alert-title">Possible no-show — Reyes 19:00</p>
<p class="alert-meta">Table 4 · 2 guests · 18 min past reservation time</p>
</div>
<div class="alert-actions">
<button class="btn-primary btn-sm" data-action="release" data-alert="noshow">Release table</button>
<button class="btn-ghost btn-sm" data-action="dismiss" data-alert="noshow">Seat arrived</button>
</div>
</li>
</ul>
<p class="alerts-empty" id="alertsEmpty" hidden>No active alerts.</p>
</section>
</main>
</div>
<script src="script.js"></script>
</body>
</html>Admin · Sales & KPI Dashboard
The manager’s view at the start of the day. Collapsible sidebar with five admin sections, topbar with restaurant + service selector + alerts. Body shows six KPI cards (covers / revenue / avg check / turn time / cancellations / tonight’s bookings), a 7-day SVG line chart with hover dots, a top-10 dish list with running totals, an hourly seat-occupancy heatmap, and a preview of tonight’s reservations.
Establishes the admin shell reused by the other four admin pages (inventory · menu editor · staff · reservations).