Strony Średni
Hotel PMS — Check-out & Folio Settlement
Front-desk check-out screen — itemised folio (room, F&B, mini-bar, spa, city tax), split / transfer charges, settle balance with multiple methods, generate invoice and release the room.
Otwórz w Lab
MCP
html css vanilla-js
Targety: JS HTML
Kod
: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.2);
--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;
}
*,
*::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;
min-height: 100vh;
}
.co {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.topbar {
background: var(--bone);
border-bottom: 1px solid var(--line);
padding: 22px 32px;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 24px;
}
.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: 2rem;
letter-spacing: -0.005em;
margin-top: 2px;
}
.topbar h1 span {
font-family: var(--font-mono);
font-weight: 700;
color: var(--navy-2);
font-size: 1.7rem;
letter-spacing: 0;
}
.topsub {
font-size: 0.92rem;
color: var(--warm-gray);
margin-top: 4px;
}
.top-actions {
display: flex;
gap: 8px;
}
.ghost {
background: transparent;
border: 1px solid var(--line-strong);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
padding: 9px 16px;
border-radius: 999px;
cursor: pointer;
}
.ghost:hover {
background: var(--cream-2);
border-color: var(--navy-2);
color: var(--navy-d);
}
.layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 24px;
padding: 24px 32px;
align-items: start;
flex: 1;
}
/* ── Folio ── */
.folio {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
}
.folio-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 20px;
border-bottom: 1px solid var(--line);
gap: 16px;
}
.seg {
display: inline-flex;
background: var(--cream-2);
padding: 4px;
border-radius: 999px;
gap: 4px;
}
.seg.full {
display: grid;
grid-template-columns: repeat(4, 1fr);
width: 100%;
margin-bottom: 14px;
}
.seg-btn {
background: transparent;
border: none;
font-family: inherit;
font-weight: 600;
font-size: 0.82rem;
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
color: var(--ink-2);
display: inline-flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.seg-btn.is-active {
background: var(--bone);
color: var(--navy-d);
box-shadow: 0 1px 2px rgba(22, 30, 44, 0.08);
}
.pill {
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 700;
background: rgba(22, 30, 44, 0.08);
padding: 2px 8px;
border-radius: 999px;
}
.seg-btn.is-active .pill {
background: rgba(22, 30, 44, 0.12);
}
.bulk {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.82rem;
color: var(--ink-2);
font-weight: 600;
}
.bulk-btn {
background: var(--cream);
border: 1px solid var(--line-strong);
font-family: inherit;
font-size: 0.78rem;
font-weight: 600;
padding: 6px 12px;
border-radius: var(--r-sm);
cursor: pointer;
color: var(--ink-2);
}
.bulk-btn:hover {
border-color: var(--gold);
color: var(--gold-d);
}
.lines {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
.lines thead {
background: var(--cream);
}
.lines th {
text-align: left;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--warm-gray);
font-weight: 700;
padding: 11px 14px;
border-bottom: 1px solid var(--line);
}
.lines th.num {
text-align: right;
}
.lines th.ck {
width: 36px;
padding-right: 0;
}
.lines td {
padding: 12px 14px;
border-bottom: 1px solid var(--line);
color: var(--ink-2);
}
.lines td.num {
text-align: right;
font-family: var(--font-mono);
font-weight: 600;
color: var(--ink);
}
.lines tbody tr:last-child td {
border-bottom: none;
}
.lines tbody tr.is-section td {
background: var(--cream);
font-family: var(--font-display);
font-weight: 700;
color: var(--navy-d);
font-size: 1rem;
padding-top: 10px;
padding-bottom: 10px;
}
.lines tbody tr.is-voided td:not(.actions) {
text-decoration: line-through;
color: var(--warm-gray);
}
.lines tbody tr.is-transferred td:not(.actions) {
opacity: 0.6;
font-style: italic;
}
.cat-pill {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: 999px;
background: rgba(22, 30, 44, 0.06);
color: var(--ink-2);
}
.cat-pill.acc {
background: rgba(74, 109, 160, 0.14);
color: var(--info);
}
.cat-pill.fb {
background: rgba(217, 144, 32, 0.16);
color: var(--warning);
}
.cat-pill.mini {
background: rgba(193, 113, 74, 0.16);
color: #a05a38;
}
.cat-pill.spa {
background: rgba(74, 119, 82, 0.14);
color: var(--success);
}
.cat-pill.tax {
background: rgba(22, 30, 44, 0.06);
color: var(--warm-gray);
}
.row-act {
background: transparent;
border: none;
font-family: inherit;
font-size: 0.78rem;
color: var(--warm-gray);
cursor: pointer;
padding: 4px 6px;
border-radius: var(--r-sm);
}
.row-act:hover {
color: var(--danger);
background: var(--cream);
}
.lines input[type="checkbox"] {
accent-color: var(--navy);
width: 14px;
height: 14px;
}
/* ── Settle rail ── */
.settle {
background: var(--bone);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 22px;
position: sticky;
top: 24px;
}
.settle h3 {
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--gold-d);
font-weight: 700;
margin-bottom: 12px;
}
.t-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 0.86rem;
color: var(--ink-2);
border-bottom: 1px dashed var(--line);
}
.t-row:last-of-type {
border-bottom: none;
}
.t-row span:last-child {
font-family: var(--font-mono);
font-weight: 600;
color: var(--ink);
}
.t-row.paid span:last-child {
color: var(--success);
}
.t-row.balance {
border-top: 1px solid var(--line-strong);
border-bottom: none;
margin-top: 4px;
padding-top: 12px;
font-weight: 700;
font-size: 1rem;
color: var(--navy-d);
}
.t-row.balance span:last-child {
font-size: 1.3rem;
color: var(--gold-d);
font-family: var(--font-display);
}
.totals {
margin-bottom: 22px;
}
.pay {
padding-top: 18px;
border-top: 1px solid var(--line);
}
.pay-body {
background: var(--cream);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px 16px;
font-size: 0.86rem;
color: var(--ink-2);
line-height: 1.55;
margin-bottom: 16px;
}
.pay-body strong {
color: var(--navy-d);
}
.pay-body .split-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-family: var(--font-mono);
}
.settle-btn {
width: 100%;
background: var(--gold);
color: var(--navy-d);
border: none;
font-family: inherit;
font-weight: 700;
font-size: 0.95rem;
padding: 14px 18px;
border-radius: 999px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
letter-spacing: 0.01em;
}
.settle-btn:hover:not(:disabled) {
background: var(--gold-light);
}
.settle-btn:disabled {
background: var(--warm-gray);
color: var(--cream);
cursor: not-allowed;
opacity: 0.7;
}
.settle-amt {
font-family: var(--font-mono);
font-weight: 700;
background: rgba(15, 29, 54, 0.16);
padding: 4px 10px;
border-radius: 999px;
font-size: 0.86rem;
}
.result {
margin-top: 16px;
background: rgba(74, 119, 82, 0.1);
border: 1px solid rgba(74, 119, 82, 0.3);
border-radius: var(--r-md);
padding: 14px 18px;
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
align-items: center;
}
.result-mark {
width: 36px;
height: 36px;
border-radius: 999px;
background: var(--success);
color: var(--bone);
display: grid;
place-items: center;
font-weight: 700;
font-size: 1.2rem;
}
.result strong {
font-size: 0.92rem;
color: var(--success);
font-family: var(--font-display);
font-weight: 700;
display: block;
}
.result strong span {
font-family: var(--font-mono);
}
.result small {
font-size: 0.78rem;
color: var(--ink-2);
display: block;
margin-top: 2px;
}
.result small em {
color: var(--success);
font-style: normal;
font-weight: 600;
}
/* ── 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: 0 10px 30px rgba(22, 30, 44, 0.18);
}
@media (max-width: 1024px) {
.layout {
grid-template-columns: 1fr;
}
.settle {
position: static;
}
}
/* ── Honor [hidden] over display (overlay/panel toggle fix) ── */
.bulk[hidden] {
display: none;
}
.result[hidden] {
display: none;
}// ── Folio data ─────────────────────────────────────────────────────────────
const LINES = [
{ id: 1, section: "Accommodation" },
{
id: 2,
date: "20 May",
desc: "Junior Suite · Night",
cat: "acc",
catLabel: "Acc",
qty: 4,
amount: 241,
},
{ id: 3, date: "20 May", desc: "City tax", cat: "tax", catLabel: "Tax", qty: 4, amount: 4.125 },
{ id: 4, section: "Food & Beverage" },
{
id: 5,
date: "20 May",
desc: "Welcome bottle · cava",
cat: "fb",
catLabel: "F&B",
qty: 1,
amount: 22,
},
{
id: 6,
date: "21 May",
desc: "Restaurant · dinner for 2",
cat: "fb",
catLabel: "F&B",
qty: 1,
amount: 96,
},
{
id: 7,
date: "22 May",
desc: "Breakfast buffet",
cat: "fb",
catLabel: "F&B",
qty: 2,
amount: 18,
},
{
id: 8,
date: "23 May",
desc: "Room service · lunch",
cat: "fb",
catLabel: "F&B",
qty: 1,
amount: 38,
},
{ id: 9, section: "Mini-bar" },
{
id: 10,
date: "21 May",
desc: "Mini-bar · 2 × water · 1 × nuts",
cat: "mini",
catLabel: "Mini",
qty: 1,
amount: 14,
},
{
id: 11,
date: "23 May",
desc: "Mini-bar · 1 × whisky",
cat: "mini",
catLabel: "Mini",
qty: 1,
amount: 18,
},
{ id: 12, section: "Spa & Wellness" },
{
id: 13,
date: "22 May",
desc: "Spa · couples massage 60'",
cat: "spa",
catLabel: "Spa",
qty: 1,
amount: 145,
},
];
// state per line: { folio: "A"|"B", void: bool }
const state = new Map();
LINES.forEach((l) => {
if (!l.section) state.set(l.id, { folio: "A", void: false, qty: l.qty });
});
// Put one mini-bar item already on folio B (€18 + tax) to match the pill in mock.
state.get(11).folio = "B";
const PAID_DEPOSIT = 482;
const VAT_RATE = 0.1;
const CITY_TAX = 16.5;
let activeFolio = "A";
let payMethod = "card";
let settled = false;
const tbody = document.getElementById("rows");
const checkAll = document.getElementById("checkAll");
const bulk = document.getElementById("bulk");
const bulkN = document.getElementById("bulkN");
const subEl = document.getElementById("subtotal");
const taxEl = document.getElementById("tax");
const balEl = document.getElementById("balance");
const payBody = document.getElementById("payBody");
const settleAmt = document.getElementById("settleAmt");
const settleBtn = document.getElementById("settle");
const result = document.getElementById("result");
const toast = document.getElementById("toast");
const fmt = (n) =>
`€${n.toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
function visibleLines() {
return LINES.filter((l) => l.section || state.get(l.id).folio === activeFolio);
}
function lineAmount(l) {
const s = state.get(l.id);
return s.qty * l.amount;
}
function renderRows() {
tbody.innerHTML = visibleLines()
.map((l) => {
if (l.section) {
return `<tr class="is-section"><td colspan="7">${l.section}</td></tr>`;
}
const s = state.get(l.id);
const cls = s.void ? "is-voided" : "";
return `
<tr class="${cls}" data-id="${l.id}">
<td class="ck"><input type="checkbox" class="line-ck" /></td>
<td>${l.date}</td>
<td>${l.desc}</td>
<td><span class="cat-pill ${l.cat}">${l.catLabel}</span></td>
<td class="num">${s.qty}</td>
<td class="num">${fmt(lineAmount(l))}</td>
<td class="actions"><button class="row-act" title="Void">✕</button></td>
</tr>`;
})
.join("");
// Restore check state on visible rows (we re-render, so reset)
checkAll.checked = false;
updateBulk();
}
function activeLines() {
return LINES.filter(
(l) => !l.section && state.get(l.id).folio === activeFolio && !state.get(l.id).void
);
}
function totals() {
const subtotal = activeLines().reduce((sum, l) => sum + lineAmount(l), 0);
// City tax line (id 3) already in subtotal; here we model VAT as 10% of non-tax categories.
const taxable = activeLines()
.filter((l) => l.cat !== "tax")
.reduce((sum, l) => sum + lineAmount(l), 0);
const tax = taxable * VAT_RATE;
const grand = subtotal + tax; // city tax separate display
return { subtotal, tax, grand };
}
function renderTotals() {
const { subtotal, tax, grand } = totals();
subEl.textContent = fmt(subtotal);
taxEl.textContent = fmt(tax);
let balance =
subtotal + tax + (activeFolio === "A" ? 0 : 0) - (activeFolio === "A" ? PAID_DEPOSIT : 0);
if (balance < 0) balance = 0;
balEl.textContent = fmt(balance);
settleAmt.textContent = fmt(balance);
settleBtn.disabled = balance < 0.01 || settled;
}
function updateBulk() {
const checked = tbody.querySelectorAll(".line-ck:checked");
if (checked.length) {
bulk.hidden = false;
bulkN.textContent = checked.length;
} else {
bulk.hidden = true;
}
}
tbody.addEventListener("change", (e) => {
if (e.target.matches(".line-ck")) updateBulk();
});
tbody.addEventListener("click", (e) => {
if (e.target.matches(".row-act")) {
const tr = e.target.closest("tr");
const id = parseInt(tr.dataset.id, 10);
state.get(id).void = !state.get(id).void;
renderRows();
renderTotals();
showToast("Line voided");
}
});
checkAll.addEventListener("change", () => {
tbody.querySelectorAll(".line-ck").forEach((c) => (c.checked = checkAll.checked));
updateBulk();
});
document.getElementById("bulk").addEventListener("click", (e) => {
const btn = e.target.closest(".bulk-btn");
if (!btn) return;
const action = btn.dataset.bulk;
const checked = [...tbody.querySelectorAll(".line-ck:checked")].map((c) =>
parseInt(c.closest("tr").dataset.id, 10)
);
checked.forEach((id) => {
if (action === "void") state.get(id).void = true;
if (action === "transfer") state.get(id).folio = activeFolio === "A" ? "B" : "A";
});
showToast(`${checked.length} line(s) ${action === "void" ? "voided" : "transferred"}`);
renderRows();
renderTotals();
});
// Folio tabs
document.querySelectorAll(".folio-head .seg-btn").forEach((b) =>
b.addEventListener("click", () => {
if (b.dataset.fol === "C") {
showToast("New folio created (mock)");
return;
}
document
.querySelectorAll(".folio-head .seg-btn")
.forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
activeFolio = b.dataset.fol;
renderRows();
renderTotals();
})
);
// Payment seg
const PAY_TEXT = {
card: `Charge balance to card on file <strong>Visa •••• 4421</strong>. Authorisation typically clears in 4–10 seconds.`,
cash: `Collect balance in cash. Cash drawer opens after confirmation. Change calculated automatically.`,
split: `<div class="split-row"><span>Card · Visa •••• 4421</span><span><strong>€350.00</strong></span></div>
<div class="split-row"><span>Cash</span><span><strong>€68.00</strong></span></div>
<p style="margin-top:6px;font-size:.78rem;color:var(--warm-gray);">Adjust amounts before settling.</p>`,
company: `Direct bill to <strong>Innovo Travel S.L.</strong> (NIF B-3382041). Folio transferred to AR · invoice issued.`,
};
function renderPay() {
payBody.innerHTML = PAY_TEXT[payMethod];
}
renderPay();
document.getElementById("paySeg").addEventListener("click", (e) => {
const b = e.target.closest(".seg-btn");
if (!b) return;
document.querySelectorAll("#paySeg .seg-btn").forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
payMethod = b.dataset.pay;
renderPay();
});
settleBtn.addEventListener("click", () => {
settled = true;
settleBtn.disabled = true;
result.hidden = false;
result.scrollIntoView({ behavior: "smooth", block: "nearest" });
showToast("Folio settled · room released");
});
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 1600);
}
renderRows();
renderTotals();<!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>Check-out · Aurelia Hotels</title>
</head>
<body>
<main class="co">
<header class="topbar">
<div>
<p class="kicker">Front desk · Check-out</p>
<h1>Folio · Room <span id="roomNo">207</span></h1>
<p class="topsub" id="guestLabel">Elena Vasquez · Junior Suite · 20–24 May</p>
</div>
<div class="top-actions">
<button class="ghost">Print preview</button>
<button class="ghost">Email folio</button>
</div>
</header>
<section class="layout">
<section class="folio">
<header class="folio-head">
<div class="seg">
<button class="seg-btn is-active" data-fol="A">Folio A · Master</button>
<button class="seg-btn" data-fol="B">Folio B · Company <span class="pill">€216</span></button>
<button class="seg-btn" data-fol="C">+ New folio</button>
</div>
<div class="bulk" id="bulk" hidden>
<span id="bulkN">0</span> selected
<button class="bulk-btn" data-bulk="transfer">Transfer →</button>
<button class="bulk-btn" data-bulk="void">Void</button>
</div>
</header>
<table class="lines" id="lines">
<thead>
<tr>
<th class="ck"><input type="checkbox" id="checkAll" /></th>
<th>Date</th>
<th>Description</th>
<th>Category</th>
<th class="num">Qty</th>
<th class="num">Amount</th>
<th></th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
</section>
<aside class="settle">
<section class="totals">
<h3>Totals</h3>
<div class="t-row"><span>Subtotal</span><span id="subtotal">€0.00</span></div>
<div class="t-row"><span>VAT 10%</span><span id="tax">€0.00</span></div>
<div class="t-row"><span>City tax</span><span id="city">€16.50</span></div>
<div class="t-row paid"><span>Paid / deposit</span><span id="paid">−€482.00</span></div>
<div class="t-row balance"><span>Balance due</span><span id="balance">€0.00</span></div>
</section>
<section class="pay">
<h3>Settlement</h3>
<div class="seg full" id="paySeg">
<button class="seg-btn is-active" data-pay="card">Card</button>
<button class="seg-btn" data-pay="cash">Cash</button>
<button class="seg-btn" data-pay="split">Split</button>
<button class="seg-btn" data-pay="company">Direct bill</button>
</div>
<div class="pay-body" id="payBody"></div>
</section>
<button class="settle-btn" id="settle">
<span>Settle & release room</span>
<span class="settle-amt" id="settleAmt">€0.00</span>
</button>
<div class="result" id="result" hidden>
<span class="result-mark">✓</span>
<div>
<strong>Folio settled · invoice <span id="invoiceNo">INV-2026-04812</span></strong>
<small>Room 207 is now <em>departed</em> · sent to housekeeping queue.</small>
</div>
</div>
</aside>
</section>
</main>
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Check-out & Folio Settlement
The end-of-stay screen. The folio is itemised by category (Accommodation · F&B · Mini-bar · Spa · City Tax) with line-level select for transfer to second folio or comp/void. The right rail holds the running totals (subtotal · taxes · paid · balance), payment options (cash · card · split), and the Settle / Issue invoice / Release room sequence. Includes a final state where the bill is marked paid and the room flips to “departed — ready for housekeeping”.