Files
Michael Stangl c6e0b95d81 Web-GUI: fixierte Gruppen anzeigen, Strafprofil waehlen, Farben nutzen
CSVConfig.HandleGaesteliste liefert zusaetzlich FixierteGruppen (Name ->
Tisch/Anzahl), damit fest platzierte Gaeste in der Gaesteliste sichtbar
(grau hinterlegt) erscheinen. Durchgaengig "VIP" -> "fixiert" umbenannt.

Backend: Strafprofile werden gecacht geladen, neuer Endpoint /strafprofile
und Query-Parameter strafen an /berechnen, damit sich Profile (default vs.
locker) in der GUI vergleichen lassen. SVG-Ausgabe nutzt das Farbschema aus
farben.cfg (Rueckfall auf Palette).

Frontend: tische.ini und CSV werden bei Auswahl sofort geladen, Auswahl der
fixierten Gruppen und des Optimierungsprofils in der Oberflaeche.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 10:10:41 +02:00

500 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sitz &amp; Platz — Testoberfläche (M1)</title>
<style>
:root {
--akzent: #2f6f4f;
--rand: #d9d4c8;
--hintergrund: #faf8f3;
}
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
margin: 0; background: var(--hintergrund); color: #2a2a24;
line-height: 1.5;
}
header {
background: var(--akzent); color: white;
padding: 1rem 1.5rem;
}
header h1 { margin: 0; font-size: 1.3rem; }
header p { margin: .2rem 0 0; opacity: .85; font-size: .85rem; }
main { max-width: 60rem; margin: 0 auto; padding: 1rem 1.5rem 4rem; }
section {
background: white; border: 1px solid var(--rand);
border-radius: 8px; padding: 1rem 1.25rem; margin-top: 1.25rem;
}
h2 { font-size: 1.05rem; margin: 0 0 .75rem; color: var(--akzent); }
table { border-collapse: collapse; width: 100%; font-size: .9rem; }
th, td { border: 1px solid var(--rand); padding: .3rem .5rem; text-align: left; }
th { background: #f1eee6; }
input[type=number] { width: 4.5rem; }
textarea {
width: 100%; min-height: 11rem; font-family: ui-monospace, monospace;
font-size: .85rem; border: 1px solid var(--rand); border-radius: 4px;
padding: .5rem;
}
button {
background: var(--akzent); color: white; border: 0;
border-radius: 4px; padding: .45rem .9rem; font-size: .9rem;
cursor: pointer; margin: .25rem .35rem .25rem 0;
}
button.sekundaer { background: #8b8577; }
button:disabled { background: #b9b4a6; cursor: not-allowed; }
.status { font-size: .85rem; margin-top: .5rem; min-height: 1.2rem; }
.status.ok { color: var(--akzent); }
.status.fehler { color: #a03030; white-space: pre-wrap; }
#svgPlan { margin-top: .75rem; border: 1px solid var(--rand); border-radius: 4px;
background: white; overflow: auto; }
#svgPlan svg { max-width: 100%; height: auto; display: block; }
#svgPlan .person { cursor: pointer; }
#svgPlan .person.aktiv { stroke: #111; stroke-width: 3; }
#gruppeBox { position: fixed; z-index: 1000; pointer-events: none; display: none;
background: #2a2a24; color: white; border-radius: 6px;
padding: .5rem .7rem; font-size: .8rem; max-width: 18rem;
line-height: 1.35; box-shadow: 0 2px 10px rgba(0,0,0,.35); }
#gruppeBox b { display: block; margin-bottom: .3rem; color: var(--akzent-hell, #d6eade); }
.kennzahl { display: inline-block; background: #f1eee6; border-radius: 4px;
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
.kennzahl b { color: var(--akzent); }
.hinweis { font-size: .8rem; color: #6b6557; }
#gruppenUebersicht tr.fixiert td { background: #e6e3dc; color: #555; }
.varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
.variante {
border: 1px solid var(--rand); border-radius: 6px; background: white;
padding: .5rem .8rem; cursor: pointer; font-size: .85rem; text-align: left;
color: #2a2a24; min-width: 11rem;
}
.variante.aktiv { border-color: var(--akzent); background: #e6f0ea;
box-shadow: 0 0 0 1px var(--akzent); }
.variante b { color: var(--akzent); font-size: 1rem; }
.variante .zeile { display: block; color: #6b6557; }
</style>
</head>
<body>
<header>
<h1>Sitz &amp; Platz <small style="font-weight:normal">— Testoberfläche (Meilenstein 1)</small></h1>
<p>Session-basiert, nichts wird dauerhaft gespeichert (TTL 24 h). Sitzung: <code id="sitzungsId">wird angelegt…</code>
· <a href="/editor.html" style="color:#d6eade">grafischer Tisch-Editor</a></p>
</header>
<main>
<section>
<h2>1. Tische definieren</h2>
<p class="hinweis">Koordinaten sind Rasterpositionen (wie in tische.ini). Tische, die
näher als der Nachbar-Radius (1,5 Rastereinheiten) beieinander liegen, gelten
automatisch als Nachbarn — Gruppen, die geteilt werden müssen, kommen bevorzugt
an Nachbartische.</p>
<table id="tischTabelle">
<thead>
<tr><th>Tisch-Nr.</th><th>Plätze</th><th>X</th><th>Y</th><th></th></tr>
</thead>
<tbody></tbody>
</table>
<button type="button" onclick="tischZeile()">+ Tisch hinzufügen</button>
<button type="button" class="sekundaer" onclick="presetReihe()">Preset: 4er-Reihe</button>
<button type="button" class="sekundaer" onclick="presetRaster()">Preset: 3×3-Raster</button>
<br>
<label class="hinweis">Oder eine bestehende <code>tische.ini</code> auswählen
(z.&nbsp;B. aus <code>work/hochzeit</code>) — wird sofort geladen:</label>
<input type="file" id="tischDatei" accept=".ini,text/plain" onchange="tischDateiLaden()">
<br>
<button type="button" onclick="tischeSpeichern()">Tische speichern</button>
<div class="status" id="tischStatus"></div>
</section>
<section>
<h2>2. Gästeliste (CSV)</h2>
<p class="hinweis">Spalten: <code>Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl</code>
<code>Gruppe</code> hält Personen zusammen (leer = Einzelgast),
<code>Tisch</code> fixiert Gäste an einem Tisch (z.&nbsp;B. Brautpaar an Tisch 1),
<code>Anzahl</code> expandiert eine Zeile in mehrere Buchungen
(anonyme Sammelbuchung). Eine Datei auswählen (wird sofort übernommen)
<em>oder</em> hier direkt bearbeiten und übernehmen:</p>
<textarea id="csvText"></textarea>
<br>
<input type="file" id="csvDatei" accept=".csv,text/csv">
<br>
<button type="button" onclick="gaesteHochladen()">Gästeliste übernehmen</button>
<div class="status" id="gaesteStatus"></div>
<div id="gruppenUebersicht"></div>
</section>
<section>
<h2>3. Sitzordnung berechnen &amp; Varianten vergleichen</h2>
<p class="hinweis">Ein Lauf liefert bis zu drei <em>unterschiedliche</em> gute
Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter),
die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar.
Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf
„Berechnen“ startet einen frischen Lauf. Über das <em>Optimierungsprofil</em> lassen sich
Bewertungs-Vorgehen vergleichen (z.&nbsp;B. <code>default</code> vs.
<code>locker</code>): Profil wählen und neu berechnen.</p>
<label class="hinweis">Optimierungsprofil:
<select id="strafenWahl"></select></label>
<button type="button" id="berechnenKnopf" onclick="berechnen()" disabled>Sitzordnung berechnen</button>
<div class="status" id="rechnenStatus"></div>
<div class="varianten" id="variantenListe"></div>
<div id="kennzahlen"></div>
<p class="hinweis" style="margin-bottom:0">Im Raumplan mit der Maus über eine Person
fahren: Tooltip zeigt Name &amp; Gruppe, ein Kasten listet alle Namen dieser Gruppe,
und die Gruppe wird im Plan hervorgehoben.</p>
<div id="svgPlan"></div>
<div id="ergebnisTabellen"></div>
</section>
<div id="gruppeBox"></div>
</main>
<script>
'use strict';
let sitzung = null;
async function api(pfad, optionen) {
const antwort = await fetch(pfad, optionen);
const daten = await antwort.json().catch(() => null);
if (!antwort.ok) {
const detail = daten && daten.detail
? (typeof daten.detail === 'string' ? daten.detail : JSON.stringify(daten.detail, null, 2))
: antwort.statusText;
throw new Error(detail);
}
return daten;
}
function status(id, text, klasse) {
const el = document.getElementById(id);
el.textContent = text;
el.className = 'status ' + (klasse || '');
}
/* ------------------------------------------------ 1. Tische */
let geladenerRadius = 1.5; // zuletzt aus der Session geladener Nachbar-Radius
async function tischeAusSessionLaden() {
// Sektion 1 aus dem serverseitigen Session-Layout fuellen (z.B. was der
// grafische Editor gespeichert hat) statt fest das Preset zu zeigen.
try {
const r = await api('/sitzung/' + sitzung + '/tische');
if (r.tische && r.tische.length) {
tischeLeeren();
for (const t of r.tische) tischZeile(t.plaetze, t.x, t.y, t.id);
geladenerRadius = r.nachbarRadius || 1.5;
return;
}
} catch (e) { /* frische/abgelaufene Session — Preset als Fallback */ }
presetReihe();
}
function tischZeile(plaetze, x, y, tid) {
const tbody = document.querySelector('#tischTabelle tbody');
const nr = tid ?? tbody.children.length + 1;
const tr = document.createElement('tr');
if (tid !== undefined) tr.dataset.tid = tid; // echte Id aus tische.ini behalten
tr.innerHTML =
'<td class="tischNr">' + nr + '</td>' +
'<td><input type="number" min="1" max="100" value="' + (plaetze ?? 8) + '" class="plaetze"></td>' +
'<td><input type="number" step="1" value="' + (x ?? nr) + '" class="x"></td>' +
'<td><input type="number" step="1" value="' + (y ?? 1) + '" class="y"></td>' +
'<td><button type="button" class="sekundaer" onclick="this.closest(\'tr\').remove(); nummerieren()">✕</button></td>';
tbody.appendChild(tr);
}
function nummerieren() {
document.querySelectorAll('#tischTabelle tbody tr').forEach((tr, i) => {
if (tr.dataset.tid === undefined)
tr.querySelector('.tischNr').textContent = i + 1;
});
}
function tischeLeeren() {
document.querySelector('#tischTabelle tbody').innerHTML = '';
}
function presetReihe() {
tischeLeeren();
for (let i = 1; i <= 4; i++) tischZeile(5, i, 1);
}
function presetRaster() {
tischeLeeren();
for (let y = 1; y <= 3; y++)
for (let x = 1; x <= 3; x++)
tischZeile(8, x, y);
}
async function tischeSpeichern() {
const zeilen = [...document.querySelectorAll('#tischTabelle tbody tr')];
if (zeilen.length === 0) { status('tischStatus', 'Mindestens ein Tisch nötig.', 'fehler'); return; }
// Zeilen aus einer geladenen tische.ini behalten ihre echte Id
// (wichtig fuer VIP-Bindungen wie "Tisch 16"); neue Zeilen bekommen
// die naechste freie Id
const vergeben = new Set(zeilen.map(tr => parseInt(tr.dataset.tid, 10)).filter(n => !isNaN(n)));
let naechste = 1;
const freieId = () => { while (vergeben.has(naechste)) naechste++; vergeben.add(naechste); return naechste; };
const tische = zeilen.map(tr => ({
id: tr.dataset.tid !== undefined ? parseInt(tr.dataset.tid, 10) : freieId(),
plaetze: parseInt(tr.querySelector('.plaetze').value, 10),
x: parseFloat(tr.querySelector('.x').value),
y: parseFloat(tr.querySelector('.y').value),
}));
try {
const r = await api('/sitzung/' + sitzung + '/tische', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tische, nachbarRadius: geladenerRadius }),
});
const nachbarn = r.tische.map(t => 'Tisch ' + t.id + ' ↔ [' + t.nachbarn.join(', ') + ']').join(' ');
status('tischStatus', '✓ ' + r.tische.length + ' Tische, ' + r.plaetzeGesamt +
' Plätze gesamt. Abgeleitete Nachbarn: ' + nachbarn, 'ok');
berechnenFreigeben();
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
}
async function tischDateiLaden() {
const eingabe = document.getElementById('tischDatei');
if (eingabe.files.length === 0) {
status('tischStatus', 'Bitte zuerst eine tische.ini auswählen.', 'fehler');
return;
}
const form = new FormData();
form.append('datei', eingabe.files[0]);
try {
const r = await api('/sitzung/' + sitzung + '/tische-datei', { method: 'POST', body: form });
tischeLeeren();
for (const t of r.tische) tischZeile(t.plaetze, t.x, t.y, t.id);
status('tischStatus', '✓ ' + r.tische.length + ' Tische aus Datei übernommen (' +
r.plaetzeGesamt + ' Plätze) — Layout ist bereits gespeichert, ' +
'„Tische speichern“ ist nur nach Änderungen nötig.', 'ok');
berechnenFreigeben();
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
finally { eingabe.value = ''; } // damit dieselbe Datei erneut auswählbar ist
}
/* ------------------------------------------------ 2. Gäste */
const beispielCSV = `Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl
Britta;Beispiel;;Brautpaar;1;
Bernd;Beispiel;Dr.;Brautpaar;1;
Anna;Huber;;Familie Huber;;
Berta;Huber;;Familie Huber;;
Carl;Huber;;Familie Huber;;
Doris;Huber;;Familie Huber;;
Emil;Meier;;Familie Meier;;
Frieda;Meier;;Familie Meier;;
Gustav;Meier;;Familie Meier;;
Hanna;Schmidt;;Studienfreunde;;
Igor;Petrov;;Studienfreunde;;
Julia;Weber;;Studienfreunde;;
Gast;Kollegium;;Kollegen;;4
Karl;Einzelgast;;;;
`;
async function csvDateiGewaehlt(ev) {
// Ausgewaehlte CSV sofort sichtbar in die Textarea laden (Single Source):
// dekodieren wie das Backend (utf-8-sig, sonst cp1252), BOM strippen.
const datei = ev.target.files[0];
if (!datei) return;
const puffer = await datei.arrayBuffer();
let text;
try {
text = new TextDecoder('utf-8', { fatal: true }).decode(puffer);
} catch (e) {
text = new TextDecoder('windows-1252').decode(puffer); // Excel/Windows
}
text = text.replace(/^/, '');
document.getElementById('csvText').value = text;
ev.target.value = ''; // damit dieselbe Datei erneut auswählbar ist
// direkt uebernehmen: die ausgewaehlte Datei wird sofort geladen (die
// Textarea ist bereits befuellt und dient gaesteHochladen als Quelle)
await gaesteHochladen();
}
async function gaesteHochladen() {
// immer die (ggf. aus einer Datei befuellte) Textarea senden
const blob = new Blob([document.getElementById('csvText').value], { type: 'text/csv' });
const form = new FormData();
form.append('datei', blob, 'gaeste.csv');
try {
const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form });
status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length +
' Gruppen' + (r.fixiert ? ', dazu ' + r.fixiert +
' fixierte Personen an festen Tischen' : '') + '.', 'ok');
// regulaere Gruppen, danach die fest platzierten ("fixierten") Gruppen
// grau hinterlegt
const regulaer = r.gruppen.map(g =>
'<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td></tr>').join('');
const fixiert = (r.fixierte || []).map(f =>
'<tr class="fixiert"><td>' + f.name +
' <span class="hinweis">(fixiert an Tisch ' + f.tisch + ')</span></td><td>' +
f.anzahl + '</td></tr>').join('');
const html = '<table><thead><tr><th>Gruppe</th><th>Personen</th></tr></thead><tbody>' +
regulaer + fixiert + '</tbody></table>';
document.getElementById('gruppenUebersicht').innerHTML = html;
berechnenFreigeben();
} catch (e) { status('gaesteStatus', 'Fehler: ' + e.message, 'fehler'); }
}
/* ------------------------------------------------ 3. Berechnen */
async function strafprofileLaden() {
// Auswahl der Strafprofile aus strafen.cfg fuellen (z.B. default, locker)
try {
const r = await api('/strafprofile');
const sel = document.getElementById('strafenWahl');
sel.innerHTML = r.profile.map(p =>
'<option value="' + p + '"' + (p === r.vorgabe ? ' selected' : '') +
'>' + p + '</option>').join('');
} catch (e) { /* Auswahl bleibt leer -> Backend nutzt sein Default */ }
}
async function berechnenFreigeben() {
try {
const s = await api('/sitzung/' + sitzung);
document.getElementById('berechnenKnopf').disabled = !(s.tische > 0 && s.personen > 0);
} catch (e) { /* Session ggf. abgelaufen — beim Berechnen gemeldet */ }
}
let letzterLauf = null; // Antwort des letzten Berechnen-Laufs (mit Varianten)
async function berechnen() {
const knopf = document.getElementById('berechnenKnopf');
knopf.disabled = true;
status('rechnenStatus', 'Der genetische Algorithmus läuft…');
try {
const profil = document.getElementById('strafenWahl').value || 'default';
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3&strafen=' +
encodeURIComponent(profil), { method: 'POST' });
letzterLauf = r;
status('rechnenStatus', '✓ Lauf ' + r.lauf + ' (Optimierungsprofil „' + r.strafen + '“): ' +
r.varianten.length +
(r.varianten.length === 1
? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).'
: ' unterschiedliche Varianten zum Vergleich.'), 'ok');
knopf.textContent = 'Neuen Lauf berechnen';
zeigeVariantenListe(r);
await zeigeVariante(0);
} catch (e) {
status('rechnenStatus', 'Fehler: ' + e.message, 'fehler');
} finally {
knopf.disabled = false;
}
}
function zeigeVariantenListe(r) {
document.getElementById('variantenListe').innerHTML = r.varianten.map((v, i) =>
'<button type="button" class="variante" data-i="' + i + '" onclick="zeigeVariante(' + i + ')">' +
'<b>Variante ' + v.nr + '</b>' +
'<span class="zeile">Wertigkeit: ' + v.kennzahlen.wertigkeit + '</span>' +
'<span class="zeile">getrennte Gruppen: ' + v.kennzahlen.getrennteGruppen +
' (' + v.kennzahlen.teilungenGesamt + ' Teilungen)</span>' +
'<span class="zeile">allein sitzend: ' + v.kennzahlen.alleinePersonen + '</span>' +
'</button>').join('');
}
async function zeigeVariante(i) {
if (!letzterLauf || !letzterLauf.varianten[i]) return;
const v = letzterLauf.varianten[i];
document.querySelectorAll('#variantenListe .variante').forEach(el =>
el.classList.toggle('aktiv', parseInt(el.dataset.i, 10) === i));
document.getElementById('kennzahlen').innerHTML =
'<span class="kennzahl">Variante: <b>' + v.nr + '</b> (Lauf ' + letzterLauf.lauf + ')</span>' +
'<span class="kennzahl">Optimierungsprofil: <b>' + letzterLauf.strafen + '</b></span>' +
'<span class="kennzahl">Wertigkeit: <b>' + v.kennzahlen.wertigkeit + '</b></span>' +
'<span class="kennzahl">getrennte Gruppen: <b>' + v.kennzahlen.getrennteGruppen + '</b></span>' +
'<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>';
let html = '<table><thead><tr><th>Tisch</th><th>Belegung</th><th>Gäste (Gruppe)</th></tr></thead><tbody>';
for (const t of v.tische) {
const gaeste = t.personen.map(p =>
((p.titel ? p.titel + ' ' : '') + p.vorname + ' ' + p.nachname +
' <span class="hinweis">(' + p.gruppe + ')</span>')).join(', ');
html += '<tr><td>Tisch ' + t.nummer + '</td><td>' + t.besetzt + '/' + t.plaetze +
'</td><td>' + gaeste + '</td></tr>';
}
html += '</tbody></table>';
html += '<h2 style="margin-top:1rem">Gruppen</h2>' +
'<table><thead><tr><th>Gruppe</th><th>Personen</th><th>Teilungen</th><th>Strafpunkte</th></tr></thead><tbody>' +
v.gruppen.map(g => '<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td><td>' +
g.teilungen + '</td><td>' + g.wert + '</td></tr>').join('') +
'</tbody></table>';
document.getElementById('ergebnisTabellen').innerHTML = html;
// Raumplan der gewaehlten Variante — inline einbetten (nicht als <img>),
// damit die <title>-Tooltips und das Gruppen-Hover funktionieren
try {
const antwort = await fetch('/sitzung/' + sitzung + '/ergebnis.svg?variante=' + v.nr);
const svgText = await antwort.text();
const plan = document.getElementById('svgPlan');
plan.innerHTML = svgText.replace(/<\?xml[^>]*\?>/, ''); // XML-Prolog raus
gruppeInteraktion(plan.querySelector('svg'));
} catch (e) { /* SVG optional — Tabellen stehen schon da */ }
}
function escHtml(s) {
return (s || '').replace(/[&<>"]/g, c =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
}
// Hover ueber einen Personenkreis: alle Kreise derselben Gruppe hervorheben
// und einen Kasten mit der Namensliste dieser Gruppe zeigen. Die Namen kommen
// direkt aus den data-name/data-gruppe-Attributen der SVG (vom Kern erzeugt).
function gruppeInteraktion(svg) {
if (!svg) return;
const box = document.getElementById('gruppeBox');
let markiert = null;
svg.querySelectorAll('.person').forEach(kreis => {
kreis.addEventListener('mouseenter', () => {
const gruppe = kreis.dataset.gruppe || '';
const gleiche = svg.querySelectorAll(
'.person[data-gruppe="' + (window.CSS ? CSS.escape(gruppe) : gruppe) + '"]');
gleiche.forEach(k => k.classList.add('aktiv'));
markiert = gleiche;
const namen = [...gleiche].map(k => k.dataset.name || '');
box.innerHTML = '<b>' + escHtml(gruppe) + ' (' + namen.length + ')</b>' +
namen.map(escHtml).join('<br>');
box.style.display = 'block';
});
kreis.addEventListener('mouseleave', () => {
if (markiert) markiert.forEach(k => k.classList.remove('aktiv'));
markiert = null;
box.style.display = 'none';
});
});
svg.addEventListener('mousemove', e => {
box.style.left = (e.clientX + 14) + 'px';
box.style.top = (e.clientY + 14) + 'px';
});
}
/* ------------------------------------------------ Start */
window.addEventListener('DOMContentLoaded', async () => {
document.getElementById('csvText').value = beispielCSV;
document.getElementById('csvDatei').addEventListener('change', csvDateiGewaehlt);
strafprofileLaden();
try {
sitzung = await sitzungHolen();
document.getElementById('sitzungsId').textContent = sitzung;
// Tische aus der (ggf. mit dem Editor geteilten) Session laden;
// Preset nur, wenn die Session noch keine Tische hat
await tischeAusSessionLaden();
berechnenFreigeben();
} catch (e) {
document.getElementById('sitzungsId').textContent = 'Fehler: ' + e.message;
presetReihe();
}
});
// Session mit dem grafischen Editor (editor.html) ueber localStorage teilen:
// eine bestehende, noch gueltige Sitzung wiederverwenden statt jedes Mal
// eine neue anzulegen — so wirkt ein im Editor gebautes Layout hier weiter.
async function sitzungHolen() {
let id = localStorage.getItem('platzSitzung');
if (id) {
const ok = await fetch('/sitzung/' + id).then(r => r.ok).catch(() => false);
if (ok) return id;
}
const r = await api('/sitzung', { method: 'POST' });
localStorage.setItem('platzSitzung', r.id);
return r.id;
}
</script>
</body>
</html>