Hotel Housekeeping Status Grid
A full-screen floors-by-rooms grid of room tiles colour-coded by housekeeping status (Clean · Dirty · In Progress · Inspected · Out of Order). Click a tile to cycle its status, filter by status type, and watch live tallies update in the summary bar.
MCP
コード
/* ── 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);
/* ── Status colours ── */
--c-clean: #4a7752;
--c-clean-bg: rgba(74, 119, 82, 0.12);
--c-dirty: #b34232;
--c-dirty-bg: rgba(179, 66, 50, 0.12);
--c-inprogress: #d99020;
--c-inprogress-bg: rgba(217, 144, 32, 0.14);
--c-inspected: #4a6da0;
--c-inspected-bg: rgba(74, 109, 160, 0.14);
--c-outoforder: #6c7280;
--c-outoforder-bg: rgba(108, 114, 128, 0.14);
}
*,
*::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 ─────────────────────────────────────────────────────────────────── */
.app {
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.1rem;
font-weight: 700;
letter-spacing: 0.01em;
}
.brand-prop {
font-size: 0.7rem;
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.86rem;
font-weight: 500;
color: rgba(251, 248, 242, 0.78);
text-decoration: none;
padding: 10px 12px;
border-radius: var(--r-md);
display: flex;
align-items: center;
gap: 10px;
}
.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);
}
.nav-item:not(.is-active) .nav-dot {
display: none;
}
/* ── Legend ─────────────────────────────────────────────────────────────────── */
.legend {
padding: 16px 0 10px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 8px;
}
.legend-title {
font-size: 0.66rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-light);
margin-bottom: 10px;
padding: 0 4px;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.8rem;
color: rgba(251, 248, 242, 0.82);
padding: 5px 4px;
}
.lswatch {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.lswatch.clean {
background: var(--c-clean);
}
.lswatch.dirty {
background: var(--c-dirty);
}
.lswatch.inprogress {
background: var(--c-inprogress);
}
.lswatch.inspected {
background: var(--c-inspected);
}
.lswatch.outoforder {
background: var(--c-outoforder);
}
.rail-foot {
display: flex;
flex-direction: column;
gap: 2px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.08);
margin-top: 12px;
}
.clock {
font-family: var(--font-mono);
font-weight: 600;
font-size: 0.88rem;
color: var(--gold-light);
}
.agent {
font-size: 0.72rem;
color: rgba(251, 248, 242, 0.6);
letter-spacing: 0.04em;
}
/* ── Main ───────────────────────────────────────────────────────────────────── */
.main {
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--cream);
}
.topbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
padding: 22px 24px 16px;
border-bottom: 1px solid var(--line);
background: var(--cream);
flex-shrink: 0;
}
.kicker {
font-size: 0.7rem;
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.9rem;
letter-spacing: -0.005em;
margin-top: 2px;
}
.topbar h1 span {
color: var(--navy-2);
font-weight: 500;
}
.topbar-right {
display: flex;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 5px;
flex-wrap: wrap;
}
.filter-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--warm-gray);
text-transform: uppercase;
letter-spacing: 0.1em;
margin-right: 2px;
}
.chip {
background: transparent;
border: 1px solid var(--line-strong);
font-family: inherit;
font-size: 0.74rem;
font-weight: 600;
color: var(--ink-2);
padding: 5px 10px;
border-radius: 999px;
cursor: pointer;
transition: background 0.1s, color 0.1s, border-color 0.1s;
}
.chip:hover {
background: var(--cream-2);
}
.chip.is-active {
background: var(--gold);
color: var(--navy-d);
border-color: var(--gold);
}
/* ── Tally bar ──────────────────────────────────────────────────────────────── */
.tally-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: var(--bone);
border-bottom: 1px solid var(--line);
flex-shrink: 0;
flex-wrap: wrap;
}
.tally {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
padding: 5px 12px;
border-radius: 999px;
font-variant-numeric: tabular-nums;
}
.tally.clean {
background: var(--c-clean-bg);
color: var(--c-clean);
}
.tally.dirty {
background: var(--c-dirty-bg);
color: var(--c-dirty);
}
.tally.inprogress {
background: var(--c-inprogress-bg);
color: var(--c-inprogress);
}
.tally.inspected {
background: var(--c-inspected-bg);
color: var(--c-inspected);
}
.tally.outoforder {
background: var(--c-outoforder-bg);
color: var(--c-outoforder);
}
.tally-total {
font-size: 0.74rem;
color: var(--warm-gray);
font-weight: 600;
margin-left: auto;
}
/* ── Grid area ──────────────────────────────────────────────────────────────── */
.grid-area {
flex: 1;
overflow-y: auto;
padding: 18px 24px 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.floor-block {
}
.floor-label {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--warm-gray);
margin-bottom: 8px;
}
.floor-rooms {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
gap: 8px;
}
/* ── Room tile ──────────────────────────────────────────────────────────────── */
.tile {
background: var(--bone);
border: 1.5px solid transparent;
border-radius: var(--r-md);
padding: 10px 8px 8px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px;
transition: transform 0.1s, box-shadow 0.1s, border-color 0.12s;
position: relative;
min-height: 82px;
}
.tile:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-1);
}
.tile.clean {
background: var(--c-clean-bg);
border-color: rgba(74, 119, 82, 0.3);
}
.tile.dirty {
background: var(--c-dirty-bg);
border-color: rgba(179, 66, 50, 0.3);
}
.tile.inprogress {
background: var(--c-inprogress-bg);
border-color: rgba(217, 144, 32, 0.35);
}
.tile.inspected {
background: var(--c-inspected-bg);
border-color: rgba(74, 109, 160, 0.3);
}
.tile.outoforder {
background: var(--c-outoforder-bg);
border-color: rgba(108, 114, 128, 0.3);
}
.tile-number {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1rem;
color: var(--navy-d);
line-height: 1;
}
.tile-status {
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1.1;
}
.tile.clean .tile-status {
color: var(--c-clean);
}
.tile.dirty .tile-status {
color: var(--c-dirty);
}
.tile.inprogress .tile-status {
color: var(--c-inprogress);
}
.tile.inspected .tile-status {
color: var(--c-inspected);
}
.tile.outoforder .tile-status {
color: var(--c-outoforder);
}
.tile-occ {
font-size: 0.62rem;
color: var(--warm-gray);
font-weight: 500;
text-transform: capitalize;
}
.tile-note-dot {
position: absolute;
top: 6px;
right: 6px;
width: 7px;
height: 7px;
border-radius: 999px;
background: var(--gold);
}
/* dim tiles not matching filter */
.tile.dimmed {
opacity: 0.28;
pointer-events: none;
}
/* ── Modal ───────────────────────────────────────────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 29, 54, 0.5);
display: grid;
place-items: center;
z-index: 50;
padding: 20px;
}
.modal-overlay[hidden] {
display: none;
}
.modal {
background: var(--bone);
border-radius: var(--r-lg);
width: 100%;
max-width: 400px;
box-shadow: var(--shadow-2);
display: flex;
flex-direction: column;
}
.modal-head {
padding: 18px 20px 14px;
border-bottom: 1px solid var(--line);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-head h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.4rem;
}
.modal-close {
background: transparent;
border: none;
font-size: 1rem;
color: var(--warm-gray);
cursor: pointer;
padding: 4px 6px;
border-radius: var(--r-sm);
}
.modal-close:hover {
background: var(--cream-2);
color: var(--ink);
}
.modal-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-row {
display: flex;
align-items: center;
gap: 12px;
}
.modal-label {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--warm-gray);
min-width: 90px;
}
.status-pill {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
}
.status-pill.clean {
background: var(--c-clean-bg);
color: var(--c-clean);
}
.status-pill.dirty {
background: var(--c-dirty-bg);
color: var(--c-dirty);
}
.status-pill.inprogress {
background: var(--c-inprogress-bg);
color: var(--c-inprogress);
}
.status-pill.inspected {
background: var(--c-inspected-bg);
color: var(--c-inspected);
}
.status-pill.outoforder {
background: var(--c-outoforder-bg);
color: var(--c-outoforder);
}
.modal-note {
width: 100%;
font-family: var(--font-body);
font-size: 0.84rem;
color: var(--ink);
background: var(--cream);
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
padding: 10px 12px;
resize: vertical;
outline: none;
}
.modal-note:focus {
border-color: var(--navy-2);
}
.modal-foot {
padding: 12px 20px 18px;
border-top: 1px solid var(--line);
display: flex;
gap: 8px;
justify-content: flex-end;
}
.modal-btn {
border: none;
font-family: inherit;
font-size: 0.84rem;
font-weight: 600;
padding: 9px 16px;
border-radius: var(--r-md);
cursor: pointer;
}
.modal-btn.advance {
background: var(--navy);
color: var(--bone);
}
.modal-btn.advance:hover {
background: var(--navy-d);
}
.modal-btn.save {
background: var(--gold);
color: var(--navy-d);
}
.modal-btn.save:hover {
background: var(--gold-light);
}
/* ── Toast ───────────────────────────────────────────────────────────────────── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--navy-d);
color: var(--bone);
padding: 11px 22px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 600;
box-shadow: var(--shadow-2);
z-index: 100;
white-space: nowrap;
}
/* ── Responsive ─────────────────────────────────────────────────────────────── */
@media (max-width: 960px) {
html,
body {
overflow: auto;
}
.app {
grid-template-columns: 1fr;
height: auto;
}
.rail {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 14px 16px;
}
.nav {
display: none;
}
.legend {
display: none;
}
.rail-foot {
display: none;
}
.main {
overflow: visible;
}
.grid-area {
overflow: visible;
}
.topbar {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 560px) {
.filter-group {
gap: 4px;
}
.chip {
font-size: 0.7rem;
padding: 4px 8px;
}
.floor-rooms {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
}
}// ── Constants ─────────────────────────────────────────────────────────────────
const STATUSES = ["clean", "dirty", "inprogress", "inspected", "outoforder"];
const STATUS_LABELS = {
clean: "Clean",
dirty: "Dirty",
inprogress: "In Progress",
inspected: "Inspected",
outoforder: "Out of Order",
};
const OCC_TYPES = ["stayover", "departure", "arrival", "vacant"];
// ── Mock room data ────────────────────────────────────────────────────────────
// 4 floors, 12 rooms each (101–112, 201–212, 301–312, 401–412)
function buildRooms() {
const seed = [
// Floor 1
{ n: "101", st: "dirty", occ: "departure" },
{ n: "102", st: "inprogress", occ: "departure" },
{ n: "103", st: "clean", occ: "arrival" },
{ n: "104", st: "clean", occ: "arrival" },
{ n: "105", st: "inspected", occ: "arrival" },
{ n: "106", st: "dirty", occ: "stayover" },
{ n: "107", st: "inprogress", occ: "stayover" },
{ n: "108", st: "clean", occ: "stayover" },
{ n: "109", st: "inspected", occ: "stayover" },
{ n: "110", st: "dirty", occ: "departure" },
{ n: "111", st: "outoforder", occ: "vacant" },
{ n: "112", st: "clean", occ: "arrival" },
// Floor 2
{ n: "201", st: "inspected", occ: "arrival" },
{ n: "202", st: "dirty", occ: "stayover" },
{ n: "203", st: "dirty", occ: "departure" },
{ n: "204", st: "clean", occ: "stayover" },
{ n: "205", st: "inprogress", occ: "stayover" },
{ n: "206", st: "clean", occ: "arrival" },
{ n: "207", st: "dirty", occ: "departure" },
{ n: "208", st: "clean", occ: "arrival" },
{ n: "209", st: "outoforder", occ: "vacant" },
{ n: "210", st: "inspected", occ: "stayover" },
{ n: "211", st: "clean", occ: "stayover" },
{ n: "212", st: "dirty", occ: "departure" },
// Floor 3
{ n: "301", st: "clean", occ: "stayover" },
{ n: "302", st: "dirty", occ: "departure" },
{ n: "303", st: "inprogress", occ: "departure" },
{ n: "304", st: "clean", occ: "arrival" },
{ n: "305", st: "inspected", occ: "arrival" },
{ n: "306", st: "dirty", occ: "stayover" },
{ n: "307", st: "clean", occ: "stayover" },
{ n: "308", st: "clean", occ: "arrival" },
{ n: "309", st: "dirty", occ: "departure" },
{ n: "310", st: "outoforder", occ: "vacant" },
{ n: "311", st: "clean", occ: "stayover" },
{ n: "312", st: "inspected", occ: "stayover" },
// Floor 4
{ n: "401", st: "dirty", occ: "departure" },
{ n: "402", st: "dirty", occ: "stayover" },
{ n: "403", st: "clean", occ: "arrival" },
{ n: "404", st: "inprogress", occ: "stayover" },
{ n: "405", st: "clean", occ: "stayover" },
{ n: "406", st: "inspected", occ: "arrival" },
{ n: "407", st: "dirty", occ: "departure" },
{ n: "408", st: "clean", occ: "arrival" },
{ n: "409", st: "clean", occ: "stayover" },
{ n: "410", st: "outoforder", occ: "vacant" },
{ n: "411", st: "dirty", occ: "departure" },
{ n: "412", st: "inspected", occ: "arrival" },
];
return seed.map((r, i) => ({ ...r, id: i, note: "" }));
}
let rooms = buildRooms();
let activeFilter = "all";
let openRoomId = null;
// ── 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);
}
// ── Tally computation ─────────────────────────────────────────────────────────
function computeTallies() {
const t = {};
STATUSES.forEach((s) => (t[s] = 0));
rooms.forEach((r) => t[r.st]++);
return t;
}
// ── Render tally bar ──────────────────────────────────────────────────────────
function renderTally() {
const t = computeTallies();
const total = rooms.length;
document.getElementById("tallyBar").innerHTML =
STATUSES.map(
(s) => `
<span class="tally ${s}">${STATUS_LABELS[s]} ${t[s]}</span>
`
).join("") + `<span class="tally-total">${total} rooms total</span>`;
}
// ── Render grid ───────────────────────────────────────────────────────────────
function renderGrid() {
const floors = [1, 2, 3, 4];
const gridArea = document.getElementById("gridArea");
gridArea.innerHTML = floors
.map((f) => {
const floorRooms = rooms.filter((r) => r.n.startsWith(String(f)));
return `
<div class="floor-block">
<p class="floor-label">Floor ${f}</p>
<div class="floor-rooms">
${floorRooms
.map((r) => {
const dimmed = activeFilter !== "all" && r.st !== activeFilter;
return `
<div class="tile ${r.st}${dimmed ? " dimmed" : ""}" data-id="${r.id}">
<div class="tile-number">${r.n}</div>
<div class="tile-status">${STATUS_LABELS[r.st]}</div>
<div class="tile-occ">${r.occ}</div>
${r.note ? `<span class="tile-note-dot"></span>` : ""}
</div>`;
})
.join("")}
</div>
</div>`;
})
.join("");
}
// ── Open room modal ───────────────────────────────────────────────────────────
function openModal(roomId) {
openRoomId = roomId;
const r = rooms[roomId];
document.getElementById("modalTitle").textContent = `Room ${r.n}`;
const pill = document.getElementById("modalStatus");
pill.textContent = STATUS_LABELS[r.st];
pill.className = `status-pill ${r.st}`;
document.getElementById("modalOcc").textContent = r.occ.charAt(0).toUpperCase() + r.occ.slice(1);
document.getElementById("modalNote").value = r.note;
document.getElementById("modalOverlay").hidden = false;
}
function closeModal() {
document.getElementById("modalOverlay").hidden = true;
openRoomId = null;
}
// ── Grid click ────────────────────────────────────────────────────────────────
document.getElementById("gridArea").addEventListener("click", (e) => {
const tile = e.target.closest(".tile");
if (!tile) return;
const id = parseInt(tile.dataset.id, 10);
openModal(id);
});
// ── Modal: advance status ─────────────────────────────────────────────────────
document.getElementById("modalAdvance").addEventListener("click", () => {
if (openRoomId === null) return;
const r = rooms[openRoomId];
const idx = STATUSES.indexOf(r.st);
const next = STATUSES[(idx + 1) % STATUSES.length];
r.st = next;
showToast(`Room ${r.n} → ${STATUS_LABELS[next]}`);
renderTally();
renderGrid();
openModal(openRoomId); // refresh modal display
});
// ── Modal: save note ──────────────────────────────────────────────────────────
document.getElementById("modalSave").addEventListener("click", () => {
if (openRoomId === null) return;
const r = rooms[openRoomId];
r.note = document.getElementById("modalNote").value.trim();
showToast(`Note saved — room ${r.n}`);
renderGrid();
closeModal();
});
// ── Modal close ───────────────────────────────────────────────────────────────
document.getElementById("modalClose").addEventListener("click", closeModal);
document.getElementById("modalOverlay").addEventListener("click", (e) => {
if (e.target === e.currentTarget) closeModal();
});
// ── Filter chips ──────────────────────────────────────────────────────────────
document.querySelectorAll(".chip[data-st]").forEach((btn) => {
btn.addEventListener("click", () => {
document.querySelectorAll(".chip[data-st]").forEach((b) => b.classList.remove("is-active"));
btn.classList.add("is-active");
activeFilter = btn.dataset.st;
renderGrid();
});
});
// ── Clock + date ──────────────────────────────────────────────────────────────
const clockEl = document.getElementById("clock");
const todayEl = document.getElementById("todayLabel");
function tick() {
const now = new Date();
clockEl.textContent = now.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
todayEl.textContent = now.toLocaleDateString("en-GB", {
weekday: "long",
day: "numeric",
month: "long",
});
}
tick();
setInterval(tick, 1000);
// ── Initial render ────────────────────────────────────────────────────────────
renderTally();
renderGrid();<!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>Housekeeping Grid · Aurelia Hotels</title>
</head>
<body>
<div class="app">
<!-- ── 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 · Paris</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="#">Concierge</a>
<a class="nav-item is-active" href="#"><span class="nav-dot"></span>Housekeeping</a>
<a class="nav-item" href="#">Maintenance</a>
<a class="nav-item" href="#">Reports</a>
</nav>
<!-- ── Legend ── -->
<div class="legend">
<p class="legend-title">Status legend</p>
<div class="legend-item"><span class="lswatch clean"></span>Clean</div>
<div class="legend-item"><span class="lswatch dirty"></span>Dirty</div>
<div class="legend-item"><span class="lswatch inprogress"></span>In Progress</div>
<div class="legend-item"><span class="lswatch inspected"></span>Inspected</div>
<div class="legend-item"><span class="lswatch outoforder"></span>Out of Order</div>
</div>
<footer class="rail-foot">
<span class="clock" id="clock">--:--</span>
<span class="agent">Housekeeping · Inês</span>
</footer>
</aside>
<!-- ── Main ── -->
<section class="main">
<header class="topbar">
<div>
<p class="kicker">Housekeeping Operations</p>
<h1>Room Status Grid · <span id="todayLabel">—</span></h1>
</div>
<div class="topbar-right">
<div class="filter-group">
<span class="filter-label">Filter</span>
<button class="chip is-active" data-st="all">All</button>
<button class="chip" data-st="clean">Clean</button>
<button class="chip" data-st="dirty">Dirty</button>
<button class="chip" data-st="inprogress">In Progress</button>
<button class="chip" data-st="inspected">Inspected</button>
<button class="chip" data-st="outoforder">OOO</button>
</div>
</div>
</header>
<!-- ── Tally bar ── -->
<div class="tally-bar" id="tallyBar">
<!-- rendered by JS -->
</div>
<!-- ── Grid area ── -->
<div class="grid-area" id="gridArea">
<!-- rendered by JS -->
</div>
</section>
</div>
<!-- ── Assignment modal ── -->
<div class="modal-overlay" id="modalOverlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<div class="modal-head">
<h2 id="modalTitle">Room —</h2>
<button class="modal-close" id="modalClose" aria-label="Close">✕</button>
</div>
<div class="modal-body">
<div class="modal-row">
<span class="modal-label">Status</span>
<span id="modalStatus" class="status-pill"></span>
</div>
<div class="modal-row">
<span class="modal-label">Occupancy</span>
<span id="modalOcc"></span>
</div>
<div class="modal-row" style="flex-direction:column;gap:6px;align-items:flex-start">
<span class="modal-label">Assignment note</span>
<textarea id="modalNote" class="modal-note" placeholder="Add note for housekeeping staff…" rows="3"></textarea>
</div>
</div>
<div class="modal-foot">
<button class="modal-btn advance" id="modalAdvance">→ Advance status</button>
<button class="modal-btn save" id="modalSave">Save note</button>
</div>
</div>
</div>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Hotel Housekeeping Status Grid
A full-screen housekeeping management grid for Aurelia Hotels showing all rooms arranged by floor. Each room tile displays the room number, current housekeeping status (Clean, Dirty, In Progress, Inspected, Out of Order), and occupancy type (Stayover, Departure, Arrival). Clicking a tile cycles its status through the workflow sequence; the summary counter bar at the top updates live with colour-coded tallies. Filter the grid to focus on any single status, and a collapsible assignment note lets supervisors annotate tiles for staff.