510 lines
15 KiB
HTML
Executable file
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>
|