Sitzplan-Editor: Namen per Drag & Drop auf Plaetze ziehen + Nachrichtenfeld pro Platz + Speichern (noch ohne ESP32-Anbindung)
This commit is contained in:
parent
91072f3405
commit
3cd48523a1
3 changed files with 160 additions and 80 deletions
|
|
@ -1,20 +1,26 @@
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
def lade():
|
||||||
|
with open(DATEI, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def home():
|
def home():
|
||||||
|
return render_template("index.html", data=lade())
|
||||||
|
|
||||||
# Sitzplan-Datei öffnen
|
|
||||||
with open("sitzplan.json", "r", encoding="utf-8") as datei:
|
|
||||||
sitzplan = json.load(datei)
|
|
||||||
|
|
||||||
return render_template(
|
@app.route("/speichern", methods=["POST"])
|
||||||
"index.html",
|
def speichern():
|
||||||
sitzplan=sitzplan
|
daten = request.get_json()
|
||||||
)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -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": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
|
||||||
|
|
@ -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ätze ziehen · pro Platz eine kurze Nachricht eintippen · 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">✓ 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">×</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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue