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>
This commit is contained in:
+14
-3
@@ -84,6 +84,11 @@ class CSVConfig:
|
|||||||
VIPGruppe = Gruppe( 0 )
|
VIPGruppe = Gruppe( 0 )
|
||||||
VipHash = {}
|
VipHash = {}
|
||||||
GruppenNamen = {}
|
GruppenNamen = {}
|
||||||
|
# fest platzierte ("fixierte") Gruppen fuer die Anzeige: Name ->
|
||||||
|
# {'tisch': Tischnummer, 'anzahl': Personenzahl}. VIPs laufen nicht
|
||||||
|
# durch die GA-Platzierung, sollen in der Gaesteliste aber sichtbar
|
||||||
|
# (grau hinterlegt) erscheinen.
|
||||||
|
FixierteGruppen = {}
|
||||||
|
|
||||||
# Gruppen entstehen beim ersten Auftreten ihres Namens;
|
# Gruppen entstehen beim ersten Auftreten ihres Namens;
|
||||||
# Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe
|
# Einzelpersonen (leerer Gruppenname) bekommen eine eigene Gruppe
|
||||||
@@ -126,9 +131,13 @@ class CSVConfig:
|
|||||||
PID = PID + 1
|
PID = PID + 1
|
||||||
P = Person( PID, Vorname, Nachname, Titel )
|
P = Person( PID, Vorname, Nachname, Titel )
|
||||||
if TischNr is not None:
|
if TischNr is not None:
|
||||||
# VIP: fester Tisch, keine GA-Platzierung
|
# fixiert: fester Tisch, keine GA-Platzierung
|
||||||
VipHash[ PID ] = TischNr
|
VipHash[ PID ] = TischNr
|
||||||
VIPGruppe.Person_dazu( P )
|
VIPGruppe.Person_dazu( P )
|
||||||
|
Name = GruppenName if GruppenName else 'Fixiert Tisch %d' % TischNr
|
||||||
|
Eintrag = FixierteGruppen.setdefault(
|
||||||
|
Name, { 'tisch': TischNr, 'anzahl': 0 } )
|
||||||
|
Eintrag['anzahl'] = Eintrag['anzahl'] + 1
|
||||||
else:
|
else:
|
||||||
PN.PersonAdd( P )
|
PN.PersonAdd( P )
|
||||||
if GruppenName == '':
|
if GruppenName == '':
|
||||||
@@ -155,7 +164,7 @@ class CSVConfig:
|
|||||||
if G.Anzahl() > 0:
|
if G.Anzahl() > 0:
|
||||||
GN.Gruppe_dazu( G )
|
GN.Gruppe_dazu( G )
|
||||||
|
|
||||||
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen ]
|
return [ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ]
|
||||||
|
|
||||||
def laden(self, FileName):
|
def laden(self, FileName):
|
||||||
"""lese alle Eingangsdaten aus einer CSV-Datei ein"""
|
"""lese alle Eingangsdaten aus einer CSV-Datei ein"""
|
||||||
@@ -179,12 +188,14 @@ Einzel;Gast;;;;
|
|||||||
"""
|
"""
|
||||||
print("\n-- Parse Beispiel-CSV:")
|
print("\n-- Parse Beispiel-CSV:")
|
||||||
C = CSVConfig()
|
C = CSVConfig()
|
||||||
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = C.HandleGaesteliste( Beispiel )
|
[ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
|
||||||
|
C.HandleGaesteliste( Beispiel )
|
||||||
print(PN)
|
print(PN)
|
||||||
print(GN)
|
print(GN)
|
||||||
print(VIPGruppe)
|
print(VIPGruppe)
|
||||||
print(VipHash)
|
print(VipHash)
|
||||||
print(GruppenNamen)
|
print(GruppenNamen)
|
||||||
|
print(FixierteGruppen)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+64
-9
@@ -44,6 +44,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from ga import Farm, Zyklus
|
from ga import Farm, Zyklus
|
||||||
from CSVConfig import CSVConfig
|
from CSVConfig import CSVConfig
|
||||||
|
from Farben import Farbschema
|
||||||
from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische
|
from Strukturdaten import Sitzplatzverteilung, Strafliste, Tisch, Tische
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
@@ -62,11 +63,43 @@ ZYKLUSDATEI = os.path.join( CFGDIR, 'zyklus.cfg' )
|
|||||||
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
|
STRAFENDATEI = os.path.join( CFGDIR, 'strafen.cfg' )
|
||||||
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
|
ZYKLUSART = os.environ.get( 'PLATZ_ZYKLUSART', 'Adaptiv' )
|
||||||
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
|
STRAFENART = os.environ.get( 'PLATZ_STRAFENART', 'default' )
|
||||||
|
FARBENDATEI = os.path.join( CFGDIR, 'farben.cfg' )
|
||||||
|
FARBENART = os.environ.get( 'PLATZ_FARBENART', 'grosse_gruppen' )
|
||||||
|
|
||||||
GAZyklus = Zyklus()
|
GAZyklus = Zyklus()
|
||||||
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
|
GAZyklus.laden( ZYKLUSDATEI, ZYKLUSART )
|
||||||
Strafen = Strafliste()
|
|
||||||
Strafen.laden( STRAFENDATEI, STRAFENART )
|
# Strafprofile werden bei Bedarf geladen und gecacht, damit sich in der GUI
|
||||||
|
# verschiedene Profile (z.B. 'default' vs 'locker') auswaehlen und vergleichen
|
||||||
|
# lassen. Das Default-Profil wird schon beim Start geladen (faellt frueh auf,
|
||||||
|
# falls die Datei/Sektion kaputt ist).
|
||||||
|
StrafProfile = {}
|
||||||
|
|
||||||
|
|
||||||
|
def strafprofil(Name):
|
||||||
|
"""liefert (gecacht) die Strafliste einer Sektion aus strafen.cfg;
|
||||||
|
wirft ValueError bei unbekannter Sektion"""
|
||||||
|
if Name not in StrafProfile:
|
||||||
|
SL = Strafliste()
|
||||||
|
SL.laden( STRAFENDATEI, Name )
|
||||||
|
StrafProfile[Name] = SL
|
||||||
|
return StrafProfile[Name]
|
||||||
|
|
||||||
|
|
||||||
|
def strafprofil_namen():
|
||||||
|
"""alle in strafen.cfg vorhandenen Profilnamen (fuer die GUI-Auswahl)"""
|
||||||
|
cfg = configparser.ConfigParser()
|
||||||
|
cfg.read( STRAFENDATEI )
|
||||||
|
return cfg.sections()
|
||||||
|
|
||||||
|
|
||||||
|
strafprofil( STRAFENART ) # Default vorwaermen / frueh scheitern
|
||||||
|
# Farbprofil fuer die SVG-Ausgabe; faellt bei fehlender Datei/Sektion auf die
|
||||||
|
# eingebaute Palette zurueck (Farbschema=None), damit der Server trotzdem laeuft.
|
||||||
|
try:
|
||||||
|
Farben = Farbschema().laden( FARBENDATEI, FARBENART )
|
||||||
|
except (ValueError, OSError):
|
||||||
|
Farben = None
|
||||||
|
|
||||||
# ---------------------------------------------------------------- Sessions
|
# ---------------------------------------------------------------- Sessions
|
||||||
SESSION_TTL_SEKUNDEN = 24 * 3600
|
SESSION_TTL_SEKUNDEN = 24 * 3600
|
||||||
@@ -160,7 +193,7 @@ def _VarianteJSON(SV, GruppenNamen, Nr):
|
|||||||
G = SV.Gruppen.GruppeVonPid( P.Id )
|
G = SV.Gruppen.GruppeVonPid( P.Id )
|
||||||
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
|
return GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id )
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return 'VIP'
|
return 'fixiert'
|
||||||
|
|
||||||
TischListe = []
|
TischListe = []
|
||||||
for T in SV.Tische:
|
for T in SV.Tische:
|
||||||
@@ -220,6 +253,7 @@ def _ErgebnisJSON(S):
|
|||||||
GruppenNamen = S.get( 'gruppenNamen', {} )
|
GruppenNamen = S.get( 'gruppenNamen', {} )
|
||||||
return {
|
return {
|
||||||
'lauf': S['laeufe'],
|
'lauf': S['laeufe'],
|
||||||
|
'strafen': S.get( 'strafen', STRAFENART ), # verwendetes Strafprofil
|
||||||
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
|
'varianten': [ _VarianteJSON( SV, GruppenNamen, Nr + 1 )
|
||||||
for Nr, SV in enumerate( S['ergebnisse'] ) ],
|
for Nr, SV in enumerate( S['ergebnisse'] ) ],
|
||||||
}
|
}
|
||||||
@@ -389,7 +423,7 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
|
|||||||
Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback
|
Text = Rohdaten.decode( 'cp1252' ) # Excel/Windows-Fallback
|
||||||
|
|
||||||
try:
|
try:
|
||||||
[ PN, GN, VIPGruppe, VipHash, GruppenNamen ] = \
|
[ PN, GN, VIPGruppe, VipHash, GruppenNamen, FixierteGruppen ] = \
|
||||||
CSVConfig().HandleGaesteliste( Text )
|
CSVConfig().HandleGaesteliste( Text )
|
||||||
except ValueError as Fehler:
|
except ValueError as Fehler:
|
||||||
raise HTTPException( status_code=400, detail=str(Fehler) )
|
raise HTTPException( status_code=400, detail=str(Fehler) )
|
||||||
@@ -407,12 +441,15 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'personen': GN.NLeute,
|
'personen': GN.NLeute,
|
||||||
'vips': VIPGruppe.Anzahl(),
|
'fixiert': VIPGruppe.Anzahl(),
|
||||||
'gruppen': [ {
|
'gruppen': [ {
|
||||||
'id': G.Id,
|
'id': G.Id,
|
||||||
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
|
'name': GruppenNamen.get( G.Id, 'Gruppe %s' % G.Id ),
|
||||||
'anzahl': G.Anzahl(),
|
'anzahl': G.Anzahl(),
|
||||||
} for G in GN ],
|
} for G in GN ],
|
||||||
|
# fest platzierte Gruppen (fuer die grau hinterlegte Anzeige)
|
||||||
|
'fixierte': [ { 'name': Name, 'tisch': Info['tisch'], 'anzahl': Info['anzahl'] }
|
||||||
|
for Name, Info in FixierteGruppen.items() ],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -420,9 +457,14 @@ def PersonenHochladen(SitzungsId: str, datei: UploadFile = File(...)):
|
|||||||
def Berechnen(SitzungsId: str,
|
def Berechnen(SitzungsId: str,
|
||||||
varianten: int = Query( default=3, ge=1, le=5,
|
varianten: int = Query( default=3, ge=1, le=5,
|
||||||
description='wieviele unterschiedliche Varianten der Lauf '
|
description='wieviele unterschiedliche Varianten der Lauf '
|
||||||
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' )):
|
'zurueckgeben soll (aus Farm.TopN, dedupliziert)' ),
|
||||||
|
strafen: str = Query( default=STRAFENART,
|
||||||
|
description='Name des Strafpunkte-Profils (Sektion in strafen.cfg), '
|
||||||
|
'z.B. default oder locker - zum Vergleich waehlbar' )):
|
||||||
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
|
"""startet die GA-Optimierung; jeder Aufruf ist ein neuer Lauf und
|
||||||
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen"""
|
liefert bis zu 'varianten' unterschiedliche gute Sitzordnungen. Das
|
||||||
|
Strafpunkte-Profil ('strafen') steuert die Bewertung waehrend der
|
||||||
|
Optimierung - so lassen sich z.B. default und locker vergleichen."""
|
||||||
S = _GibSession( SitzungsId )
|
S = _GibSession( SitzungsId )
|
||||||
if S['tische'] is None:
|
if S['tische'] is None:
|
||||||
raise HTTPException( status_code=400,
|
raise HTTPException( status_code=400,
|
||||||
@@ -454,9 +496,14 @@ def Berechnen(SitzungsId: str,
|
|||||||
detail='Tisch %d hat %d Plaetze, aber %d VIPs sind '
|
detail='Tisch %d hat %d Plaetze, aber %d VIPs sind '
|
||||||
'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) )
|
'dort fest gesetzt' % (Tid, T.Plaetze, AnzahlVIPs) )
|
||||||
|
|
||||||
|
try:
|
||||||
|
SL = strafprofil( strafen )
|
||||||
|
except ValueError as Fehler:
|
||||||
|
raise HTTPException( status_code=400, detail=str(Fehler) )
|
||||||
|
|
||||||
try:
|
try:
|
||||||
GAFarm = Farm( GAZyklus, Sitzplatzverteilung,
|
GAFarm = Farm( GAZyklus, Sitzplatzverteilung,
|
||||||
Tische=TE, Gruppen=P['GN'], Strafen=Strafen,
|
Tische=TE, Gruppen=P['GN'], Strafen=SL,
|
||||||
VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] )
|
VIPListe=P['VipHash'], VIPs=P['VIPGruppe'] )
|
||||||
except IndexError as Fehler:
|
except IndexError as Fehler:
|
||||||
raise HTTPException( status_code=400, detail=str(Fehler) )
|
raise HTTPException( status_code=400, detail=str(Fehler) )
|
||||||
@@ -481,9 +528,17 @@ def Berechnen(SitzungsId: str,
|
|||||||
|
|
||||||
S['ergebnisse'] = Ergebnisse
|
S['ergebnisse'] = Ergebnisse
|
||||||
S['laeufe'] = S['laeufe'] + 1
|
S['laeufe'] = S['laeufe'] + 1
|
||||||
|
S['strafen'] = strafen # fuer die Anzeige, welches Profil den Lauf ergab
|
||||||
return _ErgebnisJSON( S )
|
return _ErgebnisJSON( S )
|
||||||
|
|
||||||
|
|
||||||
|
@app.get( '/strafprofile' )
|
||||||
|
def Strafprofile():
|
||||||
|
"""verfuegbare Strafpunkte-Profile (Sektionen in strafen.cfg) fuer die
|
||||||
|
Auswahl in der GUI; 'vorgabe' ist das voreingestellte Profil."""
|
||||||
|
return { 'profile': strafprofil_namen(), 'vorgabe': STRAFENART }
|
||||||
|
|
||||||
|
|
||||||
@app.get( '/sitzung/{SitzungsId}/ergebnis' )
|
@app.get( '/sitzung/{SitzungsId}/ergebnis' )
|
||||||
def Ergebnis(SitzungsId: str):
|
def Ergebnis(SitzungsId: str):
|
||||||
"""alle Varianten des letzten Laufs als JSON (beste zuerst)"""
|
"""alle Varianten des letzten Laufs als JSON (beste zuerst)"""
|
||||||
@@ -513,7 +568,7 @@ def ErgebnisSVG(SitzungsId: str,
|
|||||||
os.close( Handle )
|
os.close( Handle )
|
||||||
try:
|
try:
|
||||||
S['ergebnisse'][variante - 1].alsSVG( TempPfad,
|
S['ergebnisse'][variante - 1].alsSVG( TempPfad,
|
||||||
GruppenNamen=S.get( 'gruppenNamen', {} ) )
|
GruppenNamen=S.get( 'gruppenNamen', {} ), Farbschema=Farben )
|
||||||
fsock = open( TempPfad, 'r' )
|
fsock = open( TempPfad, 'r' )
|
||||||
SVG = fsock.read()
|
SVG = fsock.read()
|
||||||
fsock.close()
|
fsock.close()
|
||||||
|
|||||||
+46
-17
@@ -61,6 +61,7 @@
|
|||||||
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
|
padding: .3rem .7rem; margin: .15rem .3rem .15rem 0; font-size: .9rem; }
|
||||||
.kennzahl b { color: var(--akzent); }
|
.kennzahl b { color: var(--akzent); }
|
||||||
.hinweis { font-size: .8rem; color: #6b6557; }
|
.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; }
|
.varianten { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .6rem; }
|
||||||
.variante {
|
.variante {
|
||||||
border: 1px solid var(--rand); border-radius: 6px; background: white;
|
border: 1px solid var(--rand); border-radius: 6px; background: white;
|
||||||
@@ -97,10 +98,9 @@
|
|||||||
<button type="button" class="sekundaer" onclick="presetReihe()">Preset: 4er-Reihe</button>
|
<button type="button" class="sekundaer" onclick="presetReihe()">Preset: 4er-Reihe</button>
|
||||||
<button type="button" class="sekundaer" onclick="presetRaster()">Preset: 3×3-Raster</button>
|
<button type="button" class="sekundaer" onclick="presetRaster()">Preset: 3×3-Raster</button>
|
||||||
<br>
|
<br>
|
||||||
<label class="hinweis">Oder eine bestehende <code>tische.ini</code> laden
|
<label class="hinweis">Oder eine bestehende <code>tische.ini</code> auswählen
|
||||||
(z. B. aus <code>work/hochzeit</code>):</label>
|
(z. B. aus <code>work/hochzeit</code>) — wird sofort geladen:</label>
|
||||||
<input type="file" id="tischDatei" accept=".ini,text/plain">
|
<input type="file" id="tischDatei" accept=".ini,text/plain" onchange="tischDateiLaden()">
|
||||||
<button type="button" class="sekundaer" onclick="tischDateiLaden()">tische.ini laden</button>
|
|
||||||
<br>
|
<br>
|
||||||
<button type="button" onclick="tischeSpeichern()">Tische speichern</button>
|
<button type="button" onclick="tischeSpeichern()">Tische speichern</button>
|
||||||
<div class="status" id="tischStatus"></div>
|
<div class="status" id="tischStatus"></div>
|
||||||
@@ -110,9 +110,10 @@
|
|||||||
<h2>2. Gästeliste (CSV)</h2>
|
<h2>2. Gästeliste (CSV)</h2>
|
||||||
<p class="hinweis">Spalten: <code>Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl</code> —
|
<p class="hinweis">Spalten: <code>Vorname;Nachname;Titel;Gruppe;Tisch;Anzahl</code> —
|
||||||
<code>Gruppe</code> hält Personen zusammen (leer = Einzelgast),
|
<code>Gruppe</code> hält Personen zusammen (leer = Einzelgast),
|
||||||
<code>Tisch</code> setzt VIPs fest (z. B. Brautpaar an Tisch 1),
|
<code>Tisch</code> fixiert Gäste an einem Tisch (z. B. Brautpaar an Tisch 1),
|
||||||
<code>Anzahl</code> expandiert eine Zeile in mehrere Buchungen
|
<code>Anzahl</code> expandiert eine Zeile in mehrere Buchungen
|
||||||
(anonyme Sammelbuchung). Datei hochladen <em>oder</em> direkt hier bearbeiten:</p>
|
(anonyme Sammelbuchung). Eine Datei auswählen (wird sofort übernommen)
|
||||||
|
<em>oder</em> hier direkt bearbeiten und übernehmen:</p>
|
||||||
<textarea id="csvText"></textarea>
|
<textarea id="csvText"></textarea>
|
||||||
<br>
|
<br>
|
||||||
<input type="file" id="csvDatei" accept=".csv,text/csv">
|
<input type="file" id="csvDatei" accept=".csv,text/csv">
|
||||||
@@ -128,7 +129,11 @@
|
|||||||
Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter),
|
Sitzordnungen zum Vergleich — die Wertigkeit (0 = perfekt, negativer = schlechter),
|
||||||
die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar.
|
die Zahl getrennter Gruppen und allein sitzende Personen machen sie vergleichbar.
|
||||||
Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf
|
Variante anklicken, um Tische und Raumplan umzuschalten; jeder weitere Klick auf
|
||||||
„Berechnen“ startet einen frischen Lauf.</p>
|
„Berechnen“ startet einen frischen Lauf. Über das <em>Optimierungsprofil</em> lassen sich
|
||||||
|
Bewertungs-Vorgehen vergleichen (z. 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>
|
<button type="button" id="berechnenKnopf" onclick="berechnen()" disabled>Sitzordnung berechnen</button>
|
||||||
<div class="status" id="rechnenStatus"></div>
|
<div class="status" id="rechnenStatus"></div>
|
||||||
<div class="varianten" id="variantenListe"></div>
|
<div class="varianten" id="variantenListe"></div>
|
||||||
@@ -259,6 +264,7 @@ async function tischDateiLaden() {
|
|||||||
'„Tische speichern“ ist nur nach Änderungen nötig.', 'ok');
|
'„Tische speichern“ ist nur nach Änderungen nötig.', 'ok');
|
||||||
berechnenFreigeben();
|
berechnenFreigeben();
|
||||||
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
|
} catch (e) { status('tischStatus', 'Fehler: ' + e.message, 'fehler'); }
|
||||||
|
finally { eingabe.value = ''; } // damit dieselbe Datei erneut auswählbar ist
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------ 2. Gäste */
|
/* ------------------------------------------------ 2. Gäste */
|
||||||
@@ -293,11 +299,10 @@ async function csvDateiGewaehlt(ev) {
|
|||||||
}
|
}
|
||||||
text = text.replace(/^/, '');
|
text = text.replace(/^/, '');
|
||||||
document.getElementById('csvText').value = text;
|
document.getElementById('csvText').value = text;
|
||||||
// stale Ausgabe eines frueheren Uploads entfernen
|
ev.target.value = ''; // damit dieselbe Datei erneut auswählbar ist
|
||||||
document.getElementById('gruppenUebersicht').innerHTML = '';
|
// direkt uebernehmen: die ausgewaehlte Datei wird sofort geladen (die
|
||||||
status('gaesteStatus', 'Datei „' + datei.name + '“ geladen — zum Übernehmen ' +
|
// Textarea ist bereits befuellt und dient gaesteHochladen als Quelle)
|
||||||
'„Gästeliste übernehmen“ klicken.', 'ok');
|
await gaesteHochladen();
|
||||||
ev.target.value = ''; // Textarea ist ab jetzt die alleinige Quelle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gaesteHochladen() {
|
async function gaesteHochladen() {
|
||||||
@@ -308,16 +313,35 @@ async function gaesteHochladen() {
|
|||||||
try {
|
try {
|
||||||
const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form });
|
const r = await api('/sitzung/' + sitzung + '/personen', { method: 'POST', body: form });
|
||||||
status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length +
|
status('gaesteStatus', '✓ ' + r.personen + ' Personen in ' + r.gruppen.length +
|
||||||
' Gruppen, dazu ' + r.vips + ' VIPs mit festem Tisch.', 'ok');
|
' 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>' +
|
const html = '<table><thead><tr><th>Gruppe</th><th>Personen</th></tr></thead><tbody>' +
|
||||||
r.gruppen.map(g => '<tr><td>' + g.name + '</td><td>' + g.anzahl + '</td></tr>').join('') +
|
regulaer + fixiert + '</tbody></table>';
|
||||||
'</tbody></table>';
|
|
||||||
document.getElementById('gruppenUebersicht').innerHTML = html;
|
document.getElementById('gruppenUebersicht').innerHTML = html;
|
||||||
berechnenFreigeben();
|
berechnenFreigeben();
|
||||||
} catch (e) { status('gaesteStatus', 'Fehler: ' + e.message, 'fehler'); }
|
} catch (e) { status('gaesteStatus', 'Fehler: ' + e.message, 'fehler'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------ 3. Berechnen */
|
/* ------------------------------------------------ 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() {
|
async function berechnenFreigeben() {
|
||||||
try {
|
try {
|
||||||
const s = await api('/sitzung/' + sitzung);
|
const s = await api('/sitzung/' + sitzung);
|
||||||
@@ -332,9 +356,12 @@ async function berechnen() {
|
|||||||
knopf.disabled = true;
|
knopf.disabled = true;
|
||||||
status('rechnenStatus', 'Der genetische Algorithmus läuft…');
|
status('rechnenStatus', 'Der genetische Algorithmus läuft…');
|
||||||
try {
|
try {
|
||||||
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3', { method: 'POST' });
|
const profil = document.getElementById('strafenWahl').value || 'default';
|
||||||
|
const r = await api('/sitzung/' + sitzung + '/berechnen?varianten=3&strafen=' +
|
||||||
|
encodeURIComponent(profil), { method: 'POST' });
|
||||||
letzterLauf = r;
|
letzterLauf = r;
|
||||||
status('rechnenStatus', '✓ Lauf ' + r.lauf + ': ' + r.varianten.length +
|
status('rechnenStatus', '✓ Lauf ' + r.lauf + ' (Optimierungsprofil „' + r.strafen + '“): ' +
|
||||||
|
r.varianten.length +
|
||||||
(r.varianten.length === 1
|
(r.varianten.length === 1
|
||||||
? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).'
|
? ' Variante gefunden (der Lauf konvergierte auf eine Sitzordnung).'
|
||||||
: ' unterschiedliche Varianten zum Vergleich.'), 'ok');
|
: ' unterschiedliche Varianten zum Vergleich.'), 'ok');
|
||||||
@@ -367,6 +394,7 @@ async function zeigeVariante(i) {
|
|||||||
|
|
||||||
document.getElementById('kennzahlen').innerHTML =
|
document.getElementById('kennzahlen').innerHTML =
|
||||||
'<span class="kennzahl">Variante: <b>' + v.nr + '</b> (Lauf ' + letzterLauf.lauf + ')</span>' +
|
'<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">Wertigkeit: <b>' + v.kennzahlen.wertigkeit + '</b></span>' +
|
||||||
'<span class="kennzahl">getrennte Gruppen: <b>' + v.kennzahlen.getrennteGruppen + '</b></span>' +
|
'<span class="kennzahl">getrennte Gruppen: <b>' + v.kennzahlen.getrennteGruppen + '</b></span>' +
|
||||||
'<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>';
|
'<span class="kennzahl">allein sitzend: <b>' + v.kennzahlen.alleinePersonen + '</b></span>';
|
||||||
@@ -439,6 +467,7 @@ function gruppeInteraktion(svg) {
|
|||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
document.getElementById('csvText').value = beispielCSV;
|
document.getElementById('csvText').value = beispielCSV;
|
||||||
document.getElementById('csvDatei').addEventListener('change', csvDateiGewaehlt);
|
document.getElementById('csvDatei').addEventListener('change', csvDateiGewaehlt);
|
||||||
|
strafprofileLaden();
|
||||||
try {
|
try {
|
||||||
sitzung = await sitzungHolen();
|
sitzung = await sitzungHolen();
|
||||||
document.getElementById('sitzungsId').textContent = sitzung;
|
document.getElementById('sitzungsId').textContent = sitzung;
|
||||||
|
|||||||
Reference in New Issue
Block a user