Vermietungs-Tool

A local web app for a small room-rental business in Austria: guest register,
room ledger, occupancy plan, invoices, the monthly tourist-tax report and the
quarterly VAT list. Application code only; it ships no data.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BlackMarkandClaude Opus 5.5 committed 2026-11-02 16:52:27 +01:00
commit 45424e6fd6
47 files changed
+6701

No files matched your search

+288
View File
@@ -0,0 +1,288 @@
:root{
--ink:#1f2a24; --muted:#6b756e; --line:#d9e0da; --bg:#f4f6f3; --panel:#ffffff;
--accent:#2f6d5b; --accent-dark:#245645; --accent-soft:#e5efea;
--warn:#b4531a; --open:#b23a3a; --paid:#2e7d5b; --cash:#7a6a2e;
}
*{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--ink);
font-family:"Segoe UI",system-ui,Arial,sans-serif;font-size:15px;line-height:1.45;
display:flex;flex-direction:column;height:100vh;overflow:hidden}
a{color:var(--accent);text-decoration:none}
a:hover{text-decoration:underline}
.topbar{display:flex;flex-shrink:0;align-items:center;gap:28px;background:var(--accent);color:#fff;
padding:0 26px;height:56px;box-shadow:0 2px 6px rgba(0,0,0,.12)}
.topbar .brand{font-size:20px;font-weight:700;letter-spacing:.2px}
.topbar nav{display:flex;gap:6px;height:100%}
.topbar nav a{color:#e8f2ee;padding:0 18px;display:flex;align-items:center;height:100%;
font-size:17px;border-bottom:4px solid transparent}
.topbar nav a:hover{background:var(--accent-dark);text-decoration:none}
.topbar nav a.active{color:#fff;border-bottom-color:#fff;font-weight:600}
main{flex:1;min-height:0;overflow:auto;width:100%;max-width:1200px;margin:0 auto;padding:20px 26px 40px}
/* grid pages fill the viewport so only the table scrolls (no second page scrollbar) */
main.grid-page{max-width:none;overflow:hidden;display:flex;flex-direction:column;padding:12px 20px}
.grid-page h1{margin:2px 0 10px;font-size:22px}
.flash{background:#fff6e6;border:1px solid #e6c98a;color:#8a5a12;border-radius:8px;
padding:12px 16px;margin-bottom:18px;font-size:16px}
h1{font-size:28px;margin:4px 0 20px}
.toolbar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
.toolbar .spacer{flex:1}
.count{color:var(--muted);font-size:15px}
.btn{display:inline-flex;align-items:center;gap:7px;background:var(--accent);color:#fff;
border:none;border-radius:8px;padding:10px 18px;font-size:16px;cursor:pointer;text-decoration:none}
.btn:hover{background:var(--accent-dark);text-decoration:none}
.btn.secondary{background:#eef2ef;color:var(--ink);border:1px solid var(--line)}
.btn.secondary:hover{background:#e3e9e4}
.btn.danger{background:#fff;color:var(--open);border:1px solid #e3b7b7}
.btn.danger:hover{background:#fbeeee}
.btn.small{padding:6px 12px;font-size:14px}
.btn:disabled{opacity:.4;cursor:not-allowed}
input[type=text],input[type=date],input[type=number],select{
font-size:16px;padding:9px 11px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
input:focus,select:focus{outline:2px solid var(--accent-soft);border-color:var(--accent)}
.search{min-width:280px}
/* overflow-anchor off: the grids swap windowed rows in and out while scrolling — the browser's scroll
anchoring would fight that and make the list jump */
.tablewrap{background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:auto;
overflow-anchor:none}
.grid-page .tablewrap{flex:1;min-height:0}
table{border-collapse:collapse;width:100%;font-size:13px}
thead th{position:sticky;top:0;z-index:1;background:var(--accent-soft);color:var(--accent-dark);text-align:left;
padding:6px 9px;font-weight:600;white-space:nowrap;border-bottom:1px solid var(--line);
cursor:pointer;user-select:none}
/* headers carry full names; th-abbr (set by grids.js when the table overflows its container)
abbreviates them with an ellipsis — the tooltip keeps the full name */
table.th-abbr thead th{max-width:78px;overflow:hidden;text-overflow:ellipsis}
/* pin toggle (Spalte fixieren): revealed on header hover at the LEFT edge — the sort arrows own the
right edge. Grid headers reserve room for it so the glyph never covers the title. */
table.grid thead th{padding-left:20px}
thead th .pinbtn{position:absolute;left:2px;top:50%;transform:translateY(-50%);
font-size:10px;line-height:1;padding:2px;cursor:pointer;opacity:0}
thead th:hover .pinbtn{opacity:.45}
thead th .pinbtn:hover,thead th.pinned .pinbtn{opacity:1}
thead th:hover{background:#d7e6df}
/* sort indicator, pinned to the header's right edge so an abbreviated title can't swallow it;
with several sort levels (shift-click) the priority number follows the arrow */
thead th[data-sd]{padding-right:24px}
thead th[data-sd]::after{position:absolute;right:6px;font-size:10px}
thead th[data-sd="a"]::after{content:"\25B2" attr(data-prio)}
thead th[data-sd="d"]::after{content:"\25BC" attr(data-prio)}
tbody td{padding:2px 9px;border-bottom:1px solid #eef1ee;white-space:nowrap}
/* the ledger has 20 columns — give its cells less air so more of them fit on screen */
table.grid tbody td{padding-left:7px;padding-right:7px}
table.grid thead th{padding-left:14px;padding-right:7px}
table.grid td.actions .btn.small{padding:4px 9px}
/* a right-aligned number needs no room on its LEFT, and the pin glyph can't collide with it either —
so Satz/Anz/Pers/Betrag and the checkbox column give that space back */
table.grid thead th.num,table.grid tbody td.num{padding-left:4px}
table.grid thead th:first-child,table.grid tbody td:first-child{padding-left:5px;padding-right:5px}
tbody tr:nth-child(even){background:#fafcfb}
tbody tr:hover{background:var(--accent-soft)}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}
td.actions{text-align:right}
.empty{padding:40px;text-align:center;color:var(--muted)}
.pill{display:inline-block;padding:2px 10px;border-radius:999px;font-size:13px;font-weight:600}
.pill.paid{background:#e4f0ea;color:var(--paid)}
.pill.open{background:#f7e3e3;color:var(--open)}
.pill.cash{background:#f2eed9;color:var(--cash)}
.pill.unit{background:#e9edf5;color:#3a4a6b}
.pill.have{background:#eef1ef;color:#5a6b62}
/* the whole text of a cut-off list cell, on hover (grids.js showTip) */
.celltip{position:fixed;z-index:50;display:none;max-width:480px;padding:6px 11px;background:var(--panel);
color:var(--ink);border:1px solid var(--line);border-radius:8px;box-shadow:0 4px 14px rgba(0,0,0,.15);
font-size:15px;line-height:1.35;white-space:normal;pointer-events:none}
/* Umsatzsteuer-Voranmeldung: one table with its sum per room, the summary at the end */
.vat-room h2,.vat-total h2{font-size:19px;margin:22px 0 6px}
.vat-room tfoot td{padding:4px 9px;font-weight:700;border-top:2px solid var(--line)}
.vat-total table{width:auto;min-width:420px;font-size:16px;background:var(--panel);
border:1px solid var(--line);border-radius:10px}
.vat-total td{padding:5px 14px;border-bottom:1px solid #eef1ee}
.vat-total td.num{padding-left:40px}
.vat-total tr.key td{font-weight:700}
/* home */
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px;margin:20px 0 30px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:20px 22px}
.card .n{font-size:34px;font-weight:700;color:var(--accent-dark)}
.card .l{color:var(--muted);margin-top:4px}
.lead{font-size:18px;color:var(--muted);max-width:60ch}
.quicklinks{display:flex;gap:14px;flex-wrap:wrap;margin-top:10px}
/* forms */
form.card{max-width:720px;margin:0 auto}
.field{display:flex;flex-direction:column;gap:6px;margin-bottom:16px}
.field label{font-weight:600;font-size:15px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
/* an invoice dated against the number order: the deliberate exception (invoice_form) */
.confirm-order{display:flex;align-items:center;gap:8px;margin:2px 0 14px;padding:8px 12px;font-size:15px;
background:#fff6e6;border:1px solid #e6c98a;border-radius:8px;color:#8a5a12}
.client-choices{flex-direction:column;align-items:flex-start}
.client-choices .btn.picked{background:var(--accent);color:#fff}
/* payments in parts: one line of date + amount each (ledger_form) */
.pay-line{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:8px}
#addPayment{align-self:flex-start;margin-bottom:6px}
.formactions{display:flex;gap:12px;margin-top:24px}
/* Belegungsplan: rooms × days timeline. The day columns are fractional (1fr), so the plan always
fills the viewport width exactly — no horizontal scrolling at any display scaling (up to 200%).
Only the room rows scroll vertically; the date header sticks. */
.planwrap{overflow-y:auto;overflow-x:hidden}
.plan{--labelw:112px;--laneh:26px;min-width:600px}
.plan-hrow,.plan-row{display:flex}
.plan-row{border-bottom:1px solid var(--line)}
.plan-row.newfloor{border-top:2px solid var(--line)}
.plan-label{flex:0 0 var(--labelw);padding:4px 10px;border-right:1px solid var(--line);min-width:0}
.plan-cells{flex:1;display:grid;grid-template-columns:repeat(var(--days),1fr);min-width:0}
.plan-head{position:sticky;top:0;z-index:3;background:var(--panel);box-shadow:0 2px 4px rgba(0,0,0,.07)}
/* the month row closes with a full-width line */
.plan-head .plan-hrow:first-child{border-bottom:1px solid var(--line)}
.plan-head .plan-hrow + .plan-hrow .plan-label{border-bottom:1px solid var(--line)}
.pm{font-size:12px;font-weight:600;color:var(--accent-dark);padding:3px 5px 1px 8px;
border-left:1px solid #b9c4bb;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* the label column already draws the divider — no doubled line on the first day column */
.pm:first-child,.pd:first-child{border-left:none}
.pd{border-left:1px solid #eef1ee;border-bottom:1px solid var(--line);text-align:center;
padding:1px 0 3px;overflow:visible;min-width:0}
.pd b{display:block;font-size:12px;line-height:1.25;white-space:nowrap}
.pd span{display:block;font-size:10px;color:var(--muted);line-height:1.2}
.pd.we{background:#f2f4f0}
.pd.mon{border-left-color:var(--line)}
/* weekend and today columns are bounded on BOTH sides ('x + y' colors the following cell's left
border = this column's end line), at normal 1px line thickness. Precedence weekend < month <
today is enforced by DOUBLED classes (today is often also .we — a Sunday — and the '+' selectors
carry an extra element of specificity that a single class cannot beat). */
.pd.we,.pd.we + .pd{border-left-color:#c9d2ca}
.pd.m1.m1{border-left-color:#b9c4bb} /* month boundary: continuous from the month row down */
.pd.today{background:#fff3d6}
.pd.today.today,.pd.today.today + .pd{border-left-color:var(--warn)}
.pd.today b{color:var(--warn)}
.plan-lanes{grid-template-rows:repeat(var(--lanes),var(--laneh));position:relative;
align-content:center}
/* room rows carry only neutral day grid lines — the weekend/today accents live in the header
alone. The lines are a full-height absolute overlay so they span the whole row even when the
room label makes the row taller than the lane stack. */
.daylines{position:absolute;inset:0;display:grid;grid-template-columns:repeat(var(--days),1fr);
pointer-events:none}
.daylines i{border-left:1px solid #f0f3ef}
.daylines i.mon{border-left-color:var(--line)}
.daylines i.we,.daylines i.we + i{border-left-color:#c9d2ca}
.daylines i.m1.m1{border-left-color:#b9c4bb}
.daylines i.today.today,.daylines i.today.today + i{border-left-color:var(--warn)}
.daylines i:first-child{border-left:none}
/* weekend/today column tints, same hues as the header */
.daylines i.we{background:#f2f4f0}
.daylines i.today{background:#fff3d6}
.plan-lanes.longterm{background:repeating-linear-gradient(45deg,#f3f5f2 0 8px,#eaeeea 8px 16px)}
/* ALL grid lines (month boundaries included) sit BEHIND cell content — the opaque
Dauervermietung filling covers its whole row, so these rows draw no lines or tints */
.plan-lanes.longterm .daylines{display:none}
.lt-note{grid-column:1/-1;grid-row:1/-1;align-self:center;padding:0 10px;color:var(--muted);
font-size:12px;z-index:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rl-top{display:flex;align-items:center;gap:6px;white-space:nowrap}
.rl-top b{font-size:14px}
.rl-top .pill{font-size:11px;padding:1px 7px}
.rl-sub{font-size:11px;color:var(--muted);white-space:nowrap}
.bar{z-index:1;margin:2px 1px;border-radius:5px;font-size:12.5px;line-height:calc(var(--laneh) - 5px);
padding:0 7px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;color:#fff}
.bar:hover{text-decoration:none;filter:brightness(1.08)}
.bar.current{background:var(--accent)}
.bar.past{background:#b9c9c0;color:#2c3a32}
.bar.future{background:var(--accent-soft);color:var(--accent-dark);box-shadow:inset 0 0 0 1px var(--accent)}
/* a stay running past the window edge (or an open departure) gets a pointed end */
.bar.clipend{clip-path:polygon(0 0,calc(100% - 8px) 0,100% 50%,calc(100% - 8px) 100%,0 100%);padding-right:13px}
.bar.clipstart{clip-path:polygon(8px 0,100% 0,100% 100%,8px 100%,0 50%);padding-left:13px}
.bar.clipstart.clipend{clip-path:polygon(8px 0,calc(100% - 8px) 0,100% 50%,calc(100% - 8px) 100%,8px 100%,0 50%)}
.plan-foot{flex-shrink:0;display:flex;gap:16px;align-items:center;flex-wrap:wrap;
padding:8px 2px 0;color:var(--muted);font-size:13px}
.plan-foot .key{display:inline-block;width:14px;height:14px;border-radius:4px;
vertical-align:-2px;margin-right:5px}
.plan-unplaced a{margin-left:2px}
.seg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff}
.seg a{padding:7px 13px;font-size:14px;color:var(--ink)}
.seg a:hover{background:#eef2ef;text-decoration:none}
.seg a.on{background:var(--accent);color:#fff}
/* at 150–200% display scaling (≈960–1280 CSS px on a 24" monitor) the chrome steps down so no page
ever grows a horizontal scrollbar */
@media (max-width:1280px){
.topbar{gap:16px;padding:0 14px}
.topbar .brand{font-size:17px}
.topbar nav a{padding:0 11px;font-size:15px}
}
@media (max-width:1020px){
.topbar .brand{display:none}
main{padding-left:14px;padding-right:14px}
main.grid-page{padding:10px 12px}
}
/* printing an app page (e.g. the TVB Meldung for the folder): the app shell scrolls inside a fixed
viewport, which would clip everything to one page — undo that and drop chrome/warnings.
MUST stay at the END of the file: several rules here tie on specificity with base rules above
(tbody td, body) and win only by source order. */
.printinfo{display:none}
/* Print view. Two roads lead here: Ctrl+P (media print) and the Drucken button, which measures the page
BEFORE printing to shrink it onto one sheet — for that it must already look on screen as it will on
paper, or the toolbar is measured too. Hence the same rules under body.printing (measuring) and
@media print (paper). */
body.printing .topbar,body.printing .toolbar,body.printing .flash{display:none}
body.printing{background:#fff;height:auto;overflow:visible;display:block}
body.printing main{overflow:visible;height:auto;max-width:none;padding:0;background:#fff}
body.printing .tablewrap{max-height:none;overflow:visible;box-shadow:none;border:0;background:#fff}
body.printing .printinfo{display:block;margin:0 0 8px;font-size:12px;color:#333}
body.printing .grid th,body.printing .grid td{position:static}
body.printing tbody td{white-space:normal}
/* Umsatzsteuer list: only the name may wrap — otherwise every cell breaks on paper ("2026-/49") */
.vat-room td,body.printing .vat-room td{white-space:nowrap}
.vat-room tbody td:nth-child(2),body.printing .vat-room tbody td:nth-child(2){white-space:normal}
/* On screen the columns are frozen to fixed pixel widths (freezeLayout, flex rows). On paper the table
must be a real table again: then the columns shrink to their content and long texts wrap instead of
pushing the list past the sheet's edge. */
body.printing .grid,body.printing .grid thead,body.printing .grid tbody,body.printing .grid tr,
body.printing .grid th,body.printing .grid td{display:revert!important;min-width:0!important}
body.printing .grid{width:100%!important;table-layout:auto!important;border-collapse:collapse}
body.printing .grid thead{display:table-header-group!important;position:static!important}
body.printing .grid th,body.printing .grid td{width:auto!important;max-width:none!important;
overflow:visible!important;text-overflow:clip!important;white-space:normal!important;
padding:2px 5px!important;font-size:12px!important;line-height:1.25!important}
body.printing .grid tr{break-inside:avoid}
body.printing .grid td.num,body.printing .grid .nowrap{white-space:nowrap!important}
/* after the display:revert rules, or revert wins over the hiding */
body.printing .grid .selcol,body.printing .grid .actions,body.printing .pinbtn{display:none!important}
body.printing .grid .pill{border:0;padding:0;background:none!important;color:inherit!important;font-weight:600}
@media print {
/* Landscape for the wide lists (the named page "list"); everything else portrait — the TVB report sets
its landscape itself (tvb.html). Printed duplex in landscape, the printer must flip "on the short
edge", or the back side stands on its head — no page can set that itself. */
@page{size:A4 portrait;margin:10mm}
@page list{size:A4 landscape;margin:10mm}
main.grid-page{page:list}
body{display:block;height:auto;overflow:visible;background:#fff}
.topbar,.noprint,.flash,.toolbar,.celltip{display:none!important}
main{overflow:visible;height:auto;max-width:none;padding:0;background:#fff}
.tablewrap{max-height:none!important;overflow:visible;box-shadow:none;border:0;background:#fff}
tbody td{white-space:normal} /* long Hinweis texts wrap instead of running off the sheet */
td.note div{max-width:240px} /* cells ignore max-width in auto layout — cap the inner block */
.tvb-unit + .tvb-unit{page-break-before:always}
.tvb-summary,.tvb-summary .tablewrap{break-inside:avoid} /* Feratel tables never split over pages */
.tvb-feratel{break-inside:avoid} /* ... and their heading and total travel WITH them */
.vat-room,.vat-total{break-inside:avoid} /* a room with its sum on one sheet */
/* the list on paper: checkboxes, the edit button and the pin say nothing there, pinned columns must
not stick over the others, the header row repeats on every sheet */
.grid .selcol,.grid .actions,.pinbtn{display:none}
.grid th,.grid td{position:static!important}
.grid thead{display:table-header-group}
.grid tr{break-inside:avoid}
.printinfo{display:block;margin:0 0 8px;font-size:12px;color:#333}
}