Sitzplan-Editor: Namen per Drag & Drop auf Plaetze ziehen + Nachrichtenfeld pro Platz + Speichern (noch ohne ESP32-Anbindung)

This commit is contained in:
Franke 2026-07-08 09:12:09 +00:00 committed by Kanban Service
parent 91072f3405
commit 3cd48523a1
3 changed files with 160 additions and 80 deletions

View file

@ -1,21 +1,27 @@
from flask import Flask, render_template from flask import Flask, render_template, request, jsonify
import json import json, os
app = Flask(__name__) app = Flask(__name__)
DATEI = os.path.join(os.path.dirname(__file__), "sitzplan.json") # relativ zum Skript
@app.route("/")
def home(): def lade():
with open(DATEI, "r", encoding="utf-8") as f:
# Sitzplan-Datei öffnen return json.load(f)
with open("sitzplan.json", "r", encoding="utf-8") as datei:
sitzplan = json.load(datei)
@app.route("/")
return render_template( def home():
"index.html", return render_template("index.html", data=lade())
sitzplan=sitzplan
)
@app.route("/speichern", methods=["POST"])
def speichern():
if __name__ == "__main__": daten = request.get_json()
app.run(host="0.0.0.0", port=5000) with open(DATEI, "w", encoding="utf-8") as f:
json.dump(daten, f, ensure_ascii=False, indent=2)
return jsonify({"status": "ok"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

View file

@ -1,27 +1,10 @@
[ {
{ "namen": ["Max", "Lea", "Tim", "Ben", "Eva"],
"platz": "platz01", "plaetze": [
"name": "Max", { "nr": 1, "name": "", "nachricht": "" },
"nachricht": "" { "nr": 2, "name": "", "nachricht": "" },
}, { "nr": 3, "name": "", "nachricht": "" },
{ { "nr": 4, "name": "", "nachricht": "" },
"platz": "platz02", { "nr": 5, "name": "", "nachricht": "" }
"name": "Lea", ]
"nachricht": "" }
},
{
"platz": "platz03",
"name": "Tim",
"nachricht": ""
},
{
"platz": "platz04",
"name": "Ben",
"nachricht": ""
},
{
"platz": "platz05",
"name": "Eva",
"nachricht": ""
}
]

View file

@ -1,49 +1,140 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="de">
<head> <head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sitzplan</title> <title>Sitzplan</title>
<style> <style>
body { * { box-sizing: border-box; }
font-family: Arial; body { font-family: Arial, system-ui, sans-serif; background: #f8fafc; color: #0f172a;
text-align: center; 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 { .grid { display: grid; grid-template-columns: repeat(5, 150px); gap: 14px;
display: grid; justify-content: center; margin: 0 auto; }
grid-template-columns: repeat(5, 120px); .seat { border: 2px solid #cbd5e1; border-radius: 12px; padding: 10px; background: #fff;
gap: 10px; min-height: 150px; display: flex; flex-direction: column; }
justify-content: center; .seat.over { border-color: #2563eb; background: #eff6ff; }
margin-top: 50px; .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; }
.seat { .pool-wrap { max-width: 800px; margin: 2rem auto 0; }
border: 2px solid black; .pool-title { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin-bottom: 0.4rem; }
padding: 20px; .pool { border: 2px solid #e2e8f0; border-radius: 12px; background: #fff; padding: 14px;
border-radius: 10px; 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; }
.seat .nachricht { .bar { max-width: 800px; margin: 1.4rem auto 0; text-align: center; }
margin-top: 8px; button { background: #2563eb; color: #fff; border: none; border-radius: 10px;
color: #2563eb; padding: 0.7em 1.6em; font-size: 1rem; font-family: inherit; cursor: pointer; }
font-size: 0.85em; button:hover { background: #1d4ed8; }
} .status { margin-left: 1rem; color: #16a34a; font-weight: 600; opacity: 0; transition: opacity 0.2s; }
.status.show { opacity: 1; }
</style> </style>
</head> </head>
<body> <body>
<h1>Sitzplan</h1> <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"> <div class="grid" id="grid"></div>
{% for platz in sitzplan %}
<div class="seat"> <div class="pool-wrap">
{{ platz.name }} <div class="pool-title">Namen</div>
{% if platz.nachricht %} <div class="pool" id="pool"></div>
<div class="nachricht">{{ platz.nachricht }}</div>
{% endif %}
</div>
{% endfor %}
</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> </body>
</html> </html>