INBOX/wechselgeldrechner_cafe_bach.html
2026-08-07 16:01:24 +02:00

510 lines
15 KiB
HTML
Executable file

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wechselgeldrechner - Café Bach</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Alfa+Slab+One&family=Lobster+Two:ital,wght@1,400;1,700&family=Roboto+Condensed:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--cream: #fff4eb;
--cream-2: #fffaf5;
--brown: #b18d6c;
--dark: #322f2a;
--red: #e62440;
--rose: #a54e62;
--blue: #3587c8;
--green: #2b4b45;
--gold: #b59336;
--muted: #cabaa3;
--shadow: 0 22px 60px rgba(50, 47, 42, .16);
--radius: 22px;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--dark);
background:
radial-gradient(circle at top left, rgba(230,36,64,.12), transparent 35rem),
linear-gradient(135deg, var(--cream), #f5e2d3 55%, #ead1bd);
font-family: "Roboto Condensed", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 18px;
line-height: 1.35;
}
.page {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 28px 0 48px;
}
header {
display: grid;
grid-template-columns: 1fr auto;
gap: 24px;
align-items: center;
margin-bottom: 24px;
}
.brand-card {
display: flex;
align-items: center;
gap: 18px;
}
.logo-mark {
width: 104px;
height: 104px;
border-radius: 28px;
display: grid;
place-items: center;
color: var(--cream);
background: linear-gradient(145deg, var(--brown), #9c7759);
box-shadow: var(--shadow);
transform: rotate(-1deg);
}
.logo-mark span {
font-family: "Lobster Two", cursive;
font-size: 48px;
font-weight: 700;
letter-spacing: -.04em;
text-shadow: 0 3px 10px rgba(50,47,42,.25);
}
h1 {
margin: 0;
font-family: "Alfa Slab One", Georgia, serif;
font-size: clamp(2.1rem, 4vw, 4.2rem);
font-weight: 400;
letter-spacing: .01em;
line-height: .95;
}
.subtitle {
margin: 8px 0 0;
font-family: "Lobster Two", cursive;
font-size: clamp(1.4rem, 2.4vw, 2.2rem);
color: var(--rose);
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-end;
}
button {
border: 0;
border-radius: 999px;
padding: 12px 18px;
font: 700 1rem/1 "Roboto Condensed", sans-serif;
cursor: pointer;
color: var(--cream);
background: var(--red);
box-shadow: 0 10px 24px rgba(230,36,64,.22);
transition: transform .16s ease, box-shadow .16s ease, background .16s ease;
}
button:hover { transform: translateY(-1px); box-shadow: 0 12px 28px rgba(230,36,64,.28); }
button.secondary { background: var(--dark); box-shadow: 0 10px 24px rgba(50,47,42,.18); }
button.ghost { background: transparent; color: var(--dark); border: 2px solid rgba(50,47,42,.16); box-shadow: none; }
main {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(320px, .8fr);
gap: 22px;
}
.panel {
background: rgba(255,244,235,.88);
border: 1px solid rgba(50,47,42,.10);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.panel-head {
padding: 20px 22px;
background: var(--dark);
color: var(--cream);
display: flex;
justify-content: space-between;
align-items: end;
gap: 16px;
}
.panel-head h2 {
margin: 0;
font-family: "Lobster Two", cursive;
font-size: 2rem;
font-style: italic;
font-weight: 700;
}
.panel-head small { opacity: .84; }
.table-wrap { overflow-x: auto; }
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 13px 16px;
border-bottom: 1px solid rgba(50,47,42,.10);
text-align: right;
vertical-align: middle;
}
th:first-child, td:first-child { text-align: left; }
th {
font-size: .93rem;
letter-spacing: .06em;
text-transform: uppercase;
color: rgba(50,47,42,.72);
background: rgba(202,186,163,.25);
}
tr.section td {
background: rgba(181,147,54,.16);
color: var(--green);
font-family: "Lobster Two", cursive;
font-size: 1.55rem;
font-weight: 700;
text-align: left;
}
input[type="number"], input[type="text"] {
width: 100%;
max-width: 150px;
padding: 10px 12px;
border: 2px solid rgba(50,47,42,.16);
border-radius: 14px;
background: var(--cream-2);
color: var(--dark);
font: 700 1.05rem/1.2 "Roboto Condensed", sans-serif;
text-align: right;
outline: none;
}
input:focus {
border-color: var(--blue);
box-shadow: 0 0 0 4px rgba(53,135,200,.16);
}
.amount { font-weight: 700; font-variant-numeric: tabular-nums; }
.summary {
display: grid;
gap: 16px;
padding: 20px;
}
.kpi {
border-radius: 18px;
padding: 18px;
background: var(--cream-2);
border: 1px solid rgba(50,47,42,.10);
}
.kpi strong {
display: block;
margin-bottom: 6px;
color: rgba(50,47,42,.70);
font-size: .95rem;
text-transform: uppercase;
letter-spacing: .05em;
}
.kpi .value {
font-family: "Alfa Slab One", Georgia, serif;
font-size: clamp(2rem, 5vw, 3.2rem);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.target-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
}
.difference.positive .value { color: var(--green); }
.difference.negative .value { color: var(--red); }
.difference.neutral .value { color: var(--gold); }
.notice {
border-left: 8px solid var(--blue);
background: rgba(53,135,200,.10);
padding: 16px 18px;
border-radius: 16px;
font-size: 1.1rem;
}
.notice.good { border-left-color: var(--green); background: rgba(43,75,69,.10); }
.notice.warn { border-left-color: var(--red); background: rgba(230,36,64,.10); }
.details {
padding: 0 20px 20px;
color: rgba(50,47,42,.78);
font-size: 1rem;
}
.details ul { margin: 10px 0 0 20px; padding: 0; }
footer {
margin-top: 22px;
color: rgba(50,47,42,.68);
font-size: .95rem;
text-align: center;
}
@media (max-width: 860px) {
header, main { grid-template-columns: 1fr; }
.toolbar { justify-content: flex-start; }
.logo-mark { width: 82px; height: 82px; border-radius: 22px; }
.logo-mark span { font-size: 39px; }
th, td { padding: 11px 12px; }
}
@media print {
body { background: white; color: #000; }
.page { width: 100%; padding: 0; }
.toolbar, footer { display: none; }
.panel { box-shadow: none; break-inside: avoid; }
main { grid-template-columns: 1fr .75fr; }
input { border: 1px solid #999; background: white; }
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="brand-card">
<div class="logo-mark" aria-hidden="true"><span>Bach</span></div>
<div>
<h1>Wechselgeld</h1>
<p class="subtitle">Kassenrechner für Café Bach</p>
</div>
</div>
<div class="toolbar" aria-label="Aktionen">
<button type="button" id="saveBtn">Speichern</button>
<button type="button" class="secondary" onclick="window.print()">Drucken</button>
<button type="button" class="ghost" id="resetBtn">Leeren</button>
</div>
</header>
<main>
<section class="panel" aria-labelledby="cash-title">
<div class="panel-head">
<h2 id="cash-title">Kassenbestand zählen</h2>
<small>Werte aus der Excel-Idee übernommen</small>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Wert</th>
<th>Anzahl</th>
<th>Betrag</th>
</tr>
</thead>
<tbody id="cashRows"></tbody>
</table>
</div>
</section>
<aside class="panel" aria-labelledby="summary-title">
<div class="panel-head">
<h2 id="summary-title">Auswertung</h2>
<small>live berechnet</small>
</div>
<div class="summary">
<div class="kpi">
<strong>Gesamt in Kasse</strong>
<div class="value" id="totalValue">0,00 €</div>
</div>
<div class="kpi">
<strong>Soll in Kasse</strong>
<div class="target-row">
<input id="targetInput" type="text" inputmode="decimal" value="350,00" aria-label="Sollbetrag in der Kasse" />
<span></span>
</div>
</div>
<div class="kpi difference neutral" id="differenceCard">
<strong>Differenz</strong>
<div class="value" id="differenceValue">0,00 €</div>
</div>
<div class="notice" id="notice">Bitte trage die Anzahl der Scheine und Münzen ein.</div>
</div>
<div class="details">
<strong>Hinweise</strong>
<ul>
<li>Positive Differenz: Es ist mehr Geld in der Kasse als Soll.</li>
<li>Negative Differenz: Es fehlt Geld bis zum Sollbetrag.</li>
<li>Der Speichern-Button legt die Eingaben lokal in diesem Browser ab.</li>
</ul>
</div>
</aside>
</main>
<footer>
Café Bach · Wechselgeldrechner · lokal im Browser nutzbar
</footer>
</div>
<script>
const denominations = [
{ type: "section", label: "Scheine" },
{ value: 100, label: "100 €" },
{ value: 50, label: "50 €" },
{ value: 20, label: "20 €" },
{ value: 10, label: "10 €" },
{ value: 5, label: "5 €" },
{ type: "section", label: "Münzen" },
{ value: 2, label: "2 €" },
{ value: 1, label: "1 €" },
{ value: 0.5, label: "50 Cent" },
{ value: 0.2, label: "20 Cent" },
{ value: 0.1, label: "10 Cent" },
{ value: 0.05, label: "5 Cent" }
];
const STORAGE_KEY = "cafe-bach-wechselgeldrechner-v1";
const formatter = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
const cashRows = document.getElementById("cashRows");
const totalValue = document.getElementById("totalValue");
const targetInput = document.getElementById("targetInput");
const differenceValue = document.getElementById("differenceValue");
const differenceCard = document.getElementById("differenceCard");
const notice = document.getElementById("notice");
function parseGermanNumber(value) {
if (typeof value !== "string") return Number(value) || 0;
const normalized = value.trim().replace(/\./g, "").replace(",", ".");
const number = Number(normalized);
return Number.isFinite(number) ? number : 0;
}
function formatGermanNumber(value) {
return new Intl.NumberFormat("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value);
}
function buildRows() {
denominations.forEach((item, index) => {
if (item.type === "section") {
const tr = document.createElement("tr");
tr.className = "section";
tr.innerHTML = `<td colspan="3">${item.label}</td>`;
cashRows.appendChild(tr);
return;
}
const tr = document.createElement("tr");
const key = String(item.value).replace(".", "_");
tr.innerHTML = `
<td><strong>${item.label}</strong></td>
<td><input type="number" min="0" step="1" data-value="${item.value}" data-key="${key}" aria-label="Anzahl ${item.label}" /></td>
<td class="amount" data-amount-for="${key}">0,00 €</td>
`;
cashRows.appendChild(tr);
});
}
function calculate() {
let total = 0;
document.querySelectorAll("input[data-value]").forEach(input => {
const count = Math.max(0, Math.floor(Number(input.value) || 0));
if (input.value && Number(input.value) !== count) input.value = count;
const denomination = Number(input.dataset.value);
const amount = count * denomination;
total += amount;
document.querySelector(`[data-amount-for="${input.dataset.key}"]`).textContent = formatter.format(amount);
});
const target = parseGermanNumber(targetInput.value);
const difference = total - target;
totalValue.textContent = formatter.format(total);
differenceValue.textContent = formatter.format(difference);
differenceCard.classList.remove("positive", "negative", "neutral");
if (Math.abs(difference) < 0.005) {
differenceCard.classList.add("neutral");
notice.className = "notice good";
notice.textContent = "Perfekt: Die Kasse entspricht genau dem Sollbetrag.";
} else if (difference > 0) {
differenceCard.classList.add("positive");
notice.className = "notice";
notice.textContent = `Es sind ${formatter.format(difference)} mehr in der Kasse als Soll. Diesen Betrag kannst du abschöpfen oder separat verbuchen.`;
} else {
differenceCard.classList.add("negative");
notice.className = "notice warn";
notice.textContent = `Es fehlen ${formatter.format(Math.abs(difference))} bis zum Sollbetrag.`;
}
}
function saveState() {
const counts = {};
document.querySelectorAll("input[data-key]").forEach(input => counts[input.dataset.key] = input.value || "");
const state = { counts, target: targetInput.value };
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
notice.className = "notice good";
notice.textContent = "Gespeichert: Deine Eingaben wurden lokal in diesem Browser abgelegt.";
setTimeout(calculate, 1400);
}
function loadState() {
try {
const state = JSON.parse(localStorage.getItem(STORAGE_KEY) || "null");
if (!state) return;
if (state.target) targetInput.value = state.target;
Object.entries(state.counts || {}).forEach(([key, value]) => {
const input = document.querySelector(`input[data-key="${key}"]`);
if (input) input.value = value;
});
} catch (error) {
console.warn("Gespeicherte Werte konnten nicht geladen werden.", error);
}
}
function resetState() {
document.querySelectorAll("input[data-key]").forEach(input => input.value = "");
targetInput.value = "350,00";
localStorage.removeItem(STORAGE_KEY);
calculate();
}
buildRows();
loadState();
calculate();
document.addEventListener("input", event => {
if (event.target.matches("input")) calculate();
});
targetInput.addEventListener("blur", () => {
targetInput.value = formatGermanNumber(parseGermanNumber(targetInput.value));
calculate();
});
document.getElementById("saveBtn").addEventListener("click", saveState);
document.getElementById("resetBtn").addEventListener("click", resetState);
</script>
</body>
</html>