Hotel Inventory & Allotment
An inventory and allotment grid for Aurelia Hotels showing room types × dates with available/sold room counts, stop-sell flags, and low-availability colour warnings. Adjust allotment per cell with steppers, toggle stop-sell, filter by room type, and watch the summary bar update in real time.
MCP
Kod
/* ── Design tokens ───────────────────────────────────────────────────────── */
:root {
--navy: #1a2b4a;
--navy-d: #0f1d36;
--navy-2: #2d4570;
--cream: #f7f3eb;
--cream-2: #ece5d4;
--bone: #fbf8f2;
--gold: #c9a649;
--gold-light: #e0c378;
--gold-d: #a88a2e;
--ink: #161e2c;
--ink-2: #2e3a52;
--warm-gray: #6c7280;
--line: rgba(22, 30, 44, 0.1);
--line-strong: rgba(22, 30, 44, 0.18);
--success: #4a7752;
--danger: #b34232;
--warning: #d99020;
--info: #4a6da0;
--font-display: "Cormorant Garamond", 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;
--shadow-1: 0 1px 2px rgba(22, 30, 44, 0.06), 0 2px 8px rgba(22, 30, 44, 0.06);
--shadow-2: 0 12px 36px rgba(15, 29, 54, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
overflow: hidden;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
}
/* ── Layout ─────────────────────────────────────────────────────────────── */
.inv {
height: 100vh;
display: grid;
grid-template-columns: 220px 1fr;
}
/* ── Rail ───────────────────────────────────────────────────────────────── */
.rail {
background: var(--navy);
color: var(--bone);
display: flex;
flex-direction: column;
padding: 22px 16px 14px;
overflow-y: auto;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding: 0 4px 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.brand-mark {
width: 38px;
height: 38px;
display: grid;
place-items: center;
border-radius: var(--r-md);
background: var(--gold);
color: var(--navy-d);
font-family: var(--font-display);
font-weight: 700;
font-size: 1.35rem;
flex-shrink: 0;
}
.brand-name {
font-family: var(--font-display);
font-size: 1.05rem;
font-weight: 700;
}
.brand-prop {
font-size: 0.68rem;
color: var(--gold-light);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-top: 2px;
}
.nav {
margin-top: 18px;
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.nav-item {
font-size: 0.84rem;
font-weight: 500;
color: rgba(251, 248, 242, 0.72);
text-decoration: none;
padding: 9px 12px;
border-radius: var(--r-md);
display: flex;
align-items: center;
gap: 8px;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--bone);
}
.nav-item.is-active {
background: rgba(201, 166, 73, 0.16);
color: var(--gold-light);
font-weight: 600;
}
.nav-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--gold);
flex-shrink: 0;
}
/* ── Legend ─────────────────────────────────────────────────────────────── */
.rail-legend {
margin-top: 20px;
padding: 14px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: var(--r-md);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.legend-title {
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gold-light);
margin-bottom: 8px;
}
.legend-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.76rem;
color: rgba(251, 248, 242, 0.72);
margin-bottom: 5px;
}
.swatch {
width: 10px;
height: 10px;
border-radius: 3px;
flex-shrink: 0;
}
.av-ok {
background: rgba(74, 119, 82, 0.5);
border: 1px solid rgba(74, 119, 82, 0.7);
}
.av-low {
background: rgba(217, 144, 32, 0.5);
border: 1px solid rgba(217, 144, 32, 0.7);
}
.av-none {
background: rgba(179, 66, 50, 0.5);
border: 1px solid rgba(179, 66, 50, 0.7);
}
.av-stop {
background: rgba(22, 30, 44, 0.4);
border: 1px solid rgba(22, 30, 44, 0.5);
}
.rail-foot {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 2px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.clock {
font-family: var(--font-mono);
font-weight: 600;
font-size: 0.84rem;
color: var(--gold-light);
}
.agent {
font-size: 0.7rem;
color: rgba(251, 248, 242, 0.55);
letter-spacing: 0.04em;
}
/* ── Main ───────────────────────────────────────────────────────────────── */
.main {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--cream);
}
/* ── Topbar ─────────────────────────────────────────────────────────────── */
.topbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
padding: 22px 28px 14px;
border-bottom: 1px solid var(--line);
background: var(--bone);
flex-shrink: 0;
}
.kicker {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--gold-d);
font-weight: 600;
}
.topbar h1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.8rem;
letter-spacing: -0.005em;
margin-top: 2px;
}
.topbar h1 span {
color: var(--navy-2);
font-weight: 500;
font-size: 1.4rem;
}
.topbar-controls {
display: flex;
align-items: center;
gap: 10px;
}
.ctrl-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--warm-gray);
display: flex;
flex-direction: column;
gap: 3px;
}
.ctrl-select {
font-family: var(--font-body);
font-size: 0.84rem;
font-weight: 500;
color: var(--ink);
background: var(--bone);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
padding: 7px 10px;
cursor: pointer;
outline: none;
}
.ctrl-select:focus {
border-color: var(--navy-2);
}
.icon-btn {
background: var(--bone);
border: 1px solid var(--line);
width: 38px;
height: 38px;
border-radius: 999px;
display: grid;
place-items: center;
cursor: pointer;
color: var(--ink-2);
align-self: flex-end;
}
.icon-btn:hover {
background: var(--cream-2);
}
/* ── Grid wrap ──────────────────────────────────────────────────────────── */
.grid-wrap {
flex: 1;
overflow: auto;
padding-bottom: 4px;
}
/* ── Inventory grid ─────────────────────────────────────────────────────── */
.inv-grid {
border-collapse: collapse;
width: max-content;
min-width: 100%;
font-size: 0.82rem;
}
.inv-grid thead th {
position: sticky;
top: 0;
z-index: 10;
background: var(--bone);
border-bottom: 2px solid var(--line-strong);
padding: 0;
}
.inv-grid thead th:first-child {
position: sticky;
left: 0;
z-index: 20;
min-width: 170px;
}
.th-inner {
padding: 10px 12px;
text-align: center;
min-width: 100px;
}
.th-date {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.78rem;
color: var(--navy-d);
display: block;
}
.th-dow {
font-size: 0.68rem;
font-weight: 600;
color: var(--warm-gray);
text-transform: uppercase;
letter-spacing: 0.1em;
display: block;
margin-top: 2px;
}
.th-dow.weekend {
color: var(--gold-d);
}
.th-room-label {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
text-align: left;
padding: 10px 16px;
}
/* ── Row header (sticky left) ───────────────────────────────────────────── */
.inv-grid tbody td.row-head {
position: sticky;
left: 0;
z-index: 5;
background: var(--bone);
border-right: 2px solid var(--line-strong);
padding: 0;
}
.row-head-inner {
padding: 0 14px;
min-height: 72px;
display: flex;
flex-direction: column;
justify-content: center;
}
.row-type-name {
font-weight: 700;
font-size: 0.88rem;
color: var(--navy-d);
letter-spacing: -0.005em;
}
.row-type-sub {
font-family: var(--font-mono);
font-size: 0.66rem;
color: var(--warm-gray);
letter-spacing: 0.08em;
margin-top: 2px;
}
/* ── Inventory cell ─────────────────────────────────────────────────────── */
.inv-grid tbody td.inv-cell {
padding: 0;
border: 1px solid var(--line);
vertical-align: top;
min-width: 100px;
height: 72px;
position: relative;
}
.cell-inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: 6px 8px;
gap: 1px;
position: relative;
transition: background 0.12s;
}
/* availability colouring via data-av */
.inv-cell[data-av="ok"] .cell-inner {
background: rgba(74, 119, 82, 0.1);
}
.inv-cell[data-av="low"] .cell-inner {
background: rgba(217, 144, 32, 0.16);
}
.inv-cell[data-av="none"] .cell-inner {
background: rgba(179, 66, 50, 0.14);
}
.inv-cell[data-av="stop"] .cell-inner {
background: rgba(22, 30, 44, 0.07);
opacity: 0.7;
}
.cell-avail {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1.1rem;
color: var(--navy-d);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.cell-avail.zero {
color: var(--danger);
}
.cell-sub {
font-size: 0.64rem;
color: var(--warm-gray);
font-variant-numeric: tabular-nums;
letter-spacing: 0.03em;
}
.cell-stop-badge {
font-size: 0.58rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 1px 5px;
border-radius: 3px;
background: rgba(22, 30, 44, 0.12);
color: var(--warm-gray);
margin-top: 1px;
}
/* stepper + stop-sell controls — visible on hover */
.cell-controls {
display: none;
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
gap: 2px;
align-items: center;
}
.inv-cell:hover .cell-controls {
display: flex;
}
.step-btn {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid var(--line-strong);
background: var(--bone);
font-size: 0.78rem;
font-weight: 700;
color: var(--navy-d);
cursor: pointer;
display: grid;
place-items: center;
line-height: 1;
}
.step-btn:hover {
background: var(--cream-2);
}
.step-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.stop-btn {
width: 20px;
height: 20px;
border-radius: 4px;
border: 1px solid var(--line-strong);
background: var(--bone);
font-size: 0.6rem;
font-weight: 700;
color: var(--warm-gray);
cursor: pointer;
display: grid;
place-items: center;
margin-left: 2px;
}
.stop-btn:hover {
background: var(--cream-2);
}
.stop-btn.active {
background: var(--danger);
color: white;
border-color: var(--danger);
}
/* dirty cell indicator */
.inv-cell.is-dirty::after {
content: "";
position: absolute;
top: 4px;
right: 4px;
width: 5px;
height: 5px;
border-radius: 999px;
background: var(--gold);
}
/* ── Summary bar ────────────────────────────────────────────────────────── */
.summary-bar {
display: flex;
align-items: center;
gap: 0;
background: var(--navy-d);
border-top: 1px solid rgba(255, 255, 255, 0.08);
flex-shrink: 0;
flex-wrap: wrap;
}
.sum-kpi {
display: flex;
flex-direction: column;
gap: 1px;
padding: 10px 20px;
border-right: 1px solid rgba(255, 255, 255, 0.08);
}
.sum-label {
font-size: 0.64rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(251, 248, 242, 0.55);
}
.sum-val {
font-family: var(--font-mono);
font-size: 1rem;
font-weight: 700;
color: var(--bone);
font-variant-numeric: tabular-nums;
}
.sum-val.warn {
color: var(--gold-light);
}
.sum-val.danger {
color: #e07060;
}
.save-btn {
margin-left: auto;
margin-right: 16px;
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-size: 0.84rem;
font-weight: 700;
padding: 10px 20px;
border-radius: var(--r-md);
cursor: pointer;
white-space: nowrap;
}
.save-btn:hover {
background: var(--gold-light);
}
/* ── Toast ──────────────────────────────────────────────────────────────── */
.toast {
position: fixed;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 11px 20px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--shadow-2);
white-space: nowrap;
pointer-events: none;
}
/* ── Responsive ─────────────────────────────────────────────────────────── */
@media (max-width: 960px) {
.inv {
grid-template-columns: 1fr;
}
.rail {
display: none;
}
.topbar {
flex-direction: column;
align-items: flex-start;
padding: 14px 16px 10px;
}
.topbar-controls {
flex-wrap: wrap;
}
.summary-bar {
justify-content: flex-start;
}
.save-btn {
margin: 8px 16px;
}
}
@media (max-width: 560px) {
.topbar h1 {
font-size: 1.3rem;
}
.sum-kpi {
padding: 8px 12px;
}
.sum-val {
font-size: 0.9rem;
}
}// ── Config ───────────────────────────────────────────────────────────────────
const START_DATE = new Date("2026-06-09T00:00:00");
const NUM_DAYS = 14;
// ── Room types ───────────────────────────────────────────────────────────────
const ROOM_TYPES = [
{ code: "STD", name: "Standard Double", physical: 12 },
{ code: "STD", name: "Standard Twin", physical: 8 },
{ code: "DLX", name: "Deluxe Double", physical: 10 },
{ code: "DLX", name: "Deluxe Sea View", physical: 6 },
{ code: "JNR", name: "Junior Suite", physical: 5 },
{ code: "STE", name: "Grand Suite", physical: 3 },
{ code: "STE", name: "Penthouse Suite", physical: 2 },
];
// ── Occupancy seeds (per date index, 0-13) ───────────────────────────────────
const OCC_SEEDS = [0.52, 0.58, 0.7, 0.78, 0.86, 0.92, 0.96, 0.65, 0.6, 0.7, 0.81, 0.88, 0.91, 0.74];
// ── State ────────────────────────────────────────────────────────────────────
// cells[rtIdx][dayIdx] = { allotment, sold, stopSell }
const cells = [];
let dirtySet = new Set(); // "ri-di" keys
// ── Init state ────────────────────────────────────────────────────────────────
function initCells() {
ROOM_TYPES.forEach((rt, ri) => {
cells[ri] = [];
for (let di = 0; di < NUM_DAYS; di++) {
const occ = OCC_SEEDS[di] + (Math.random() * 0.1 - 0.05);
const sold = Math.min(rt.physical, Math.round(rt.physical * occ));
cells[ri][di] = {
allotment: rt.physical,
sold: sold,
stopSell: false,
};
}
});
}
// ── Availability tier ─────────────────────────────────────────────────────────
function avTier(c) {
if (c.stopSell) return "stop";
const avail = c.allotment - c.sold;
if (avail <= 0) return "none";
if (avail <= 3) return "low";
return "ok";
}
// ── Date helpers ─────────────────────────────────────────────────────────────
function addDays(d, n) {
return new Date(d.getTime() + n * 86400000);
}
function fmtDate(d) {
return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
}
function fmtDow(d) {
return d.toLocaleDateString("en-GB", { weekday: "short" });
}
// ── Toast helper ─────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
// ── Summary bar ──────────────────────────────────────────────────────────────
function updateSummary() {
const vis = getVisibleRtIndices();
let totalAllotment = 0,
totalSold = 0,
totalAvail = 0,
stopSells = 0,
soldOuts = 0;
vis.forEach((ri) => {
for (let di = 0; di < NUM_DAYS; di++) {
const c = cells[ri][di];
totalAllotment += c.allotment;
totalSold += c.sold;
totalAvail += Math.max(0, c.allotment - c.sold);
if (c.stopSell) stopSells++;
if (!c.stopSell && c.allotment - c.sold <= 0) soldOuts++;
}
});
document.getElementById("sumAvailable").textContent = totalAvail;
document.getElementById("sumSold").textContent = totalSold;
document.getElementById("sumAllotment").textContent = totalAllotment;
document.getElementById("sumStopSell").textContent = stopSells;
document.getElementById("sumSoldOut").textContent = soldOuts;
}
// ── Build thead ──────────────────────────────────────────────────────────────
function buildHead() {
const gridHead = document.getElementById("gridHead");
const tr = document.createElement("tr");
const thCorner = document.createElement("th");
thCorner.innerHTML = `<span class="th-room-label">Room Type</span>`;
tr.appendChild(thCorner);
for (let di = 0; di < NUM_DAYS; di++) {
const d = addDays(START_DATE, di);
const dow = fmtDow(d);
const isWk = d.getDay() === 0 || d.getDay() === 6;
const th = document.createElement("th");
th.innerHTML = `<div class="th-inner">
<span class="th-date">${fmtDate(d)}</span>
<span class="th-dow${isWk ? " weekend" : ""}">${dow}</span>
</div>`;
tr.appendChild(th);
}
gridHead.innerHTML = "";
gridHead.appendChild(tr);
}
// ── Build tbody ──────────────────────────────────────────────────────────────
function buildBody() {
const gridBody = document.getElementById("gridBody");
gridBody.innerHTML = "";
ROOM_TYPES.forEach((rt, ri) => {
const tr = document.createElement("tr");
tr.dataset.ri = ri;
const tdHead = document.createElement("td");
tdHead.className = "row-head";
tdHead.innerHTML = `<div class="row-head-inner">
<div class="row-type-name">${rt.name}</div>
<div class="row-type-sub">${rt.code} · ${rt.physical} rooms</div>
</div>`;
tr.appendChild(tdHead);
for (let di = 0; di < NUM_DAYS; di++) {
tr.appendChild(buildCell(ri, di));
}
gridBody.appendChild(tr);
});
}
// ── Build single cell ─────────────────────────────────────────────────────────
function buildCell(ri, di) {
const c = cells[ri][di];
const key = `${ri}-${di}`;
const avail = Math.max(0, c.allotment - c.sold);
const tier = avTier(c);
const td = document.createElement("td");
td.className = "inv-cell";
td.dataset.ri = ri;
td.dataset.di = di;
td.dataset.av = tier;
if (dirtySet.has(key)) td.classList.add("is-dirty");
td.innerHTML = cellHTML(c, avail, tier);
// wire stepper buttons
td.querySelector(".step-up").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].allotment = Math.min(ROOM_TYPES[ri].physical + 4, c.allotment + 1);
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
});
td.querySelector(".step-dn").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].allotment = Math.max(c.sold, c.allotment - 1);
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
});
td.querySelector(".stop-btn").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].stopSell = !c.stopSell;
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
showToast(cells[ri][di].stopSell ? "Stop-sell activated" : "Stop-sell removed");
});
return td;
}
// ── Cell inner HTML ───────────────────────────────────────────────────────────
function cellHTML(c, avail, tier) {
const canStepUp = c.allotment < ROOM_TYPES[0].physical + 4; // loose upper cap
const canStepDn = c.allotment > c.sold;
return `<div class="cell-inner">
<span class="cell-avail${avail === 0 && !c.stopSell ? " zero" : ""}">${c.stopSell ? "—" : avail}</span>
<span class="cell-sub">${c.sold} sold / ${c.allotment} allot</span>
${c.stopSell ? `<span class="cell-stop-badge">Stop-sell</span>` : ""}
<div class="cell-controls">
<button class="step-btn step-dn"${c.stopSell || !canStepDn ? " disabled" : ""}>−</button>
<button class="step-btn step-up"${c.stopSell ? " disabled" : ""}>+</button>
<button class="stop-btn${c.stopSell ? " active" : ""}" title="Toggle stop-sell">S</button>
</div>
</div>`;
}
// ── Refresh single cell in place ──────────────────────────────────────────────
function refreshCell(td, ri, di) {
const c = cells[ri][di];
const key = `${ri}-${di}`;
const avail = Math.max(0, c.allotment - c.sold);
const tier = avTier(c);
td.dataset.av = tier;
td.classList.toggle("is-dirty", dirtySet.has(key));
td.innerHTML = cellHTML(c, avail, tier);
// rebind after innerHTML replacement
td.querySelector(".step-up").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].allotment = Math.min(ROOM_TYPES[ri].physical + 4, c.allotment + 1);
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
});
td.querySelector(".step-dn").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].allotment = Math.max(c.sold, c.allotment - 1);
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
});
td.querySelector(".stop-btn").addEventListener("click", (e) => {
e.stopPropagation();
cells[ri][di].stopSell = !c.stopSell;
dirtySet.add(key);
refreshCell(td, ri, di);
updateSummary();
showToast(cells[ri][di].stopSell ? "Stop-sell activated" : "Stop-sell removed");
});
}
// ── Room type filter ──────────────────────────────────────────────────────────
function getVisibleRtIndices() {
const f = document.getElementById("filterType").value;
return ROOM_TYPES.reduce((acc, rt, i) => {
if (f === "all" || rt.code === f) acc.push(i);
return acc;
}, []);
}
function applyFilter() {
const vis = new Set(getVisibleRtIndices());
document.querySelectorAll("#gridBody tr[data-ri]").forEach((tr) => {
tr.style.display = vis.has(parseInt(tr.dataset.ri)) ? "" : "none";
});
updateSummary();
}
document.getElementById("filterType").addEventListener("change", applyFilter);
// ── Save ──────────────────────────────────────────────────────────────────────
document.getElementById("saveBtn").addEventListener("click", () => {
const n = dirtySet.size;
if (n === 0) {
showToast("No changes to save");
return;
}
dirtySet.clear();
document.querySelectorAll(".inv-cell.is-dirty").forEach((td) => td.classList.remove("is-dirty"));
updateSummary();
showToast(`${n} allotment change${n !== 1 ? "s" : ""} saved`);
});
// ── Date range label ─────────────────────────────────────────────────────────
function setDateRange() {
const end = addDays(START_DATE, NUM_DAYS - 1);
document.getElementById("dateRange").textContent = `${fmtDate(START_DATE)} – ${fmtDate(end)}`;
}
// ── Clock ────────────────────────────────────────────────────────────────────
function tick() {
const now = new Date();
const hhmm = now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
const wk = now.toLocaleDateString("en-GB", { weekday: "short" });
document.getElementById("clock").textContent = `${hhmm} · ${wk}`;
}
tick();
setInterval(tick, 1000);
// ── Boot ─────────────────────────────────────────────────────────────────────
initCells();
buildHead();
buildBody();
setDateRange();
updateSummary();<!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=Cormorant+Garamond:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Inventory & Allotment · Aurelia Hotels</title>
</head>
<body>
<main class="inv">
<!-- ── Rail ── -->
<aside class="rail">
<div class="brand">
<span class="brand-mark">Æ</span>
<div>
<p class="brand-name">Aurelia Hotels</p>
<p class="brand-prop">Aurelia · Madrid</p>
</div>
</div>
<nav class="nav">
<a class="nav-item" href="#">Dashboard</a>
<a class="nav-item" href="#">Reservations</a>
<a class="nav-item" href="#">Room rack</a>
<a class="nav-item" href="#">Rate mgmt</a>
<a class="nav-item" href="#">Channels</a>
<a class="nav-item is-active" href="#"><span class="nav-dot"></span>Inventory</a>
<a class="nav-item" href="#">Reports</a>
</nav>
<!-- Legend -->
<div class="rail-legend">
<p class="legend-title">Availability key</p>
<div class="legend-row"><span class="swatch av-ok"></span>Available ≥4</div>
<div class="legend-row"><span class="swatch av-low"></span>Low 1–3</div>
<div class="legend-row"><span class="swatch av-none"></span>None / sold out</div>
<div class="legend-row"><span class="swatch av-stop"></span>Stop-sell active</div>
</div>
<footer class="rail-foot">
<span class="clock" id="clock">--:-- · --</span>
<span class="agent">Revenue · Sofia M.</span>
</footer>
</aside>
<!-- ── Main ── -->
<section class="main">
<header class="topbar">
<div>
<p class="kicker">Inventory</p>
<h1>Allotment Grid · <span id="dateRange">—</span></h1>
</div>
<div class="topbar-controls">
<label class="ctrl-label">Room type
<select id="filterType" class="ctrl-select">
<option value="all">All types</option>
<option value="STD">Standard</option>
<option value="DLX">Deluxe</option>
<option value="JNR">Junior Suite</option>
<option value="STE">Suite</option>
</select>
</label>
<button class="icon-btn" title="Notifications">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M12 22a2 2 0 0 0 2-2h-4a2 2 0 0 0 2 2zm6-6V11a6 6 0 0 0-12 0v5l-2 2v1h16v-1l-2-2z" fill="currentColor"/></svg>
</button>
</div>
</header>
<!-- ── Inventory grid ── -->
<div class="grid-wrap">
<table class="inv-grid" id="invGrid">
<thead id="gridHead"></thead>
<tbody id="gridBody"></tbody>
</table>
</div>
<!-- ── Summary bar ── -->
<div class="summary-bar">
<div class="sum-kpi">
<span class="sum-label">Total available</span>
<span class="sum-val" id="sumAvailable">—</span>
</div>
<div class="sum-kpi">
<span class="sum-label">Total sold</span>
<span class="sum-val" id="sumSold">—</span>
</div>
<div class="sum-kpi">
<span class="sum-label">Total allotment</span>
<span class="sum-val" id="sumAllotment">—</span>
</div>
<div class="sum-kpi">
<span class="sum-label">Stop-sells active</span>
<span class="sum-val warn" id="sumStopSell">—</span>
</div>
<div class="sum-kpi">
<span class="sum-label">Sold-out cells</span>
<span class="sum-val danger" id="sumSoldOut">—</span>
</div>
<button class="save-btn" id="saveBtn">Save changes</button>
</div>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Hotel Inventory & Allotment
A full-screen inventory-management console for Aurelia Hotels, sharing the navy-and-gold visual language of the rate-management grid. Rows are room types; columns are a 14-day date window. Each cell displays total allotment, rooms sold, and rooms still available — with amber and red tint warnings when availability drops below 3 or 1. Use the +/− stepper buttons on any cell to raise or lower allotment, or click the stop-sell toggle (S) to halt sales for that cell, which dims it and blocks the counter. A room-type filter at the top narrows the visible rows. The persistent summary footer always shows total available rooms, total sold, and active stop-sells across all visible cells.