Sitzplanalgorithmus/Projekt/templates/index.html

140 lines
6.6 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sitzplan</title>
<style>
* { box-sizing: border-box; }
body { font-family: Arial, system-ui, sans-serif; background: #f8fafc; color: #0f172a;
margin: 0; padding: 2rem 1rem 4rem; }
h1 { text-align: center; margin: 0 0 0.2em; }
.hint { text-align: center; color: #64748b; margin: 0 0 1.8rem; font-size: 0.95rem; }
.grid { display: grid; grid-template-columns: repeat(5, 150px); gap: 14px;
justify-content: center; margin: 0 auto; }
.seat { border: 2px solid #cbd5e1; border-radius: 12px; padding: 10px; background: #fff;
min-height: 150px; display: flex; flex-direction: column; }
.seat.over { border-color: #2563eb; background: #eff6ff; }
.seat .nr { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; }
.dropzone { flex: 1; display: flex; align-items: center; justify-content: center;
border: 2px dashed #e2e8f0; border-radius: 8px; margin: 6px 0; min-height: 44px;
color: #94a3b8; font-size: 0.8rem; text-align: center; }
.chip { background: #1e293b; color: #fff; border-radius: 8px; padding: 6px 12px;
font-weight: 600; cursor: grab; display: inline-flex; align-items: center; gap: 8px; }
.chip .x { cursor: pointer; opacity: 0.6; font-weight: 700; }
.chip .x:hover { opacity: 1; }
.msg { width: 100%; border: 1px solid #cbd5e1; border-radius: 8px; padding: 6px 8px;
font-size: 0.85rem; font-family: inherit; }
.msg:focus { outline: none; border-color: #2563eb; }
.pool-wrap { max-width: 800px; margin: 2rem auto 0; }
.pool-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin-bottom: 0.4rem; }
.pool { border: 2px solid #e2e8f0; border-radius: 12px; background: #fff; padding: 14px;
min-height: 60px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.pool.over { border-color: #2563eb; background: #eff6ff; }
.pool:empty::before { content: "alle Namen sind verteilt"; color: #94a3b8; font-size: 0.85rem; }
.bar { max-width: 800px; margin: 1.4rem auto 0; text-align: center; }
button { background: #2563eb; color: #fff; border: none; border-radius: 10px;
padding: 0.7em 1.6em; font-size: 1rem; font-family: inherit; cursor: pointer; }
button:hover { background: #1d4ed8; }
.status { margin-left: 1rem; color: #16a34a; font-weight: 600; opacity: 0; transition: opacity 0.2s; }
.status.show { opacity: 1; }
</style>
</head>
<body>
<h1>Sitzplan</h1>
<p class="hint">Namen aus der Box unten auf die Pl&auml;tze ziehen &middot; pro Platz eine kurze Nachricht eintippen &middot; dann speichern.</p>
<div class="grid" id="grid"></div>
<div class="pool-wrap">
<div class="pool-title">Namen</div>
<div class="pool" id="pool"></div>
</div>
<div class="bar">
<button onclick="speichern()">Speichern</button>
<span class="status" id="status">&#10003; gespeichert</span>
</div>
<script>
const initial = {{ data|tojson }};
const namen = initial.namen.slice();
const belegung = {}; // nr -> name ("" = leer)
const nachricht = {}; // nr -> text
initial.plaetze.forEach(p => { belegung[p.nr] = p.name || ""; nachricht[p.nr] = p.nachricht || ""; });
const plaetze = initial.plaetze.map(p => p.nr);
function poolNamen() {
const vergeben = Object.values(belegung).filter(Boolean);
return namen.filter(n => !vergeben.includes(n));
}
function chip(name, from) {
const c = document.createElement("div");
c.className = "chip"; c.draggable = true; c.dataset.name = name;
c.innerHTML = name + (from !== "pool" ? ' <span class="x" title="entfernen">&times;</span>' : '');
c.addEventListener("dragstart", e => e.dataTransfer.setData("text/plain", JSON.stringify({ name, from })));
if (from !== "pool") {
c.querySelector(".x").addEventListener("click", () => { belegung[from] = ""; render(); });
}
return c;
}
function render() {
const grid = document.getElementById("grid");
grid.innerHTML = "";
plaetze.forEach(nr => {
const seat = document.createElement("div");
seat.className = "seat"; seat.dataset.nr = nr;
seat.innerHTML = '<div class="nr">Platz ' + nr + '</div>';
const zone = document.createElement("div");
zone.className = "dropzone";
if (belegung[nr]) { zone.appendChild(chip(belegung[nr], nr)); }
else { zone.textContent = "hierher ziehen"; }
seat.appendChild(zone);
const inp = document.createElement("input");
inp.className = "msg"; inp.placeholder = "Nachricht …"; inp.value = nachricht[nr] || "";
inp.addEventListener("input", e => nachricht[nr] = e.target.value);
seat.appendChild(inp);
seat.addEventListener("dragover", e => { e.preventDefault(); seat.classList.add("over"); });
seat.addEventListener("dragleave", () => seat.classList.remove("over"));
seat.addEventListener("drop", e => {
e.preventDefault(); seat.classList.remove("over");
const { name, from } = JSON.parse(e.dataTransfer.getData("text/plain"));
if (from !== "pool") belegung[from] = ""; // von altem Platz entfernen
belegung[nr] = name; // (evtl. alter Name hier wird frei)
render();
});
grid.appendChild(seat);
});
const pool = document.getElementById("pool");
pool.innerHTML = "";
poolNamen().forEach(n => pool.appendChild(chip(n, "pool")));
}
const pool = document.getElementById("pool");
pool.addEventListener("dragover", e => { e.preventDefault(); pool.classList.add("over"); });
pool.addEventListener("dragleave", () => pool.classList.remove("over"));
pool.addEventListener("drop", e => {
e.preventDefault(); pool.classList.remove("over");
const { name } = JSON.parse(e.dataTransfer.getData("text/plain"));
for (const nr of plaetze) if (belegung[nr] === name) belegung[nr] = "";
render();
});
function speichern() {
const daten = { namen, plaetze: plaetze.map(nr => ({ nr, name: belegung[nr], nachricht: nachricht[nr] || "" })) };
fetch("/speichern", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(daten) })
.then(r => r.json())
.then(() => { const s = document.getElementById("status"); s.classList.add("show"); setTimeout(() => s.classList.remove("show"), 1800); });
}
render();
</script>
</body>
</html>