Pages Medium
Admin — Reservations Manager
Reservation manager — list / calendar / waitlist tabs, today's bookings with seating status, party-size filter, walk-in & no-show buttons and an enquiry/waitlist sidebar.
Open in Lab
MCP
html css vanilla-js
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;
}
*,
*::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;
}
/* Rail (shared) */
.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);
border-radius: 8px;
font-family: var(--font-display);
font-weight: 800;
display: grid;
place-items: center;
}
.brand-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.05rem;
}
.rail-nav {
flex: 1;
padding: 12px;
display: flex;
flex-direction: column;
gap: 2px;
}
.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;
}
.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-icon {
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;
}
.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(--terracotta);
color: var(--bone);
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 32px 48px;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.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;
gap: 6px;
align-items: center;
}
.date-pick {
display: inline-flex;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
padding: 3px 6px;
border-radius: 999px;
font-size: 0.84rem;
font-weight: 700;
color: var(--ink);
gap: 6px;
}
.btn-sq {
padding: 6px 10px !important;
font-size: 0.94rem !important;
background: transparent !important;
border: none !important;
color: var(--ink-2);
}
.btn-sq:hover {
color: var(--terracotta) !important;
background: transparent !important;
}
.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);
}
/* Status */
.status {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 18px;
}
@media (max-width: 980px) {
.status {
grid-template-columns: 1fr 1fr;
}
}
.stat {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 10px;
padding: 14px 16px;
}
.stat-warn {
border-left: 3px solid var(--warning);
}
.stat-label {
font-size: 0.72rem;
color: var(--warm-gray);
font-weight: 600;
}
.stat-value {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.55rem;
margin-top: 2px;
}
.stat-value small {
font-family: var(--font-body);
font-weight: 600;
font-size: 0.82rem;
color: var(--warm-gray);
}
.stat-meta {
font-size: 0.72rem;
color: var(--warm-gray);
margin-top: 2px;
font-weight: 600;
}
.is-up {
color: var(--success);
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
border-bottom: 1px solid rgba(44, 26, 14, 0.12);
margin-bottom: 16px;
}
.tab {
background: transparent;
border: none;
color: var(--ink-2);
font-family: inherit;
font-size: 0.94rem;
font-weight: 700;
padding: 12px 18px;
cursor: pointer;
position: relative;
}
.tab:hover {
color: var(--terracotta-d);
}
.tab.is-active {
color: var(--terracotta);
}
.tab.is-active::after {
content: "";
position: absolute;
left: 14px;
right: 14px;
bottom: -1px;
height: 3px;
background: var(--terracotta);
border-radius: 999px;
}
.tab-count {
margin-left: 6px;
background: var(--cream-2);
color: var(--ink-2);
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
padding: 2px 7px;
border-radius: 999px;
}
.tab.is-active .tab-count {
background: var(--terracotta);
color: var(--bone);
}
/* Board */
.board {
display: grid;
}
/* View shared bits */
.view-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.filters {
display: inline-flex;
align-items: center;
gap: 10px;
}
.seg-label {
font-size: 0.72rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--warm-gray);
font-weight: 700;
}
.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.8rem;
font-weight: 600;
padding: 6px 12px;
border-radius: 999px;
cursor: pointer;
}
.seg-btn.is-active {
background: var(--forest);
color: var(--bone);
}
.search {
display: inline-flex;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.1);
padding: 7px 14px;
border-radius: 999px;
gap: 8px;
color: var(--warm-gray);
}
.search input {
background: transparent;
border: none;
outline: none;
font-family: inherit;
font-size: 0.86rem;
color: var(--ink);
width: 240px;
}
/* List view */
.res-rows {
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.res-row {
display: grid;
grid-template-columns: 64px 1fr auto auto;
gap: 16px;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 12px;
padding: 12px 18px;
}
.res-row.row-hidden {
display: none !important;
}
.r-time {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1.05rem;
color: var(--ink);
}
.r-body {
display: flex;
flex-direction: column;
gap: 2px;
}
.r-name {
font-weight: 700;
font-size: 0.96rem;
}
.r-meta {
font-size: 0.78rem;
color: var(--warm-gray);
display: flex;
flex-wrap: wrap;
gap: 4px 12px;
}
.r-meta strong {
color: var(--ink-2);
}
.r-note {
font-size: 0.74rem;
color: var(--terracotta);
font-style: italic;
margin-top: 2px;
}
.r-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
}
.r-status[data-s="confirmed"] {
background: var(--cream-2);
color: var(--ink-2);
}
.r-status[data-s="arrived"] {
background: rgba(201, 168, 76, 0.22);
color: #8a7325;
}
.r-status[data-s="seated"] {
background: rgba(45, 74, 62, 0.16);
color: var(--forest-d);
}
.r-status[data-s="no-show"] {
background: rgba(179, 67, 42, 0.18);
color: var(--danger);
}
.r-status[data-s="cancelled"] {
background: var(--cream-2);
color: var(--warm-gray);
text-decoration: line-through;
}
.r-actions {
display: flex;
gap: 6px;
align-items: center;
}
.act {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.1);
padding: 6px 12px;
border-radius: 999px;
font-family: inherit;
font-size: 0.74rem;
font-weight: 700;
color: var(--ink-2);
cursor: pointer;
}
.act:hover {
background: var(--cream-2);
}
.act-primary {
background: var(--forest);
color: var(--bone);
border: none;
}
.act-primary:hover {
background: var(--forest-d);
}
.act-danger {
background: transparent;
color: var(--danger);
border-color: rgba(179, 67, 42, 0.3);
}
.act-danger:hover {
background: rgba(179, 67, 42, 0.08);
}
/* Calendar */
.cal-head {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
gap: 4px;
margin-bottom: 8px;
font-size: 0.74rem;
color: var(--warm-gray);
font-weight: 700;
}
.cal-head > div {
text-align: center;
padding: 8px 6px;
}
.cal-head .is-today {
background: var(--terracotta);
color: var(--bone);
border-radius: 8px;
}
.cal-grid {
display: grid;
grid-template-columns: 60px repeat(7, 1fr);
gap: 4px;
}
.cal-time {
display: grid;
place-items: center;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--warm-gray);
font-weight: 700;
}
.cal-cell {
height: 56px;
border-radius: 6px;
background: var(--cream-2);
position: relative;
display: grid;
place-items: center;
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--ink-2);
cursor: pointer;
}
.cal-cell[data-h="0"] {
background: var(--cream-2);
}
.cal-cell[data-h="1"] {
background: rgba(201, 168, 76, 0.25);
color: #8a7325;
}
.cal-cell[data-h="2"] {
background: rgba(193, 113, 74, 0.28);
color: var(--terracotta-d);
}
.cal-cell[data-h="3"] {
background: var(--terracotta);
color: var(--bone);
}
.cal-cell[data-h="4"] {
background: var(--forest);
color: var(--bone);
}
.cal-legend {
margin-top: 12px;
display: flex;
gap: 10px;
font-size: 0.78rem;
color: var(--warm-gray);
font-weight: 600;
}
.cl {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
}
.cl::before {
content: "";
width: 10px;
height: 10px;
border-radius: 3px;
}
.cl-low::before {
background: rgba(201, 168, 76, 0.4);
}
.cl-mid::before {
background: var(--terracotta);
}
.cl-high::before {
background: var(--forest);
}
/* Waitlist */
.wait-rows {
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.wait-row {
display: grid;
grid-template-columns: 64px 1fr auto auto;
gap: 16px;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 12px;
padding: 14px 18px;
}
.w-when {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.92rem;
color: var(--ink-2);
}
.w-name {
font-weight: 700;
}
.w-note {
font-size: 0.78rem;
color: var(--warm-gray);
margin-top: 2px;
}
.w-tag {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: var(--gold);
color: var(--ink);
}
.w-tag-quiet {
background: var(--cream-2);
color: var(--ink-2);
}
/* Toast */
.toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%);
background: var(--forest-d);
color: var(--bone);
padding: 10px 18px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18);
z-index: 10;
}
@media (max-width: 880px) {
.app {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
.rail {
flex-direction: row;
border-right: none;
border-bottom: 1px solid var(--forest-d);
overflow-x: auto;
padding: 8px 12px;
}
.rail-brand,
.rail-foot {
display: none;
}
.rail-nav {
flex-direction: row;
padding: 0;
}
.res-row,
.wait-row {
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
}
.r-time,
.w-when {
grid-row: 1;
grid-column: 1;
}
.r-status,
.w-tag {
grid-row: 1;
grid-column: 2;
justify-self: end;
}
.r-body {
grid-row: 2;
grid-column: 1 / -1;
}
.r-actions {
grid-row: 3;
grid-column: 1 / -1;
justify-content: flex-end;
}
}const BOOKINGS = [
{
id: "b1",
time: "19:00",
name: "Reyes",
phone: "+34 612 …",
party: 2,
table: "T4 · window",
status: "seated",
note: "",
},
{
id: "b2",
time: "19:00",
name: "García-Tan",
phone: "+34 645 …",
party: 4,
table: "T11 · centre",
status: "arrived",
note: "Birthday · candle ready",
},
{
id: "b3",
time: "19:15",
name: "Khoury",
phone: "+34 654 …",
party: 2,
table: "Bar 1",
status: "confirmed",
note: "Regular · usual seat",
},
{
id: "b4",
time: "19:30",
name: "Marquez",
phone: "+34 698 …",
party: 2,
table: "T3 · window",
status: "confirmed",
note: "",
},
{
id: "b5",
time: "19:45",
name: "Loredo",
phone: "+34 622 …",
party: 4,
table: "T5 · centre",
status: "confirmed",
note: "Allergic to nuts",
},
{
id: "b6",
time: "20:00",
name: "Mendoza",
phone: "+34 661 …",
party: 6,
table: "Long table",
status: "confirmed",
note: "Catering enquiry · ask about events",
},
{
id: "b7",
time: "20:00",
name: "Yamamoto",
phone: "+34 633 …",
party: 3,
table: "T12 · centre",
status: "confirmed",
note: "",
},
{
id: "b8",
time: "20:15",
name: "Park",
phone: "+34 678 …",
party: 2,
table: "P1 · patio",
status: "confirmed",
note: "",
},
{
id: "b9",
time: "20:30",
name: "Singh",
phone: "+34 671 …",
party: 2,
table: "P4 · patio",
status: "confirmed",
note: "",
},
{
id: "b10",
time: "20:30",
name: "Tanaka",
phone: "+34 685 …",
party: 3,
table: "T7 · window",
status: "confirmed",
note: "Anniversary",
},
{
id: "b11",
time: "21:00",
name: "Costa",
phone: "+34 612 …",
party: 3,
table: "T4 · window",
status: "confirmed",
note: "",
},
{
id: "b12",
time: "21:00",
name: "Vega",
phone: "+34 620 …",
party: 4,
table: "T5 · centre",
status: "confirmed",
note: "",
},
{
id: "b13",
time: "21:15",
name: "Davis",
phone: "+44 7 …",
party: 4,
table: "P5 · patio",
status: "confirmed",
note: "Visiting from London",
},
{
id: "b14",
time: "21:30",
name: "Walk-in expected",
phone: "—",
party: 2,
table: "Bar 2",
status: "cancelled",
note: "Cancelled this morning",
},
];
const WAITLIST = [
{
id: "w1",
when: "tonight",
name: "Bautista, +1",
note: "Party of 2 · flexible 19–22h · prefers patio",
tag: "Promote",
quiet: false,
},
{
id: "w2",
when: "Fri 21:00",
name: "Iyengar",
note: "×4 · added to waitlist at 11:32 today",
tag: "On list",
quiet: true,
},
{
id: "w3",
when: "Sat 20:30",
name: "Bechtel",
note: "×2 · birthday · happy with any table",
tag: "On list",
quiet: true,
},
];
const STATUS_LABEL = {
confirmed: "Confirmed",
arrived: "Arrived",
seated: "Seated",
"no-show": "No-show",
cancelled: "Cancelled",
};
let activeView = "list";
let partyFilter = "all";
let query = "";
const resList = document.getElementById("resList");
const waitList = document.getElementById("waitList");
const toast = document.getElementById("toast");
function partyMatches(p) {
if (partyFilter === "all") return true;
if (partyFilter === "more") return p >= 7;
if (partyFilter === "2") return p <= 2;
if (partyFilter === "4") return p >= 3 && p <= 4;
if (partyFilter === "6") return p >= 5 && p <= 6;
return true;
}
function renderList() {
const q = query.toLowerCase();
resList.innerHTML = BOOKINGS.map((b) => {
const hide =
!partyMatches(b.party) ||
(q && !`${b.name} ${b.phone} ${b.table} ${b.note}`.toLowerCase().includes(q));
const actions =
b.status === "cancelled"
? `<button class="act" data-action="reinstate" data-id="${b.id}">Reinstate</button>`
: b.status === "seated"
? `<button class="act" data-action="check" data-id="${b.id}">Mark check</button>
<button class="act act-danger" data-action="cancel" data-id="${b.id}">Cancel</button>`
: `<button class="act act-primary" data-action="seat" data-id="${b.id}">Seat now</button>
<button class="act" data-action="noshow" data-id="${b.id}">No-show</button>
<button class="act act-danger" data-action="cancel" data-id="${b.id}">Cancel</button>`;
return `<li class="res-row ${hide ? "row-hidden" : ""}" data-id="${b.id}">
<span class="r-time">${b.time}</span>
<div class="r-body">
<p class="r-name">${b.name}</p>
<p class="r-meta">
<strong>×${b.party}</strong>
· <span>${b.table}</span>
· <span>${b.phone}</span>
</p>
${b.note ? `<p class="r-note">${b.note}</p>` : ""}
</div>
<span class="r-status" data-s="${b.status}">${STATUS_LABEL[b.status]}</span>
<div class="r-actions">${actions}</div>
</li>`;
}).join("");
}
function renderWaitlist() {
waitList.innerHTML = WAITLIST.map(
(w) => `<li class="wait-row" data-id="${w.id}">
<span class="w-when">${w.when}</span>
<div>
<p class="w-name">${w.name}</p>
<p class="w-note">${w.note}</p>
</div>
<span class="w-tag ${w.quiet ? "w-tag-quiet" : ""}">${w.tag}</span>
<button class="act act-primary" data-action="promote" data-id="${w.id}">${w.quiet ? "Offer slot" : "Promote ↗"}</button>
</li>`
).join("");
}
function renderCalendar() {
const calHead = document.getElementById("calHead");
const calGrid = document.getElementById("cal");
if (!calHead || !calGrid) return;
const days = ["Tue", "Wed", "Thu", "Fri", "Sat", "Sun", "Mon"];
const today = 0; // demo: today = Tue (offset 0)
calHead.innerHTML =
`<div></div>` +
days.map((d, i) => `<div class="${i === today ? "is-today" : ""}">${d}</div>`).join("");
const HOURS = ["18", "19", "20", "21", "22"];
// Density 0..4 per cell — heat varies by time and day
const DENSITY = [
[1, 1, 2, 2, 3, 4, 1],
[3, 3, 3, 4, 4, 4, 2],
[4, 4, 4, 4, 4, 3, 2],
[3, 3, 4, 4, 4, 1, 0],
[1, 1, 2, 3, 3, 0, 0],
];
let html = "";
HOURS.forEach((h, r) => {
html += `<div class="cal-time">${h}h</div>`;
DENSITY[r].forEach((v, c) => {
const count = v === 0 ? 0 : v + 1;
html += `<div class="cal-cell" data-h="${v}" data-day="${days[c]}" data-hour="${h}">${count ? `${count}×` : ""}</div>`;
});
});
calGrid.innerHTML = html;
}
// View switching
document.querySelectorAll("[data-view]").forEach((el) => {
if (el.tagName === "BUTTON") {
el.addEventListener("click", () => {
activeView = el.dataset.view;
document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("is-active", t === el));
document.querySelectorAll(".view").forEach((v) => (v.hidden = v.dataset.view !== activeView));
if (activeView === "cal") renderCalendar();
});
}
});
document.querySelectorAll(".seg-btn").forEach((btn) =>
btn.addEventListener("click", () => {
partyFilter = btn.dataset.party;
document
.querySelectorAll(".seg-btn")
.forEach((b) => b.classList.toggle("is-active", b === btn));
renderList();
})
);
document.getElementById("search").addEventListener("input", (e) => {
query = e.target.value.trim();
renderList();
});
resList.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const id = btn.dataset.id;
const b = BOOKINGS.find((x) => x.id === id);
if (!b) return;
switch (btn.dataset.action) {
case "seat":
b.status = "seated";
showToast(`${b.name} seated at ${b.table}`);
break;
case "noshow":
b.status = "no-show";
showToast(`${b.name} marked no-show`);
break;
case "cancel":
b.status = "cancelled";
showToast(`${b.name} cancelled`);
break;
case "reinstate":
b.status = "confirmed";
showToast(`${b.name} back on the list`);
break;
case "check":
showToast(`Bill brought to ${b.name}`);
break;
}
renderList();
});
waitList.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
showToast("Sent an offer · waiting for confirmation");
});
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2400);
}
renderList();
renderWaitlist();<!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>Reservations · Casa Olivar Admin</title>
</head>
<body>
<div class="app">
<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">
<a class="r-link" href="#"><span class="r-icon">📊</span><span>Dashboard</span></a>
<a class="r-link is-active" href="#"><span class="r-icon">📅</span><span>Reservations</span><span class="r-badge">14</span></a>
<a class="r-link" href="#"><span class="r-icon">🍽</span><span>Menu</span></a>
<a class="r-link" href="#"><span class="r-icon">📦</span><span>Inventory</span></a>
<a class="r-link" href="#"><span class="r-icon">👥</span><span>Staff</span></a>
</nav>
<footer class="rail-foot">
<div class="rail-user">
<span class="user-avatar">I</span>
<div>
<p class="user-name">Iria Costa</p>
<p class="user-role">Floor lead</p>
</div>
</div>
</footer>
</aside>
<main class="main">
<header class="top">
<div>
<p class="kicker">Tuesday · 12 May · service starts 19:00</p>
<h1>Reservations</h1>
</div>
<div class="top-tools">
<div class="date-pick">
<button class="ghost btn-sq" type="button" id="prev">‹</button>
<span id="dateLabel">Today</span>
<button class="ghost btn-sq" type="button" id="next">›</button>
</div>
<button class="ghost" type="button">+ Walk-in</button>
<button class="primary" type="button">+ New booking</button>
</div>
</header>
<!-- KPI strip -->
<section class="status">
<article class="stat">
<p class="stat-label">Booked covers</p>
<p class="stat-value" id="kCovers">38 <small>/ 42 seats</small></p>
<p class="stat-meta is-up">▲ 6 vs same Tue last week</p>
</article>
<article class="stat">
<p class="stat-label">Walk-in capacity</p>
<p class="stat-value">4 <small>seats</small></p>
<p class="stat-meta">19:00 · 21:30 buckets</p>
</article>
<article class="stat stat-warn">
<p class="stat-label">No-shows (rolling 30 d)</p>
<p class="stat-value">4.2% <small>rate</small></p>
<p class="stat-meta">2 large parties tonight</p>
</article>
<article class="stat">
<p class="stat-label">Waitlist</p>
<p class="stat-value">3 <small>requests</small></p>
<p class="stat-meta">avg promote · 24 min</p>
</article>
</section>
<!-- View tabs -->
<nav class="tabs" aria-label="View">
<button class="tab is-active" data-view="list">List</button>
<button class="tab" data-view="cal">Calendar</button>
<button class="tab" data-view="wait">Waitlist <span class="tab-count">3</span></button>
</nav>
<!-- Body -->
<section class="board">
<!-- LIST -->
<section class="view" data-view="list">
<header class="view-head">
<div class="filters">
<label class="seg-label">Party</label>
<div class="seg">
<button class="seg-btn is-active" data-party="all">All</button>
<button class="seg-btn" data-party="2">1–2</button>
<button class="seg-btn" data-party="4">3–4</button>
<button class="seg-btn" data-party="6">5–6</button>
<button class="seg-btn" data-party="more">7+</button>
</div>
</div>
<div class="search">
<svg viewBox="0 0 24 24" width="14" height="14"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="m20 20-3.5-3.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search name, phone or note…" />
</div>
</header>
<ul class="res-rows" id="resList"></ul>
</section>
<!-- CALENDAR -->
<section class="view" data-view="cal" hidden>
<header class="cal-head" id="calHead"></header>
<div class="cal-grid" id="cal"></div>
<p class="cal-legend">
<span class="cl cl-low">Light</span>
<span class="cl cl-mid">Busy</span>
<span class="cl cl-high">Full</span>
</p>
</section>
<!-- WAITLIST -->
<section class="view" data-view="wait" hidden>
<ul class="wait-rows" id="waitList"></ul>
</section>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
</div>
<script src="script.js"></script>
</body>
</html>Admin · Reservations Manager
The hostess station view. Top: today’s KPIs (booked covers · walk-in capacity · no-shows · waitlist). View tabs swap between list, calendar (this week, hourly), and waitlist. List view shows each booking with party · time · table · status badges (confirmed / arrived / seated / no-show / cancelled), inline “Seat now” / “No-show” / “Cancel” buttons, and a side panel with the day’s enquiries and waitlist requests with promote-to-booking buttons.