페이지 Medium
Hotel PMS — Night Audit Report
End-of-day audit screen showing occupancy, revenue by department, no-shows, posting summary and discrepancies — with a Run audit pipeline that posts room charges and rolls business date.
Lab에서 열기
MCP
html css vanilla-js
Targets: JS HTML
Code
: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 10px 30px rgba(22, 30, 44, 0.14);
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
background: var(--cream);
color: var(--ink);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
.audit {
max-width: 1140px;
margin: 0 auto;
padding: 28px 28px 64px;
}
/* ── Topbar ── */
.topbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
padding-bottom: 18px;
margin-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.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: 2.1rem;
letter-spacing: -0.005em;
margin-top: 2px;
}
.topbar h1 span {
color: var(--navy-2);
font-variant-numeric: tabular-nums;
}
.topbar .sub {
font-size: 0.84rem;
color: var(--warm-gray);
margin-top: 6px;
}
.topbar .sub strong {
color: var(--ink-2);
font-weight: 600;
}
.topactions {
display: flex;
gap: 10px;
}
.ghost {
background: var(--bone);
border: 1px solid var(--line-strong);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
padding: 9px 14px;
border-radius: var(--r-sm);
cursor: pointer;
}
.ghost:hover {
border-color: var(--navy-2);
color: var(--navy-d);
background: var(--cream-2);
}
/* ── KPI band ── */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 22px;
}
.kpi {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px;
position: relative;
overflow: hidden;
}
.kpi::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--gold);
}
.kpi-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--warm-gray);
font-weight: 600;
}
.kpi-val {
font-family: var(--font-display);
font-weight: 700;
font-size: 2rem;
color: var(--navy-d);
margin: 4px 0 2px;
letter-spacing: -0.01em;
font-variant-numeric: tabular-nums;
}
.kpi-val small {
font-size: 1.05rem;
font-weight: 600;
color: var(--warm-gray);
margin-left: 1px;
}
.kpi-meta {
font-size: 0.74rem;
color: var(--warm-gray);
}
/* ── Grid of cards ── */
.grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 22px;
}
.card {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 4px 0 8px;
display: flex;
flex-direction: column;
}
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 18px 12px;
border-bottom: 1px solid var(--line);
gap: 12px;
}
.card-head h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.3rem;
letter-spacing: -0.005em;
}
.card-meta {
font-size: 0.74rem;
color: var(--warm-gray);
font-weight: 600;
}
.pill {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
}
.pill.warn {
background: rgba(217, 144, 32, 0.16);
color: var(--warning);
}
.pill.ok {
background: rgba(74, 119, 82, 0.14);
color: var(--success);
}
/* ── Revenue by department ── */
.dept {
list-style: none;
padding: 8px 18px 6px;
}
.dept li {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 10px 14px;
padding: 9px 0;
border-bottom: 1px dashed var(--line);
}
.dept li:last-child {
border-bottom: none;
}
.dept .d-name {
font-size: 0.86rem;
font-weight: 600;
color: var(--ink);
}
.dept .d-amt {
font-family: var(--font-mono);
font-size: 0.84rem;
font-weight: 700;
color: var(--navy-d);
text-align: right;
font-variant-numeric: tabular-nums;
}
.dept .d-bar {
grid-column: 1 / -1;
height: 5px;
background: var(--cream-2);
border-radius: 999px;
overflow: hidden;
}
.dept .d-bar span {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--gold-d), var(--gold-light));
border-radius: 999px;
width: 0;
transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ── Postings list ── */
.posts {
list-style: none;
padding: 8px 18px 6px;
}
.posts li {
display: grid;
grid-template-columns: 1fr auto;
gap: 2px 14px;
padding: 11px 0;
border-bottom: 1px dashed var(--line);
}
.posts li:last-child {
border-bottom: none;
}
.posts strong {
font-size: 0.88rem;
font-weight: 600;
color: var(--ink);
}
.posts .num {
font-family: var(--font-mono);
font-size: 0.86rem;
font-weight: 700;
color: var(--navy-d);
text-align: right;
font-variant-numeric: tabular-nums;
}
.posts small {
font-size: 0.72rem;
color: var(--warm-gray);
grid-column: 1 / -1;
}
/* ── Discrepancies ── */
.discr {
list-style: none;
padding: 8px 18px 6px;
}
.discr li {
display: grid;
grid-template-columns: 6px 1fr auto;
align-items: center;
gap: 4px 12px;
padding: 12px 0 12px 0;
border-bottom: 1px dashed var(--line);
position: relative;
}
.discr li::before {
content: "";
width: 6px;
align-self: stretch;
border-radius: 999px;
background: var(--warm-gray);
}
.discr li.warn::before {
background: var(--warning);
}
.discr li.info::before {
background: var(--info);
}
.discr li.ok::before {
background: var(--success);
}
.discr li:last-child {
border-bottom: none;
}
.discr strong {
font-size: 0.88rem;
font-weight: 600;
color: var(--ink);
}
.discr small {
grid-column: 2 / 3;
font-size: 0.74rem;
color: var(--warm-gray);
margin-top: 2px;
}
.discr li.is-resolved {
opacity: 0.55;
}
.discr li.is-resolved strong {
text-decoration: line-through;
text-decoration-color: var(--line-strong);
}
.mini {
background: var(--navy);
color: var(--bone);
border: 1px solid var(--navy);
font-family: inherit;
font-size: 0.74rem;
font-weight: 600;
padding: 6px 12px;
border-radius: var(--r-sm);
cursor: pointer;
grid-row: 1 / 3;
grid-column: 3;
white-space: nowrap;
}
.mini:hover {
background: var(--navy-d);
}
.mini:disabled {
background: var(--cream-2);
color: var(--warm-gray);
border-color: var(--line);
cursor: default;
}
/* ── System checks ── */
.checks {
list-style: none;
padding: 8px 18px 6px;
}
.checks li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
font-size: 0.86rem;
color: var(--ink-2);
border-bottom: 1px dashed var(--line);
}
.checks li:last-child {
border-bottom: none;
}
.dot {
width: 9px;
height: 9px;
border-radius: 999px;
flex-shrink: 0;
background: var(--warm-gray);
}
.dot.ok {
background: var(--success);
box-shadow: 0 0 0 3px rgba(74, 119, 82, 0.16);
}
.dot.warn {
background: var(--warning);
box-shadow: 0 0 0 3px rgba(217, 144, 32, 0.18);
}
/* ── Run box ── */
.runbox {
background: var(--navy);
color: var(--bone);
border-radius: var(--r-lg);
padding: 22px 24px 24px;
box-shadow: var(--shadow-2);
}
.runbox > header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 16px;
}
.runbox .kicker {
color: var(--gold-light);
}
.runbox h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.7rem;
margin-top: 2px;
}
.runbox .sub {
font-size: 0.84rem;
color: rgba(251, 248, 242, 0.72);
margin-top: 6px;
}
.runbox .sub strong {
color: var(--gold-light);
font-weight: 600;
}
.run {
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-weight: 700;
font-size: 0.95rem;
padding: 13px 22px;
border-radius: var(--r-md);
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.run:hover:not(:disabled) {
background: var(--gold-light);
}
.run:disabled {
opacity: 0.55;
cursor: default;
}
.pipeline {
list-style: none;
display: flex;
flex-direction: column;
gap: 4px;
}
.pipeline li {
display: grid;
grid-template-columns: 36px 1fr auto;
align-items: center;
gap: 14px;
padding: 14px 16px;
border-radius: var(--r-md);
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
transition: background 0.2s, border-color 0.2s;
}
.pipeline .stg {
width: 36px;
height: 36px;
display: grid;
place-items: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: var(--gold-light);
font-weight: 700;
font-size: 1rem;
}
.pipeline strong {
font-size: 0.92rem;
font-weight: 600;
color: var(--bone);
}
.pipeline small {
display: block;
font-size: 0.74rem;
color: rgba(251, 248, 242, 0.6);
margin-top: 2px;
}
.pipeline .state {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(251, 248, 242, 0.55);
white-space: nowrap;
}
.pipeline li.is-running {
background: rgba(201, 166, 73, 0.12);
border-color: rgba(201, 166, 73, 0.4);
}
.pipeline li.is-running .stg {
background: var(--gold);
color: var(--navy-d);
animation: pulse 1s ease-in-out infinite;
}
.pipeline li.is-running .state {
color: var(--gold-light);
}
.pipeline li.is-done {
background: rgba(74, 119, 82, 0.14);
border-color: rgba(74, 119, 82, 0.34);
}
.pipeline li.is-done .stg {
background: var(--success);
color: var(--bone);
font-size: 1.05rem;
}
.pipeline li.is-done .state {
color: #8fc99a;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
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);
z-index: 20;
}
/* ── Responsive ── */
@media (max-width: 900px) {
.kpis {
grid-template-columns: repeat(2, 1fr);
}
.grid {
grid-template-columns: 1fr;
}
.runbox > header {
flex-direction: column;
align-items: flex-start;
}
.run {
width: 100%;
}
}
@media (max-width: 520px) {
.kpis {
grid-template-columns: 1fr;
}
.topbar {
flex-direction: column;
align-items: flex-start;
}
}// ── Revenue by department (sums to €31,420 total revenue KPI) ─────────────────
const DEPTS = [
{ name: "Rooms", amount: 24288 },
{ name: "F&B · Restaurant", amount: 4182 },
{ name: "Spa & Wellness", amount: 1476 },
{ name: "Bar & Lounge", amount: 892 },
{ name: "Minibar", amount: 344 },
{ name: "Parking", amount: 210 },
{ name: "Laundry", amount: 28 },
];
const eur = (n) =>
"€" + n.toLocaleString("en-GB", { minimumFractionDigits: 0, maximumFractionDigits: 0 });
// ── Render departments ────────────────────────────────────────────────────────
const dept = document.getElementById("dept");
const maxDept = Math.max(...DEPTS.map((d) => d.amount));
dept.innerHTML = DEPTS.map(
(d) => `
<li>
<span class="d-name">${d.name}</span>
<span class="d-amt">${eur(d.amount)}</span>
<span class="d-bar"><span data-pct="${(d.amount / maxDept) * 100}"></span></span>
</li>`
).join("");
// Animate bars in after first paint
requestAnimationFrame(() => {
requestAnimationFrame(() => {
dept.querySelectorAll(".d-bar > span").forEach((bar) => {
bar.style.width = bar.dataset.pct + "%";
});
});
});
// ── Toast ─────────────────────────────────────────────────────────────────────
const toast = document.getElementById("toast");
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 1800);
}
// ── Discrepancy actions ───────────────────────────────────────────────────────
const discr = document.getElementById("discr");
const discN = document.getElementById("discN");
function updateDiscCount() {
const open = discr.querySelectorAll(
"li.warn:not(.is-resolved), li.info:not(.is-resolved)"
).length;
if (open === 0) {
discN.textContent = "All clear";
discN.classList.remove("warn");
discN.classList.add("ok");
} else {
discN.textContent = `${open} to review`;
}
}
discr.addEventListener("click", (e) => {
const btn = e.target.closest("button.mini");
if (!btn) return;
const li = btn.closest("li");
const label = li.querySelector("strong").textContent;
const verb = btn.textContent.trim();
li.classList.add("is-resolved");
btn.disabled = true;
btn.textContent = "Done";
showToast(`${verb} · ${label}`);
updateDiscCount();
});
// ── Topbar ghost actions ──────────────────────────────────────────────────────
document
.querySelectorAll(".topactions .ghost")
.forEach((b) => b.addEventListener("click", () => showToast(b.textContent.trim())));
// ── Run audit pipeline ────────────────────────────────────────────────────────
const runBtn = document.getElementById("run");
const stages = Array.from(document.querySelectorAll("#pipeline li"));
const STAGE_LABELS = ["①", "②", "③", "④"];
let auditDone = false;
function runStage(i) {
if (i >= stages.length) {
finishAudit();
return;
}
const li = stages[i];
li.classList.add("is-running");
li.querySelector(".state").textContent = "running…";
setTimeout(() => {
li.classList.remove("is-running");
li.classList.add("is-done");
li.querySelector(".stg").textContent = "✓";
li.querySelector(".state").textContent = "posted";
runStage(i + 1);
}, 900);
}
function finishAudit() {
auditDone = true;
// Roll the business date displayed in the header to the next day.
const bd = document.getElementById("bd");
const nextBd = document.getElementById("nextBd");
if (bd && nextBd) bd.textContent = nextBd.textContent;
runBtn.textContent = "Audit complete ✓";
showToast("Night audit complete · business date rolled");
}
runBtn.addEventListener("click", () => {
if (auditDone) return;
runBtn.disabled = true;
runBtn.textContent = "Running audit…";
// Reset any prior state, then run.
stages.forEach((li, i) => {
li.classList.remove("is-running", "is-done");
li.querySelector(".stg").textContent = STAGE_LABELS[i];
li.querySelector(".state").textContent = "queued";
});
runStage(0);
});
updateDiscCount();<!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>Night Audit · Aurelia Hotels</title>
</head>
<body>
<main class="audit">
<header class="topbar">
<div>
<p class="kicker">Night audit · Aurelia · Madrid</p>
<h1>Audit · <span id="bd">Sat 24 May 2026</span></h1>
<p class="sub">Auditor on duty · <strong>Sergio Méndez</strong> · started <span id="started">23:42</span></p>
</div>
<div class="topactions">
<button class="ghost">Export PDF</button>
<button class="ghost">Print summary</button>
</div>
</header>
<section class="kpis">
<article class="kpi">
<p class="kpi-label">Occupancy</p>
<p class="kpi-val">82<small>%</small></p>
<p class="kpi-meta">132 / 160 rooms sold</p>
</article>
<article class="kpi">
<p class="kpi-label">ADR</p>
<p class="kpi-val">€184</p>
<p class="kpi-meta">avg rate, sold rooms</p>
</article>
<article class="kpi">
<p class="kpi-label">RevPAR</p>
<p class="kpi-val">€151</p>
<p class="kpi-meta">revenue / available rm</p>
</article>
<article class="kpi">
<p class="kpi-label">Total revenue</p>
<p class="kpi-val">€31,420</p>
<p class="kpi-meta">net of refunds</p>
</article>
</section>
<section class="grid">
<article class="card">
<header class="card-head">
<h2>Revenue by department</h2>
<span class="card-meta">7 sources</span>
</header>
<ul class="dept" id="dept"></ul>
</article>
<article class="card">
<header class="card-head">
<h2>Postings to apply</h2>
<span class="card-meta">automatic</span>
</header>
<ul class="posts">
<li><strong>Room & tax</strong><span class="num">€24,288.00</span><small>132 rooms × ADR + VAT 10%</small></li>
<li><strong>City tax</strong><span class="num">€218.40</span><small>per-guest, per-night</small></li>
<li><strong>POS imports · F&B</strong><span class="num">€4,182.10</span><small>34 tickets · auto-charged to rooms</small></li>
<li><strong>POS imports · Spa</strong><span class="num">€1,476.00</span><small>8 tickets</small></li>
<li><strong>Adjustments</strong><span class="num">−€144.50</span><small>1 manual void · 2 comps</small></li>
</ul>
</article>
<article class="card">
<header class="card-head">
<h2>Discrepancies</h2>
<span class="pill warn" id="discN">3 to review</span>
</header>
<ul class="discr" id="discr">
<li class="warn">
<strong>No-show — 118</strong><small>Thomas Reuter · 1 night · charge no-show fee €92</small>
<button class="mini">Charge</button>
</li>
<li class="warn">
<strong>Open folio — 207</strong><small>Elena Vasquez · departed but balance €482 still open</small>
<button class="mini">Open</button>
</li>
<li class="info">
<strong>Cash overage — Front desk drawer 1</strong><small>+€2.40 reported · post to overages account</small>
<button class="mini">Post</button>
</li>
<li class="ok">
<strong>All other folios balanced</strong><small>129 in-house folios reconciled · no variance</small>
</li>
</ul>
</article>
<article class="card">
<header class="card-head">
<h2>System checks</h2>
<span class="pill ok">All green</span>
</header>
<ul class="checks">
<li><span class="dot ok"></span>Channel manager sync up to date</li>
<li><span class="dot ok"></span>Payment gateway · 0 unsettled charges</li>
<li><span class="dot ok"></span>Backup · last 21:30 · 100% complete</li>
<li><span class="dot warn"></span>POS terminal 4 · last ping 22:14 (recheck recommended)</li>
<li><span class="dot ok"></span>Door lock system · 0 alarms</li>
</ul>
</article>
</section>
<section class="runbox" id="runbox">
<header>
<div>
<p class="kicker">Final step</p>
<h2>Run night audit</h2>
<p class="sub">Posts pending charges, closes cash drawers and rolls business date to <strong id="nextBd">Sun 25 May 2026</strong>.</p>
</div>
<button class="run" id="run">Run audit ▸</button>
</header>
<ol class="pipeline" id="pipeline">
<li data-stage="0">
<span class="stg">①</span>
<div><strong>Post room & tax charges</strong><small>Apply nightly room rate and VAT to in-house folios</small></div>
<span class="state">queued</span>
</li>
<li data-stage="1">
<span class="stg">②</span>
<div><strong>Apply discrepancy actions</strong><small>No-show fee · adjust folios · post cash variance</small></div>
<span class="state">queued</span>
</li>
<li data-stage="2">
<span class="stg">③</span>
<div><strong>Close cash drawers & deposits</strong><small>Settle floor floats and post deposit to safe</small></div>
<span class="state">queued</span>
</li>
<li data-stage="3">
<span class="stg">④</span>
<div><strong>Roll business date & lock</strong><small>Finalise day · open the next business date</small></div>
<span class="state">queued</span>
</li>
</ol>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Night Audit Report
The screen the night auditor opens between 23:00 and 02:00. KPI band shows occupancy / ADR / RevPAR / revenue. Sections cover: posting summary (room charges to post · taxes · POS imports), discrepancies (mismatched folios, missing keys, no-shows), and a final Run audit sequence that animates through stages (post charges → close cash drawers → roll business date) and yields a finalised audit report.