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,21 +1,27 @@
|
|||
from flask import Flask, render_template
|
||||
import json
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def home():
|
||||
|
||||
# Sitzplan-Datei öffnen
|
||||
with open("sitzplan.json", "r", encoding="utf-8") as datei:
|
||||
sitzplan = json.load(datei)
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
sitzplan=sitzplan
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import json, os
|
||||
|
||||
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("/")
|
||||
def home():
|
||||
return render_template("index.html", data=lade())
|
||||
|
||||
|
||||
@app.route("/speichern", methods=["POST"])
|
||||
def speichern():
|
||||
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__":
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
|
|
|
|||
|
|
@ -1,27 +1,10 @@
|
|||
[
|
||||
{
|
||||
"platz": "platz01",
|
||||
"name": "Max",
|
||||
"nachricht": ""
|
||||
},
|
||||
{
|
||||
"platz": "platz02",
|
||||
"name": "Lea",
|
||||
"nachricht": ""
|
||||
},
|
||||
{
|
||||
"platz": "platz03",
|
||||
"name": "Tim",
|
||||
"nachricht": ""
|
||||
},
|
||||
{
|
||||
"platz": "platz04",
|
||||
"name": "Ben",
|
||||
"nachricht": ""
|
||||
},
|
||||
{
|
||||
"platz": "platz05",
|
||||
"name": "Eva",
|
||||
"nachricht": ""
|
||||
}
|
||||
]
|
||||
{
|
||||
"namen": ["Max", "Lea", "Tim", "Ben", "Eva"],
|
||||
"plaetze": [
|
||||
{ "nr": 1, "name": "", "nachricht": "" },
|
||||
{ "nr": 2, "name": "", "nachricht": "" },
|
||||
{ "nr": 3, "name": "", "nachricht": "" },
|
||||
{ "nr": 4, "name": "", "nachricht": "" },
|
||||
{ "nr": 5, "name": "", "nachricht": "" }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,140 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sitzplan</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial;
|
||||
text-align: center;
|
||||
}
|
||||
* { 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, 120px);
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.seat {
|
||||
border: 2px solid black;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.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; }
|
||||
|
||||
.seat .nachricht {
|
||||
margin-top: 8px;
|
||||
color: #2563eb;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.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ätze ziehen · pro Platz eine kurze Nachricht eintippen · dann speichern.</p>
|
||||
|
||||
<div class="grid">
|
||||
{% for platz in sitzplan %}
|
||||
<div class="seat">
|
||||
{{ platz.name }}
|
||||
{% if platz.nachricht %}
|
||||
<div class="nachricht">{{ platz.nachricht }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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">✓ 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>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue