diff --git a/admin.html b/admin.html index 9dde665..ca0dd56 100644 --- a/admin.html +++ b/admin.html @@ -102,6 +102,7 @@
Stammvertreter
Mitglieder
+
Agenda
Einstellungen
@@ -146,6 +147,27 @@ + +
+
+
+
Agenda-Folien (Online-Meeting)
+
+ + +
+
+

+ Punkte fĂŒr den Online-Meeting-Ablauf (z.B. BegrĂŒĂŸung, DSGVO, BNI-Zahlen, Events) – als Bild oder als Live-Einbettung + aus Zoho Show. Die Reihenfolge hier ist nur der Katalog; welche Punkte in welcher Reihenfolge live gezeigt werden, + stellt die Moderatorin frei im Steuerpult (index.html) ein. +

+
+
Lade

+ +
+
+
@@ -291,6 +313,42 @@
+ + + -
-
Reihenfolge (Ziehen zum Sortieren)
- -
- â ż -
AM
-
Anna MĂŒller
Steuerberatung
- NÄCHSTER - - +
+ Steuerungs-Fenster mit Mitgliederliste +
+
+ + +
+

Redezeit anpassen

+

Standard-Redezeit fĂŒr alle Mitglieder oder abweichende Zeit pro Person setzen.

+ +

A) Standardzeit fĂŒr alle

+
+

Oben in der Kopfzeile des Steuerungs-Fensters auf das Zeit-Feld „⏱ 0:50" (bzw. den aktuellen Wert) klicken.

+

Im Dialog die neue Zeit im Format M:SS (z. B. 1:00) oder als Minuten (z. B. 1) eingeben.

+

Mit OK bestĂ€tigen — die neue Zeit gilt sofort fĂŒr alle Mitglieder ohne individuelle Zeit.

+
+ +
+
+
+
+
+ Dialog – Standard-Redezeit setzen
- -
- â ż -
KS
-
Klaus Schmidt
IT-Systeme
- - +
+ Prompt: Standard-Redezeit fĂŒr alle Mitglieder
- -
- â ż -
SL
-
Sandra Lang
Marketing
- - +
+ +

B) Individuelle Zeit pro Mitglied

+
+

In der Mitgliederliste rechts neben jedem Namen erscheint ein kleines rot umrandetes Zeit-Feld.

+

Auf dieses Feld klicken und den abweichenden Wert eingeben (leer lassen = Standardzeit ĂŒbernehmen).

+

NĂŒtzlich z. B. fĂŒr neue Mitglieder mit lĂ€ngerer Vorstellung oder fĂŒr EhrengĂ€ste.

+
+ +
+
+
+
+
+ Individuelles Zeit-Feld pro Mitglied +
+
+ Kleines Zeit-Feld neben dem Mitglied
-
â†ș = Vertretung eintragen  Â·  đŸš« = Als abwesend markieren
@@ -359,22 +396,8 @@
Steuerung – Vertretung eintragen
-
-
-

Vertreter fĂŒr Klaus Schmidt

-
STAMMVERTRETER WÄHLEN
-
-
MF
-
Max Fischer
Fischer GmbH · Versicherung
-
-
-
LB
-
Lisa Berger
Berger & Co · Marketing
-
-
ODER MANUELL EINGEBEN
-
Name des Vertreters 

-
Firma 

-
+
+ Modal: Vertretung fĂŒr Mitglied erfassen
@@ -388,10 +411,38 @@
-

Tab „Besucher" öffnen.

-

Auf „+ Besucher hinzufĂŒgen" klicken.

-

Name und optional Firma eingeben und speichern.

-

Besucher erscheinen am Ende der Vorstellungsrunde nach allen Mitgliedern.

+

In der rechten Seitenleiste den Tab „Besucher" öffnen.

+

Name eingeben, optional Firma, Fachgebiet, „Eingeladen von" (Mitglied wĂ€hlen) und Foto-URL ergĂ€nzen.

+

Auf „+ Besucher hinzufĂŒgen" klicken — der Besucher erscheint sofort in der Reihenfolge.

+

Besucher werden am Ende der Vorstellungsrunde nach allen Mitgliedern eingeblendet.

+
+
+ +
+
+
+
+
+ Steuerung – Besucher hinzufĂŒgen +
+
+ Formular: Besucher hinzufĂŒgen +
+
+ +
+ Schnellerfassung per PDF: Wenn du eine Besucher-/Vertreterliste als PDF vorliegen hast (z. B. aus dem Chapter-Portal), klick oben in der Kopfzeile auf „📄 PDF importieren" und lege die PDF in das Feld — EintrĂ€ge werden automatisch als Zeilen erkannt und du kannst sie vor dem Import noch anpassen. +
+ +
+
+
+
+
+ Steuerung – Schnellerfassung per PDF +
+
+ Modal: Besucher- und Vertreterliste per PDF importieren
@@ -408,7 +459,7 @@
- Das Admin-Panel öffnest du ĂŒber http://localhost:8181/admin.html — am einfachsten ĂŒber den Link im Steuerungs-Fenster unter dem Tab „Admin". + Das Admin-Panel öffnest du am einfachsten ĂŒber den Link im Steuerungs-Fenster unter dem Tab „Admin" (Button „⚙ Admin-Panel öffnen") — das funktioniert unabhĂ€ngig davon, wie das Tool gestartet wurde. Alternativ, falls das Tool ĂŒber start.command (Terminal) gestartet wurde, ist es auch direkt unter http://localhost:8181/admin.html erreichbar — diese Adresse funktioniert nur, wenn das Tool auf diesem Weg lĂ€uft.
@@ -422,37 +473,8 @@
-
-
BNI Admin-Panel · BNI Chapter Sonne
-
-
Stammvertreter
-
Mitglieder
-
Einstellungen
-
-
-
- Mitglieder -
- 🔄 Vom Server laden - đŸ“€ Auf Server hochladen -
-
-
-
AM
-
Anna MĂŒller
Steuerberatung
- ✓ Foto -
-
-
KS
-
Klaus Schmidt
IT-Systeme
- + Foto -
-
-
SL
-
Sandra Lang
Marketing
- ✓ Foto -
-
+
+ Admin-Panel Mitglieder-Tab
@@ -495,13 +517,18 @@
  • Mitglieder vom Server laden
  • Fotos auf Server hochladen
  • Stammvertreter sichern
  • +
  • Agenda-Punkte mit Live-Einbettung anzeigen (Zoho Show)
  • - Empfehlung: Vor jedem Meeting einmal kurz „Vom Server laden" klicken — so hast du immer die aktuellste Mitgliederliste & Fotos, auch wenn beim Meeting kein Internet verfĂŒgbar ist. + Empfehlung: Vor jedem Meeting einmal kurz „Vom Server laden" klicken — vorausgesetzt, die Liste wurde vorher aktualisiert und auf den Server hochgeladen. So sind Mitgliederliste & Fotos auf dem aktuellen Stand, auch wenn beim Meeting kein Internet verfĂŒgbar ist. +
    + +
    + Neue Mitglieder oder Änderungen? Wenn du keine Berechtigung zum Hochladen hast, gib die Änderungen an dein Support-Team weiter — dort werden die Mitgliederdaten angepasst und auf dem Server gespeichert. Beim nĂ€chsten „Vom Server laden" hast du die neue Version.
    @@ -525,7 +552,7 @@
    - 🆕 Update 1.2.0 verfĂŒgbar – Timer-Schalter im Admin-MenĂŒ + 🆕 Update 1.5 verfĂŒgbar – Farbwarnung, Glockenton & 8-Augen-Timer
    @@ -572,14 +599,8 @@
    Steuerung – Admin-Tab -
    -
    -
    Einstellungen
    - -
    +
    + Admin-Sidebar mit Zeitanzeige-Checkbox
    @@ -588,6 +609,246 @@ + +
    +
    +
    12
    +

    8-Augen-GesprÀche Timer

    +
    + +
    + FĂŒr 8-Augen-GesprĂ€che gibt es einen eigenen Vollbild-Timer mit einer Uhr pro TischgrĂ¶ĂŸe (3er / 4er / 5er / 6er). Am 4er-Tisch wird die gewĂŒnschte Sprechzeit eingegeben — die anderen Tische werden automatisch so berechnet, dass alle Redner die passende Zeit haben. +
    + +
    +
    +

    Im Steuerungs-Fenster den Tab „Admin" (rechte Seitenleiste) öffnen.

    +

    Im Abschnitt „8-Augen-GesprĂ€che" auf â€žđŸ‘„ 8-Augen Timer starten" klicken — der Vollbild-Timer öffnet sich.

    +

    In der unteren Leiste unter „Wechsel 4er" die gewĂŒnschte Sprechzeit eingeben (z. B. 8:00 fĂŒr 8 Minuten) und mit ✓ ĂŒbernehmen.

    +

    Mit den TischgrĂ¶ĂŸen-Buttons (3er / 4er / 5er / 6er) auswĂ€hlen, welche Timer angezeigt werden sollen — nicht benötigte einfach ausblenden.

    +

    Auf „▶ Start" klicken — alle sichtbaren Timer laufen gleichzeitig los.

    +

    Am Ende jeder Sprechzeit erklingt der Glockenton, danach folgt eine 3 Sekunden Wechsel-Phase (blaue Karte, Countdown), bevor die nÀchste Sprechzeit startet.

    +

    Wenn alle Redner durch sind, stoppt der Timer automatisch — die Karten werden grĂŒn mit ✓ Alle N Redner durch.

    +

    Mit „✕ Beenden" schließt sich der Vollbild-Timer und die App springt zurĂŒck in die Vorstellungsrunde.

    +
    +
    + +
    +
    +
    +
    +
    + Anzeige – 8-Augen Timer (Vollbild) +
    +
    + 8-Augen-Timer Vollbild +
    +
    + +
    + Farbwarnung: Timer-Karte wird gelb ab 30 s Restzeit und rot ab 10 s. Bei jedem Rednerwechsel und Rundenende ertönt zusÀtzlich ein Glockenton. +
    + +
    + Rechenlogik: Die Rundendauer bleibt fĂŒr alle Tische gleich (z. B. 8 min am 4er = 4 × 8 min = 32 min Runde). Der 3er-Tisch bekommt entsprechend 10:40 pro Redner, der 5er 6:24, der 6er 5:20. Die 3 Sekunden Wechsel hĂ€ngen sich zusĂ€tzlich hinter jeden Redner (außer dem letzten). +
    +
    + + +
    +
    +
    13
    +

    Mitglieder mit BNI-Website synchronisieren

    +
    + +
    + Neue Mitglieder oder aktualisierte Fachgebiete/Fotos direkt aus der offiziellen BNI-Mitgliederliste in die App ĂŒbernehmen. LĂ€uft einmalig eingerichtet ĂŒber zwei Bookmarklets im Browser. +
    + +
    +

    Ablauf

    +
    +

    Im Steuerungs-Fenster oben in der Kopfzeile auf „🔄 Mitglieder aktualisieren" klicken.

    +

    Schritt 1 (einmalig): Beide Bookmarklet-Buttons in die Lesezeichenleiste des Browsers ziehen. Zuerst „Diagnose" auf der BNI-Mitgliederliste ausfĂŒhren und die heruntergeladene Diagnose-Datei hier hochladen — damit passt sich der Export-Button automatisch an die aktuelle Seitenstruktur an.

    +

    Schritt 2: BNI-Mitgliederliste öffnen (Link im Dialog), warten bis alle Mitglieder geladen sind, dann Bookmarklet „BNI Mitglieder exportieren" klicken — es lĂ€dt automatisch eine JSON-Datei herunter.

    +

    Schritt 3: Die JSON-Datei per „JSON-Datei laden & synchronisieren" in der App einlesen. Bestehende Mitglieder werden aktualisiert (Foto, Fachgebiet), neue kommen dazu. Es wird niemand gelöscht.

    +
    +
    + +
    +
    +
    +
    +
    + Dialog – Mitglieder synchronisieren +
    +
    + Modal: Mitglieder mit BNI-Website abgleichen +
    +
    + +
    + Hinweis: Nach dem Sync die neue Liste unbedingt ĂŒber das Admin-Panel „Auf Server hochladen" — sonst haben andere GerĂ€te die alten Daten. Ohne Upload-Berechtigung: Änderungen ans Support-Team melden. +
    +
    + + +
    +
    +
    14
    +

    Import / Export & Backup

    +
    + +
    + VollstĂ€ndiges Backup der Mitgliederdaten als JSON-Datei — nĂŒtzlich fĂŒr UmzĂŒge zwischen GerĂ€ten, fĂŒr einen sicheren Zwischenstand oder um lokale Kurzlinks & Offline-Fotos (Profildaten) zu sichern. +
    + +
    +

    Mitgliederdaten (JSON)

    +
    +

    Im Steuerungs-Fenster oben auf „⬇ Import / Export" klicken.

    +

    „Exportieren" — die kompletten Mitgliederdaten werden als Text im Feld angezeigt (per Copy&Paste sichern oder in eine Datei speichern).

    +

    „Importieren" — vorher exportierten Text wieder einfĂŒgen und einlesen. Ersetzt die aktuellen Daten im Browser.

    +
    + +

    Profildaten (Kurzlinks & Offline-Fotos)

    +
    +

    Kurzlinks (z. B. Website-Weiterleitungen) und lokal hochgeladene Fotos werden getrennt gespeichert und bleiben bei einem Mitglieder-Sync erhalten.

    +

    „⬇ profildaten.json speichern" — Datei sichern und im Projektordner ablegen.

    +

    Nach einem Browser-Reset oder auf einem neuen GerĂ€t: „⬆ PROFILDATEN.JSON LADEN" — Kurzlinks & Fotos sind wieder da.

    +
    +
    + +
    +
    +
    +
    +
    + Dialog – Import / Export +
    +
    + Modal: Import / Export +
    +
    +
    + + +
    +
    +
    15
    +

    Agenda-Folien fĂŒr Online-Meetings (Hybrid-Chapter)

    +
    + +
    + Hybrid-Chapter (z. B. Lumora) treffen sich einmal im Monat vor Ort, sonst online. FĂŒr Online-Meetings lĂ€uft ein deutlich + grĂ¶ĂŸerer Ablauf als nur die Vorstellungsrunde — BegrĂŒĂŸung, Meeting-Regeln, DSGVO, BNI-Zahlen, Auszeichnungen, Events + usw. Diese Rahmen-Folien lassen sich direkt im Tool anlegen und an der passenden Stelle im Ablauf anzeigen — als + hochgeladenes Bild oder als Live-Einbettung aus Zoho Show. +
    + +
    +

    Punkte im Admin-Panel anlegen

    +
    +

    Im Admin-Panel den Tab „Agenda" öffnen.

    +

    Auf „+ Agenda-Punkt hinzufĂŒgen" klicken (oder einen bestehenden Punkt anklicken, um ihn zu bearbeiten).

    +

    Eine Bezeichnung eingeben und die Anzeigeart wĂ€hlen: „Bild" oder „Live-Einbettung".

    +

    Auf „Speichern" klicken — der Punkt erscheint in der Liste.

    +
    +
    + +
    + Admin-Panel Agenda-Tab mit Liste der Punkte +
    + +

    A) Anzeigeart „Bild"

    +
    +

    FĂŒr einen Folien-Export (z. B. aus PowerPoint oder Zoho Show als Bild exportiert). Über „Bild wĂ€hlen" die Datei aus dem Finder wĂ€hlen — sie wird automatisch auf eine fĂŒr Vollbild-Anzeige passende GrĂ¶ĂŸe verkleinert, bevor sie hochgeladen wird.

    +
    +
    +
    +
    +
    +
    + Admin-Panel – Agenda-Punkt bearbeiten (Bild) +
    +
    + Modal: Agenda-Punkt bearbeiten, Anzeigeart Bild +
    +
    + +

    B) Anzeigeart „Live-Einbettung"

    +
    +

    FĂŒr eine Folie, die das Team gemeinsam in Zoho Show pflegt — Änderungen sind sofort live, ohne erneuten Upload. In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, den dortigen Link in das Feld „Einbettungs-URL" einfĂŒgen. Am zuverlĂ€ssigsten funktioniert das, wenn jeder Agenda-Punkt eine eigene, kleine Zoho-Show-Datei ist (statt einer großen Gesamt-PrĂ€sentation) — dann startet die Einbettung immer bei der ersten Folie.

    +
    +
    +
    +
    +
    +
    + Admin-Panel – Agenda-Punkt bearbeiten (Live-Einbettung) +
    +
    + Modal: Agenda-Punkt bearbeiten, Anzeigeart Live-Einbettung +
    +
    + +
    + Nicht vergessen: Nach dem Anlegen/Ändern von Punkten im Admin-Panel oben rechts auf â€žđŸ“€ Auf Server hochladen" klicken — sonst kommen die Änderungen nicht im Steuerpult (bzw. auf anderen GerĂ€ten) an. +
    + +
    +

    Reihenfolge festlegen und live anzeigen

    +
    +

    Im Steuerpult den neuen Tab „Agenda" öffnen (ganz links, neben „Mitglieder" und „Besucher").

    +

    Punkte per Drag & Drop in die gewĂŒnschte Reihenfolge bringen — genau wie bei der Mitgliederliste.

    +

    Auf „▶ Anzeigen" klicken, um einen Punkt sofort auf dem Display zu zeigen.

    +

    Mit dem đŸš«-Button lĂ€sst sich ein Punkt aus der aktiven Reihenfolge ausblenden (fĂŒr dieses Meeting nicht gebraucht) — er taucht dann unten unter „Ausgeblendet" auf und kann dort per „+ Einblenden" jederzeit wieder aktiviert werden.

    +
    +
    + +
    +
    +
    +
    +
    + Steuerpult – Agenda-Tab +
    +
    + Steuerpult mit Agenda-Tab und sortierbarer Reihenfolge +
    +
    + +
    + Mitglieder-, Besucher- und Marketing-/Ergebnisrunde sind eingebaute Agenda-Punkte: Sie lassen sich genau wie die Bild-/Einbettungs-Folien frei in die Reihenfolge einsortieren — z. B. BegrĂŒĂŸung → Regeln → Mitglieder-Vorstellungsrunde → Auszeichnung → Besucher-Vorstellungsrunde → Events. +
    + +
    +

    Marketing-/Ergebnisrunde

    +

    LĂ€uft dieselbe Mitgliederliste noch einmal in derselben Reihenfolge durch wie die Vorstellungsrunde — aber ohne Redezeit-Timer. Gedacht fĂŒr die Runde, in der jedes Mitglied kurz berichtet, was es in der vergangenen Woche fĂŒr andere Mitglieder getan hat (Empfehlungen, Besucher, 4-Augen-GesprĂ€che, Trainings, Umsatzdank).

    +
    + +
    +

    „Weiter"-Button folgt der Agenda

    +

    Der rote „Weiter"-Button oben fĂŒhrt automatisch zum jeweils nĂ€chsten Punkt in der festgelegten Reihenfolge — egal ob als NĂ€chstes eine Folie, die Mitglieder-Vorstellungsrunde oder die Besucherrunde kommt. Bei einer Live-Einbettung kann „Weiter" nicht innerhalb der eingebetteten Folie selbst blĂ€ttern (das bleibt Sache von Zoho Show), sondern springt direkt zum nĂ€chsten Agenda-Punkt.

    +
    + +
    +
    +
    +
    +
    + Display-Ansicht – laufende Agenda-Folie +
    +
    + Display-Ansicht mit einer live eingebetteten Agenda-Folie +
    +
    + +
    + Bild wird nicht angezeigt? Meist liegt es daran, dass der Bild-Upload noch nicht abgeschlossen war, als „Speichern" geklickt wurde — einfach das Bild im Admin-Panel erneut auswĂ€hlen und speichern. Erscheint stattdessen eine eingebettete Live-Folie nicht: in Zoho Show prĂŒfen, ob die Datei ĂŒber „Veröffentlichen" freigegeben ist, und im Browser testweise Werbeblocker/Privacy-Erweiterungen deaktivieren. +
    +
    +
    BNI Vorstellungsrunde · Interne Anleitung · Bei Fragen an den Administrator wenden diff --git a/index.html b/index.html index 03d327a..5744c4b 100644 --- a/index.html +++ b/index.html @@ -800,6 +800,89 @@ .timer-ring-progress.expired { stroke: #ef4444; animation: timerBlink 0.5s alternate infinite; } @keyframes timerBlink { to { opacity: 0.15; } } + /* 8-Augen Timer */ + .augen-card { + background: rgba(255,255,255,0.05); + border: 2px solid rgba(255,255,255,0.10); + border-radius: 20px; + padding: clamp(18px,2.5vh,32px) clamp(18px,2.5vw,32px); + display: flex; flex-direction: column; + align-items: center; justify-content: center; gap: clamp(8px,1.4vh,18px); + transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease; + min-height: 0; + color: #ffffff; + } + .augen-card .augen-card-time { color: #ffffff; text-shadow: 0 2px 12px rgba(0,0,0,0.5); } + .augen-card .augen-card-label { color: rgba(255,255,255,0.75); } + .augen-card .augen-card-sub { color: rgba(255,255,255,0.65); } + .augen-card.warn { color: #000; } + .augen-card.warn .augen-card-time { color: #000; text-shadow: none; } + .augen-card.warn .augen-card-label { color: rgba(0,0,0,0.75); } + .augen-card.warn .augen-card-sub { color: rgba(0,0,0,0.65); } + .augen-card.danger { color: #fff; } + .augen-card.danger .augen-card-time { color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,0.4); } + .augen-card.danger .augen-card-label { color: rgba(255,255,255,0.9); } + .augen-card.danger .augen-card-sub { color: rgba(255,255,255,0.85); } + .augen-card.wechsel { + background: #1e40af; + border-color: rgba(255,255,255,0.3); + color: #fff; + } + .augen-card.wechsel .augen-card-time { color: #fff; letter-spacing: 4px; font-size: clamp(2rem,7vw,5rem); } + .augen-card.wechsel .augen-card-label { color: rgba(255,255,255,0.85); } + .augen-card.wechsel .augen-card-sub { color: rgba(255,255,255,0.9); font-weight: 700; } + .augen-card.finished { + background: rgba(34,197,94,0.15); + border-color: rgba(34,197,94,0.5); + color: #86efac; + } + .augen-card.finished .augen-card-time { color: #86efac; } + .augen-card.finished .augen-card-label { color: rgba(134,239,172,0.85); } + .augen-card.finished .augen-card-sub { color: rgba(134,239,172,0.75); } + .augen-card.warn { background: #fbbf24; border-color: rgba(0,0,0,0.15); color: #000; } + .augen-card.danger { background: #cf2030; border-color: rgba(255,255,255,0.30); color: #fff; } + .augen-card.pulse { animation: augenPulse 0.9s ease-out; } + @keyframes augenPulse { + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(207,32,48,0.55); } + 40% { transform: scale(1.03); box-shadow: 0 0 0 40px rgba(207,32,48,0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(207,32,48,0); } + } + .augen-card-label { + font-size: clamp(0.85rem,1.8vw,1.35rem); + font-weight: 700; + letter-spacing: 3px; + text-transform: uppercase; + opacity: 0.65; + } + .augen-card.warn .augen-card-label, + .augen-card.danger .augen-card-label { opacity: 0.85; } + .augen-card-time { + font-size: clamp(3rem,10vw,8rem); + font-weight: 800; + font-variant-numeric: tabular-nums; + letter-spacing: 2px; + line-height: 1; + } + .augen-card-sub { + font-size: clamp(0.75rem,1.4vw,1.05rem); + font-weight: 600; + opacity: 0.55; + } + .augen-card.warn .augen-card-sub, + .augen-card.danger .augen-card-sub { opacity: 0.8; } + .augen-size-btn { + padding: 6px 14px; border-radius: 8px; + border: 1px solid rgba(255,255,255,0.15); + background: rgba(255,255,255,0.05); + color: rgba(255,255,255,0.55); + font-family: inherit; font-size: 0.9rem; font-weight: 600; + cursor: pointer; transition: all 0.15s ease; + } + .augen-size-btn.active { + background: #cf2030; border-color: #cf2030; color: #fff; + } + .augen-size-btn:hover { border-color: rgba(255,255,255,0.35); } + #display-timer-text { font-size: clamp(2rem, 4vh, 5rem); font-weight: 700; color: rgba(255,255,255,0.95); @@ -1221,7 +1304,12 @@
    - + +
    BNI @@ -1317,6 +1405,7 @@
    Reihenfolge (ziehen zum Sortieren)
    +
    Agenda
    Mitglieder
    Besucher
    @@ -1329,6 +1418,10 @@ + +
    @@ -1421,6 +1514,30 @@
    + + + @@ -1638,7 +1755,7 @@ - + @@ -1789,7 +1906,7 @@ // ============================================================ // CONSTANTS & CHAPTER CONFIG // ============================================================ -const APP_VERSION = '1.5'; +const APP_VERSION = '2.0'; const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw'; const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde'; function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; } @@ -1819,6 +1936,7 @@ let STORAGE_KEY = activeChapter ? `bni-chapter-${activeChapter.code}-v2` : 'bni- let state = loadState(); let sortableInstance = null; let sortableVisitorInstance = null; +let sortableAgendaInstance = null; let qrInstance = null; let activeTab = 'members'; // 'members' | 'visitors' @@ -1833,11 +1951,22 @@ function defaultState() { visitors: [], // { id, name, company, specialty, photoUrl } visitorsOrder: [], // visitor IDs in order visitorsCurrentIndex: -1, - mode: 'members', // 'members' | 'absent-summary' | 'visitors' | 'coordinator-report' + mode: 'members', // 'members' | 'absent-summary' | 'visitors' | 'coordinator-report' | 'augen-timer' | 'round-transition' | 'agenda-item' defaultTimer: 60, // seconds (standard speaking time) memberTimers: {}, // { memberId: customSeconds } showTimer: true, // false = timer completely hidden coordinatorReportData: null, + agendaCatalog: [], // { id, label, type: 'image'|'embed', imagePath?, embedUrl? } — geladen aus chapters//agenda.json + agendaOrder: [], // Reihenfolge von IDs (Katalog-Punkte + eingebaute Vorstellungsrunden), aktiv fĂŒr dieses Meeting (wie meetingOrder) + agendaKnownIds: [], // IDs, die schon einmal automatisch in agendaOrder eingefĂŒgt wurden (verhindert, dass bewusst ausgeblendete Punkte zurĂŒckkehren) + currentAgendaItemId: null, + marketingActive: false, // true = aktuelle 'members'-Runde ist die Marketing-/Ergebnisrunde (gleiche Reihenfolge, ohne Timer) + augen: { // 8-Augen-GesprĂ€che Multi-Timer + visibleSizes: [3, 4, 5, 6], + baseSeconds: 600, // Wechsel-Intervall am 4er-Tisch (Basis) + startedAt: null, + running: false, + }, }; } @@ -2222,6 +2351,18 @@ function renderCurrentView() { renderControl(); } _refreshSoundBtn(); + + // Augen-Timer-Overlay auch in Control-View sichtbar machen + const augenEl = document.getElementById('display-augen-timer'); + if (augenEl) { + if (state.mode === 'augen-timer') { + augenEl.style.display = 'flex'; + renderAugenTimer(); + _augenStartTick(); + } else if (view === 'control') { + augenEl.style.display = 'none'; + } + } } window.addEventListener('hashchange', renderCurrentView); @@ -2255,6 +2396,31 @@ function renderDisplay() { const displayHeader = document.querySelector('#view-display .display-header'); const absentSummaryEl = document.getElementById('display-absent-summary'); const coordReportEl = document.getElementById('display-coordinator-report'); + const augenEl = document.getElementById('display-augen-timer'); + const agendaItemEl = document.getElementById('display-agenda-item'); + + // 8-AUGEN TIMER MODE + if (state.mode === 'augen-timer') { + emptyEl.style.display = 'none'; + mainCard.style.display = 'none'; + contentEl.style.display = 'none'; + nextBar.style.display = 'none'; + nextBarEnd.style.display = 'none'; + roundBadge.classList.remove('visible'); + progressText.textContent = ''; + if (displayHeader) displayHeader.style.display = 'none'; + if (absentSummaryEl) absentSummaryEl.style.display = 'none'; + if (coordReportEl) coordReportEl.style.display = 'none'; + if (agendaItemEl) agendaItemEl.style.display = 'none'; + const qrWrap = document.getElementById('qr-wrap'); + if (qrWrap) qrWrap.style.display = 'none'; + if (augenEl) { + augenEl.style.display = 'flex'; + renderAugenTimer(); + } + return; + } + if (augenEl) augenEl.style.display = 'none'; // COORDINATOR REPORT MODE if (state.mode === 'coordinator-report') { @@ -2267,6 +2433,7 @@ function renderDisplay() { progressText.textContent = ''; if (displayHeader) displayHeader.style.display = ''; if (absentSummaryEl) absentSummaryEl.style.display = 'none'; + if (agendaItemEl) agendaItemEl.style.display = 'none'; const qrWrap = document.getElementById('qr-wrap'); if (qrWrap) qrWrap.style.display = 'none'; if (coordReportEl) { @@ -2277,6 +2444,51 @@ function renderDisplay() { } if (coordReportEl) coordReportEl.style.display = 'none'; + // AGENDA-FOLIE MODE (Bild oder Live-Einbettung) + if (state.mode === 'agenda-item') { + emptyEl.style.display = 'none'; + mainCard.style.display = 'none'; + contentEl.style.display = 'none'; + nextBar.style.display = 'none'; + nextBarEnd.style.display = 'none'; + roundBadge.classList.remove('visible'); + progressText.textContent = ''; + if (displayHeader) displayHeader.style.display = 'none'; + if (absentSummaryEl) absentSummaryEl.style.display = 'none'; + const qrWrap = document.getElementById('qr-wrap'); + if (qrWrap) qrWrap.style.display = 'none'; + const it = (state.agendaCatalog || []).find(a => a.id === state.currentAgendaItemId); + const imgEl = document.getElementById('agenda-item-image'); + const embedEl = document.getElementById('agenda-item-embed'); + const errEl = document.getElementById('agenda-item-error'); + if (agendaItemEl && it) { + agendaItemEl.style.display = 'flex'; + if (errEl) errEl.style.display = 'none'; + if (it.type === 'embed' && it.embedUrl) { + embedEl.src = it.embedUrl; + embedEl.style.display = ''; + imgEl.style.display = 'none'; + imgEl.src = ''; + } else if (it.imagePath) { + const src = it.imagePath.startsWith('http') ? it.imagePath + : giteaUrl(`chapters/${activeChapter.code}/${it.imagePath}`); + imgEl.onerror = () => { + imgEl.style.display = 'none'; + if (errEl) errEl.style.display = 'block'; + }; + imgEl.onload = () => { if (errEl) errEl.style.display = 'none'; }; + imgEl.src = src; + imgEl.style.display = ''; + embedEl.style.display = 'none'; + embedEl.src = ''; + } + } else if (agendaItemEl) { + agendaItemEl.style.display = 'none'; + } + return; + } + if (agendaItemEl) agendaItemEl.style.display = 'none'; + // ABSENT SUMMARY MODE if (state.mode === 'absent-summary') { emptyEl.style.display = 'none'; @@ -2378,6 +2590,10 @@ function renderDisplay() { // Round badge if (state.mode === 'visitors') { + roundBadge.textContent = '2. Runde – Besucher'; + roundBadge.classList.add('visible'); + } else if (state.mode === 'members' && state.marketingActive) { + roundBadge.textContent = 'Marketing-/Ergebnisrunde'; roundBadge.classList.add('visible'); } else { roundBadge.classList.remove('visible'); @@ -2721,6 +2937,7 @@ function getDisplayData(presenter) { function renderControl() { renderMemberQueue(); renderVisitorQueue(); + renderAgendaQueue(); renderVisitorsList(); renderMembersManage(); renderSubstitutesList(); @@ -2907,6 +3124,287 @@ function renderVisitorQueue() { }); } +// ============================================================ +// AGENDA-FOLIEN (Online-Meeting-Ablauf: Bild, Live-Einbettung, oder eingebaute Vorstellungsrunden) +// ============================================================ +// Die Vorstellungsrunden sind fest eingebaute Agenda-Punkte, damit sie in derselben +// frei sortierbaren Reihenfolge stehen können wie die Bild-/Einbettungs-Folien. +const BUILTIN_AGENDA_ITEMS = [ + { id: '__members_round__', label: 'Mitglieder-Vorstellungsrunde', builtin: 'members' }, + { id: '__visitors_round__', label: 'Besucher-Vorstellungsrunde', builtin: 'visitors' }, + { id: '__marketing_round__', label: 'Marketing-/Ergebnisrunde', builtin: 'marketing' }, +]; + +function getAgendaItem(id) { + return BUILTIN_AGENDA_ITEMS.find(b => b.id === id) || (state.agendaCatalog || []).find(a => a.id === id); +} + +// FĂŒgt neu bekannt gewordene Punkte (Katalog + eingebaute Runden) genau einmal automatisch +// in die aktive Reihenfolge ein. Punkte, die die Moderatorin bewusst ausgeblendet hat, +// bleiben dauerhaft ausgeblendet (agendaKnownIds merkt sich, was schon einmal eingefĂŒgt wurde). +function ensureAgendaOrderDefaults() { + if (!state.agendaOrder) state.agendaOrder = []; + if (!state.agendaKnownIds) state.agendaKnownIds = []; + const known = new Set(state.agendaKnownIds); + const inOrder = new Set(state.agendaOrder); + const allIds = [...BUILTIN_AGENDA_ITEMS.map(b => b.id), ...(state.agendaCatalog || []).map(a => a.id)]; + allIds.forEach(id => { + if (!known.has(id)) { + // FĂŒr bereits vorhandene agendaOrder-EintrĂ€ge (Migration von vor agendaKnownIds) + // nur als "bekannt" nachtragen, nicht erneut einfĂŒgen. + if (!inOrder.has(id)) { state.agendaOrder.push(id); inOrder.add(id); } + state.agendaKnownIds.push(id); + known.add(id); + } + }); + // Vom Server gelöschte Katalog-Punkte aus der aktiven Reihenfolge entfernen; eingebaute Runden bleiben immer gĂŒltig. + // Set-Dedupe als Sicherheitsnetz gegen doppelte EintrĂ€ge aus frĂŒheren LĂ€ufen. + const validIds = new Set(allIds); + state.agendaOrder = Array.from(new Set(state.agendaOrder.filter(id => validIds.has(id)))); +} + +function updateAgendaTabVisibility() { + const tab = document.getElementById('tab-agenda'); + if (!tab) return; + tab.style.display = ''; // immer sichtbar – enthĂ€lt immer mind. die eingebauten Vorstellungsrunden +} + +async function fetchChapterAgendaFromGitea() { + if (!activeChapter) return; + if (!state.agendaCatalog) state.agendaCatalog = []; + ensureAgendaOrderDefaults(); + try { + const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/agenda.json`), { cache: 'no-store' }); + const items = res.ok ? (await res.json()).items : null; + if (Array.isArray(items)) { + // Mergen statt Überschreiben: lokale, noch nicht hochgeladene Punkte + // (z.B. gerade erst im Admin-Panel angelegt) dĂŒrfen nicht verloren gehen. + const map = new Map(state.agendaCatalog.map(i => [i.id, i])); + items.forEach(it => map.set(it.id, Object.assign({}, map.get(it.id) || {}, it))); + state.agendaCatalog = Array.from(map.values()); + } + ensureAgendaOrderDefaults(); + saveState(false); + updateAgendaTabVisibility(); + renderAgendaQueue(); + } catch(e) { /* offline → zuletzt bekannten Katalog behalten */ } +} + +function renderAgendaQueue() { + const container = document.getElementById('agenda-queue'); + if (!container) return; + updateAgendaTabVisibility(); + ensureAgendaOrderDefaults(); + container.innerHTML = ''; + + const order = state.agendaOrder || []; + if (order.length === 0) { + container.innerHTML = `
    +

    Keine Agenda-Punkte aktiv

    +

    Punkte unten unter "Ausgeblendet" wieder einblenden, oder im Admin-Panel neue Punkte anlegen.

    +
    `; + renderAgendaHiddenSection(); + return; + } + + order.forEach(id => { + const it = getAgendaItem(id); + if (!it) return; + const isCurrent = it.builtin + ? state.mode === it.builtin + : (state.mode === 'agenda-item' && state.currentAgendaItemId === id); + + const item = document.createElement('div'); + item.className = 'queue-item' + (isCurrent ? ' current' : ''); + item.dataset.id = id; + + const badge = isCurrent ? `Live` : ''; + const typeTag = it.builtin + ? `
    Vorstellungsrunde
    ` + : it.type === 'embed' + ? `
    Live-Einbettung
    ` + : `
    Bild
    `; + + const showCall = it.builtin ? `showAgendaRound('${it.builtin}')` : `showAgendaItem('${it.id}')`; + + item.innerHTML = ` + ${badge} +
    â ż
    +
    +
    ${escHtml(it.label || it.id)}
    + ${typeTag} +
    +
    + + +
    + `; + container.appendChild(item); + }); + + renderAgendaHiddenSection(); + + if (sortableAgendaInstance) sortableAgendaInstance.destroy(); + sortableAgendaInstance = Sortable.create(container, { + animation: 150, + handle: '.drag-handle', + ghostClass: 'sortable-ghost', + filter: '.empty-hint', + onEnd: (evt) => { + const moved = state.agendaOrder.splice(evt.oldIndex, 1)[0]; + state.agendaOrder.splice(evt.newIndex, 0, moved); + saveState(); + renderControl(); + }, + }); +} + +function renderAgendaHiddenSection() { + const section = document.getElementById('agenda-hidden-section'); + if (!section) return; + if (activeTab !== 'agenda') { section.style.display = 'none'; return; } + + const order = new Set(state.agendaOrder || []); + const allItems = [...BUILTIN_AGENDA_ITEMS, ...(state.agendaCatalog || [])]; + const hidden = allItems.filter(it => !order.has(it.id)); + if (hidden.length === 0) { section.style.display = 'none'; return; } + + section.style.display = 'block'; + section.className = 'absent-section'; + let html = `
    Ausgeblendet
    `; + hidden.forEach(it => { + html += `
    +
    ${escHtml(it.label || it.id)}
    +
    + +
    +
    `; + }); + section.innerHTML = html; +} + +function hideAgendaItem(id) { + state.agendaOrder = (state.agendaOrder || []).filter(aid => aid !== id); + if (state.mode === 'agenda-item' && state.currentAgendaItemId === id) closeAgendaItem(); + saveState(); + renderControl(); +} + +function unhideAgendaItem(id) { + if (!state.agendaOrder) state.agendaOrder = []; + if (!state.agendaOrder.includes(id)) state.agendaOrder.push(id); + saveState(); + renderControl(); +} + +function showAgendaItem(id) { + const it = (state.agendaCatalog || []).find(a => a.id === id); + if (!it) return; + if (state.mode !== 'agenda-item') state._agendaPrevMode = state.mode; + state.mode = 'agenda-item'; + state.currentAgendaItemId = id; + saveState(); + renderControl(); + renderDisplay(); +} + +function closeAgendaItem() { + state.mode = state._agendaPrevMode || 'members'; + delete state._agendaPrevMode; + state.currentAgendaItemId = null; + saveState(); + renderControl(); + renderDisplay(); +} + +// Startet/holt eine der eingebauten Vorstellungsrunden auf das Display – wie ein Klick +// auf den jeweiligen Tab, nur direkt aus der Agenda-Reihenfolge heraus nutzbar. +function showAgendaRound(kind) { + if (kind === 'members') { + state.mode = 'members'; + state.marketingActive = false; + if (state.currentIndex < 0) state.currentIndex = 0; + switchTab('members'); + } else if (kind === 'marketing') { + // Zweiter Durchlauf durch dieselbe Mitgliederliste (Marketing-/Ergebnisrunde) – immer + // von vorne, ohne Redezeit-Timer. + state.mode = 'members'; + state.marketingActive = true; + state.currentIndex = 0; + switchTab('members'); + stopTimer(); + } else { + state.mode = 'visitors'; + if (state.visitorsCurrentIndex < 0) state.visitorsCurrentIndex = 0; + switchTab('visitors'); + } + saveState(); + renderControl(); + renderDisplay(); +} + +// Eine eingebaute Vorstellungsrunde ist beim automatischen Weiterspringen nur "nutzbar", wenn +// es dafĂŒr ĂŒberhaupt Personen gibt (z.B. keine Besucher angemeldet → Runde beim Weiter-Klick ĂŒberspringen). +// Manuelles Anklicken in der Agenda-Liste ist davon nicht betroffen. +function isAgendaStepUsable(id) { + const it = getAgendaItem(id); + if (!it) return false; + if (it.builtin === 'members' || it.builtin === 'marketing') return state.meetingOrder.length > 0; + if (it.builtin === 'visitors') return state.visitorsOrder.length > 0; + return true; +} + +// Findet die ID des nĂ€chsten gĂŒltigen Punkts nach `currentId` in der aktiven Agenda-Reihenfolge +// (Katalog-Folien + eingebaute Vorstellungsrunden). null = Ende der Agenda erreicht. +function nextAgendaSequenceId(currentId) { + const order = state.agendaOrder || []; + const idx = order.indexOf(currentId); + if (idx === -1) return null; + for (let i = idx + 1; i < order.length; i++) { + if (isAgendaStepUsable(order[i])) return order[i]; + } + return null; +} + +// Beschriftung fĂŒr den "Weiter"-Button: zeigt, wohin der nĂ€chste Klick tatsĂ€chlich fĂŒhrt. +function nextStepButtonLabel(afterId) { + const nextId = nextAgendaSequenceId(afterId); + if (!nextId) return '↩ Beenden'; + const it = getAgendaItem(nextId); + if (!it) return '↩ Beenden'; + if (it.builtin === 'visitors') return '2. Runde'; // vertrauter Text fĂŒr den Übergang mit Logo-Zwischenschritt + return it.label; +} + +// Springt zum ĂŒbergebenen Agenda-Schritt (Folie oder eingebaute Runde), oder beendet das +// Meeting (zurĂŒck zum Logo), wenn kein weiterer Schritt mehr folgt. +function goToAgendaSequenceStep(id) { + if (!id) { endMeetingFlow(); return; } + const it = getAgendaItem(id); + if (!it) { endMeetingFlow(); return; } + if (it.builtin === 'visitors') { + // FĂŒr den Übergang in die Besucherrunde den bestehenden Logo-Zwischenschritt beibehalten. + startRoundTransition(); + return; + } + if (it.builtin === 'members' || it.builtin === 'marketing') { + showAgendaRound(it.builtin); + return; + } + showAgendaItem(id); +} + +function endMeetingFlow() { + state.currentIndex = -1; + state.visitorsCurrentIndex = -1; + state.mode = 'members'; + state.marketingActive = false; + switchTab('members'); + saveState(); + renderControl(); + renderDisplay(); +} + function renderVisitorsList() { const container = document.getElementById('visitors-list'); container.innerHTML = ''; @@ -3020,6 +3518,12 @@ function updateNextButton() { return; } + if (state.mode === 'agenda-item') { + btn.disabled = false; + nameEl.textContent = nextStepButtonLabel(state.currentAgendaItemId); + return; + } + const next = getNextPresenter(); const hasStarted = state.currentIndex >= 0 || state.mode === 'visitors'; @@ -3041,7 +3545,7 @@ function updateNextButton() { // Absent-summary mode if (state.mode === 'absent-summary') { btn.disabled = false; - nameEl.textContent = state.visitorsOrder.length > 0 ? '2. Runde' : '↩ Beenden'; + nameEl.textContent = nextStepButtonLabel('__members_round__'); return; } @@ -3055,15 +3559,18 @@ function updateNextButton() { const visitorsDone = state.mode === 'visitors' && state.visitorsCurrentIndex >= state.visitorsOrder.length - 1; const hasAbsent = state.absentIds.length > 0 || (state.medicalIds || []).length > 0; - if (membersDone && hasAbsent) { + if (membersDone && hasAbsent && !state.marketingActive) { btn.disabled = false; nameEl.textContent = 'Abwesenheiten'; - } else if (membersDone && state.visitorsOrder.length === 0) { + } else if (membersDone && state.marketingActive) { btn.disabled = false; - nameEl.textContent = '↩ Beenden'; + nameEl.textContent = nextStepButtonLabel('__marketing_round__'); + } else if (membersDone) { + btn.disabled = false; + nameEl.textContent = nextStepButtonLabel('__members_round__'); } else if (visitorsDone) { btn.disabled = false; - nameEl.textContent = '↩ Beenden'; + nameEl.textContent = nextStepButtonLabel('__visitors_round__'); } else { btn.disabled = false; nameEl.textContent = ''; @@ -3095,6 +3602,13 @@ function goNext() { _coordReportClose(); return; } + if (state.mode === 'agenda-item') { + // Folgt der Agenda-Reihenfolge zum nĂ€chsten Punkt. Bei eingebetteten Live-Folien kann + // "Weiter" nicht innerhalb der Einbettung selbst blĂ€ttern, sondern nur zum nĂ€chsten + // Agenda-Punkt springen. + goToAgendaSequenceStep(nextAgendaSequenceId(state.currentAgendaItemId)); + return; + } if (state.mode === 'round-transition') { state.mode = 'visitors'; state.visitorsCurrentIndex = 0; @@ -3104,52 +3618,43 @@ function goNext() { renderDisplay(); return; } - if (state.mode === 'members') { + if (state.mode === 'members' && state.marketingActive) { + // Marketing-/Ergebnisrunde: dieselbe Reihenfolge wie die Vorstellungsrunde, aber ohne + // Timer und ohne Abwesenheiten-Zwischenschritt. if (state.currentIndex < state.meetingOrder.length - 1) { state.currentIndex++; } else { - // End of members: show absent summary if applicable, otherwise go to visitors + goToAgendaSequenceStep(nextAgendaSequenceId('__marketing_round__')); + return; + } + } else if (state.mode === 'members') { + if (state.currentIndex < state.meetingOrder.length - 1) { + state.currentIndex++; + } else { + // Ende der Mitgliederrunde: erst Abwesenheiten (falls vorhanden), danach dem + // nĂ€chsten Punkt in der Agenda-Reihenfolge nach "Mitglieder-Vorstellungsrunde" folgen. const hasAbsent = state.absentIds.length > 0 || (state.medicalIds || []).length > 0; if (hasAbsent) { state.mode = 'absent-summary'; - } else if (state.visitorsOrder.length > 0) { - startRoundTransition(); + } else { + goToAgendaSequenceStep(nextAgendaSequenceId('__members_round__')); return; } } } else if (state.mode === 'absent-summary') { - if (state.visitorsOrder.length > 0) { - startRoundTransition(); - return; - } else { - // End → back to logo - state.currentIndex = -1; - state.visitorsCurrentIndex = -1; - state.mode = 'members'; - switchTab('members'); - } + goToAgendaSequenceStep(nextAgendaSequenceId('__members_round__')); + return; } else if (state.mode === 'visitors') { if (state.visitorsCurrentIndex < state.visitorsOrder.length - 1) { state.visitorsCurrentIndex++; } else { - // End → back to logo - state.currentIndex = -1; - state.visitorsCurrentIndex = -1; - state.mode = 'members'; - switchTab('members'); - } - } else if (state.mode === 'members' && state.currentIndex >= state.meetingOrder.length - 1) { - // End of members, no absent, no visitors → back to logo - const hasAbsent = state.absentIds.length > 0 || (state.medicalIds || []).length > 0; - if (!hasAbsent && state.visitorsOrder.length === 0) { - state.currentIndex = -1; - state.visitorsCurrentIndex = -1; - state.mode = 'members'; - switchTab('members'); + // Ende der Besucherrunde: dem nĂ€chsten Punkt nach "Besucher-Vorstellungsrunde" folgen. + goToAgendaSequenceStep(nextAgendaSequenceId('__visitors_round__')); + return; } } - // Start timer for new presenter (2s delay) - if (state.mode === 'members' || state.mode === 'visitors') { + // Start timer for new presenter (2s delay) – in der Marketing-/Ergebnisrunde bewusst kein Timer. + if ((state.mode === 'members' && !state.marketingActive) || state.mode === 'visitors') { const nowPresenter = getCurrentPresenter(); if (nowPresenter) scheduleTimer(getPresenterSeconds(nowPresenter)); else stopTimer(); @@ -3167,6 +3672,7 @@ function resetMeeting() { state.currentIndex = -1; state.visitorsCurrentIndex = -1; state.mode = 'members'; + state.marketingActive = false; switchTab('members'); saveState(); renderControl(); @@ -3177,8 +3683,13 @@ function switchTab(tab) { activeTab = tab; document.getElementById('member-queue').style.display = tab === 'members' ? '' : 'none'; document.getElementById('visitor-queue').style.display = tab === 'visitors' ? '' : 'none'; + document.getElementById('agenda-queue').style.display = tab === 'agenda' ? '' : 'none'; + const agendaHidden = document.getElementById('agenda-hidden-section'); + if (agendaHidden && tab !== 'agenda') agendaHidden.style.display = 'none'; + else if (agendaHidden) renderAgendaHiddenSection(); document.getElementById('tab-members').classList.toggle('active', tab === 'members'); document.getElementById('tab-visitors').classList.toggle('active', tab === 'visitors'); + document.getElementById('tab-agenda').classList.toggle('active', tab === 'agenda'); } function switchRightPanel(panelId) { @@ -3352,7 +3863,17 @@ function openVisitorEdit(id) { document.getElementById('ve-name').value = v.name || ''; document.getElementById('ve-company').value = v.company || ''; document.getElementById('ve-specialty').value = v.specialty || ''; - document.getElementById('ve-invited-by').value = v.invitedBy || ''; + const sel = document.getElementById('ve-invited-by'); + if (sel) { + sel.innerHTML = ''; + state.members.slice().sort((a,b) => a.name.localeCompare(b.name)).forEach(m => { + const opt = document.createElement('option'); + opt.value = m.id; + opt.textContent = m.name; + if (m.id === v.invitedBy) opt.selected = true; + sel.appendChild(opt); + }); + } document.getElementById('ve-photo').value = v.photoUrl || ''; openModal('modal-visitor-edit'); } @@ -3366,7 +3887,7 @@ function saveVisitorEdit() { v.name = name; v.company = document.getElementById('ve-company').value.trim(); v.specialty = document.getElementById('ve-specialty').value.trim(); - v.invitedBy = document.getElementById('ve-invited-by').value.trim(); + v.invitedBy = document.getElementById('ve-invited-by').value || ''; v.photoUrl = document.getElementById('ve-photo').value.trim(); saveState(); renderControl(); @@ -4244,6 +4765,296 @@ function scheduleTimer(seconds) { }, 2000); } +// ============================================================ +// 8-AUGEN-GESPRÄCHE MULTI-TIMER +// ============================================================ +const _augen = { iv: null, lastSlot: {}, lastPhase: {}, prevRunning: false }; +const AUGEN_WECHSEL_SEC = 3; + +function _augenCfg() { + if (!state.augen) { + state.augen = { visibleSizes: [3,4,5,6], baseSeconds: 600, startedAt: null, running: false }; + } + return state.augen; +} + +function _augenSpeakSecondsFor(size) { + // Sprechzeit pro Redner: gleiche Gesamt-Sprechzeit pro Runde (4er = Basis) + const cfg = _augenCfg(); + return (4 * cfg.baseSeconds) / size; +} + +function _augenSecondsFor(size) { + // Slot = Sprechzeit + 3s Wechsel (außer letzter Slot — wird separat behandelt) + return _augenSpeakSecondsFor(size) + AUGEN_WECHSEL_SEC; +} + +function _augenTableFinishAt(size) { + // (size-1) volle Slots (Sprech + Wechsel) + 1 letzter Slot (nur Sprech) + const speak = _augenSpeakSecondsFor(size); + return (size - 1) * (speak + AUGEN_WECHSEL_SEC) + speak; +} + +function _augenParseTime(str) { + const s = String(str || '').trim(); + if (!s) return null; + if (s.includes(':')) { + const [m, sec] = s.split(':').map(x => parseInt(x, 10) || 0); + return m * 60 + sec; + } + const n = parseFloat(s.replace(',', '.')); + if (isNaN(n) || n <= 0) return null; + // Nur Ganzzahl → Minuten; sonst als Sekunden interpretieren wenn > 60 + return n < 60 ? Math.round(n * 60) : Math.round(n); +} + +function _augenFmt(sec) { + const s = Math.max(0, Math.round(sec)); + const m = Math.floor(s / 60); + const r = s % 60; + return `${m}:${String(r).padStart(2,'0')}`; +} + +function showAugenTimer() { + if (state.mode === 'augen-timer') { augenClose(); return; } + state._prevMode = state.mode; + state.mode = 'augen-timer'; + _augenCfg(); // ensure exists + saveState(); + renderCurrentView(); + _augenStartTick(); +} + +function augenClose() { + _augenStopTick(); + if (state.augen) { + state.augen.running = false; + state.augen.startedAt = null; + } + state.mode = state._prevMode || 'members'; + delete state._prevMode; + saveState(); + renderCurrentView(); +} + +function augenToggleRun() { + const cfg = _augenCfg(); + if (cfg.running) { + cfg.running = false; + cfg.startedAt = null; + } else { + cfg.running = true; + cfg.startedAt = Date.now(); + cfg._finished = false; + _augen.lastSlot = {}; + _augen.lastPhase = {}; + } + saveState(); + renderAugenTimer(); + _augenStartTick(); +} + +function augenReset() { + const cfg = _augenCfg(); + cfg.running = false; + cfg.startedAt = null; + cfg._finished = false; + _augen.lastSlot = {}; + _augen.lastPhase = {}; + saveState(); + renderAugenTimer(); +} + +function augenToggleSize(size) { + const cfg = _augenCfg(); + const list = Array.isArray(cfg.visibleSizes) ? cfg.visibleSizes.slice() : [3,4,5,6]; + const i = list.indexOf(size); + if (i >= 0) list.splice(i, 1); else list.push(size); + list.sort((a,b) => a - b); + if (list.length === 0) return; // mindestens einer sichtbar + cfg.visibleSizes = list; + saveState(); + renderAugenTimer(); +} + +function augenApplyBaseInput() { + const el = document.getElementById('augen-base-input'); + if (!el) return; + const sec = _augenParseTime(el.value); + if (!sec || sec < 10) { alert('Bitte gĂŒltige Zeit eingeben (z. B. 10:00 oder 10)'); return; } + const cfg = _augenCfg(); + cfg.baseSeconds = sec; + // Wenn lĂ€uft: neu starten damit Rechnung sofort greift + if (cfg.running) { + cfg.startedAt = Date.now(); + _augen.lastSlot = {}; + } + saveState(); + renderAugenTimer(); +} + +function _augenStartTick() { + if (_augen.iv) clearInterval(_augen.iv); + _augen.iv = setInterval(_augenTick, 250); +} +function _augenStopTick() { + if (_augen.iv) { clearInterval(_augen.iv); _augen.iv = null; } +} + +function _augenTick() { + if (state.mode !== 'augen-timer') { _augenStopTick(); return; } + const cfg = _augenCfg(); + if (!cfg.running || !cfg.startedAt) return; + + const elapsed = (Date.now() - cfg.startedAt) / 1000; + const sizes = cfg.visibleSizes || [3,4,5,6]; + + // Übergangs- und Ende-Erkennung pro Tisch. + // Sprechzeit pro Redner = 4*base/N. Slot = Sprech + 3s Wechsel (letzter Slot: nur Sprech). + let allDone = true; + sizes.forEach(size => { + const speak = _augenSpeakSecondsFor(size); + const T = speak + AUGEN_WECHSEL_SEC; + const finishAt = _augenTableFinishAt(size); + const done = elapsed >= finishAt; + if (!done) allDone = false; + + const speakerIdx = Math.min(size, Math.floor(elapsed / T) + 1); + const inSlot = elapsed - (speakerIdx - 1) * T; + const phase = done ? 'done' : (inSlot < speak ? 'speak' : 'wechsel'); + const prev = _augen.lastPhase[size]; + _augen.lastPhase[size] = phase; + + if (prev === 'speak' && phase === 'wechsel') { + playBellSound(); + _augenPulse(size); + } + if (prev && prev !== 'done' && phase === 'done') { + // Tisch fertig + playBellSound(); + _augenPulse(size); + } + }); + + if (allDone) { + cfg.running = false; + cfg.startedAt = null; + cfg._finished = true; + saveState(); + } + + _augenUpdateTimes(); +} + +function _augenPulse(size) { + const card = document.querySelector(`.augen-card[data-size="${size}"]`); + if (!card) return; + card.classList.remove('pulse'); + void card.offsetWidth; // reflow fĂŒr Neustart der Animation + card.classList.add('pulse'); +} + +function _augenUpdateTimes() { + const cfg = _augenCfg(); + const running = !!(cfg.running && cfg.startedAt); + const elapsed = running ? (Date.now() - cfg.startedAt) / 1000 : 0; + const sizes = cfg.visibleSizes || [3,4,5,6]; + const longestFinish = sizes.reduce((m, s) => Math.max(m, _augenTableFinishAt(s)), 0); + const finishedAll = !running && cfg._finished; + + sizes.forEach(size => { + const card = document.querySelector(`.augen-card[data-size="${size}"]`); + if (!card) return; + const speak = _augenSpeakSecondsFor(size); + const T = speak + AUGEN_WECHSEL_SEC; + const finishAt = _augenTableFinishAt(size); + const tableDone = running ? elapsed >= finishAt : finishedAll; + const speakerIdx = running ? Math.min(size, Math.floor(elapsed / T) + 1) : 1; + const isLast = speakerIdx >= size; + const inSlot = running ? (elapsed - (speakerIdx - 1) * T) : 0; + const isWechsel = running && !tableDone && !isLast && inSlot >= speak; + const remSpeak = running ? Math.max(0, speak - inSlot) : speak; + const remWechsel = running ? Math.max(0, T - inSlot) : 0; + const nextIdx = Math.min(size, speakerIdx + 1); + const timeEl = card.querySelector('.augen-card-time'); + const subEl = card.querySelector('.augen-card-sub'); + card.classList.remove('warn','danger','wechsel','finished'); + if (tableDone) { + if (timeEl) timeEl.textContent = '✓'; + if (subEl) subEl.textContent = `Alle ${size} Redner durch`; + card.classList.add('finished'); + } else if (isWechsel) { + if (timeEl) timeEl.textContent = 'Wechsel'; + if (subEl) subEl.textContent = `Redner ${nextIdx} / ${size} in ${Math.ceil(remWechsel)}
`; + card.classList.add('wechsel'); + } else { + if (timeEl) timeEl.textContent = _augenFmt(remSpeak); + if (subEl) subEl.textContent = `Redner ${speakerIdx} / ${size} · Sprechzeit ${_augenFmt(speak)}`; + if (running) { + if (remSpeak <= 10) card.classList.add('danger'); + else if (remSpeak <= 30) card.classList.add('warn'); + } + } + }); + + const info = document.getElementById('augen-round-info'); + if (info) { + const totalRound = 4 * cfg.baseSeconds; + if (running) { + const remRound = Math.max(0, longestFinish - elapsed); + info.textContent = `Rundendauer ${_augenFmt(totalRound)} · Ende in ${_augenFmt(remRound)}`; + } else if (finishedAll) { + info.textContent = `Runde beendet · Rundendauer ${_augenFmt(totalRound)}`; + } else { + info.textContent = `Rundendauer ${_augenFmt(totalRound)} · bereit`; + } + } + const label = document.getElementById('augen-run-btn-label'); + if (label) label.textContent = running ? '⏞ Stop' : '▶ Start'; +} + +function renderAugenTimer() { + const cfg = _augenCfg(); + const grid = document.getElementById('augen-timer-grid'); + if (!grid) return; + const sizes = (cfg.visibleSizes && cfg.visibleSizes.length ? cfg.visibleSizes : [4]).slice().sort((a,b) => a-b); + const n = sizes.length; + // Grid-Layout: 1 → 1 col, 2 → 2 cols, 3 → 3 cols, 4 → 2x2 + if (n === 1) grid.style.gridTemplateColumns = '1fr'; + else if (n === 2) grid.style.gridTemplateColumns = 'repeat(2, 1fr)'; + else if (n === 3) grid.style.gridTemplateColumns = 'repeat(3, 1fr)'; + else grid.style.gridTemplateColumns = 'repeat(2, 1fr)'; + grid.style.gridAutoRows = '1fr'; + + grid.innerHTML = sizes.map(size => ` +
    +
    ${size}er Tisch
    +
    --:--
    +
    +
    + `).join(''); + + // Controls-Bar nur in Control-View + const bar = document.getElementById('augen-controls-bar'); + if (bar) { + const inControl = getView() !== 'display'; + bar.style.display = inControl ? 'flex' : 'none'; + if (inControl) { + const inp = document.getElementById('augen-base-input'); + if (inp && document.activeElement !== inp) inp.value = _augenFmt(cfg.baseSeconds); + const tog = document.getElementById('augen-size-toggles'); + if (tog) { + tog.innerHTML = [3,4,5,6].map(s => { + const active = (cfg.visibleSizes || []).includes(s); + return ``; + }).join(''); + } + } + } + + _augenUpdateTimes(); +} + function getPresenterSeconds(presenter) { const def = state.defaultTimer > 0 ? state.defaultTimer : 60; if (!presenter) return def; @@ -4463,6 +5274,7 @@ async function activateChapter() { setTimeout(() => { renderCurrentView(); fetchChapterMembersFromGitea(); + fetchChapterAgendaFromGitea(); }, 800); } catch(e) { status.textContent = 'Verbindungsfehler. Bitte Internetverbindung prĂŒfen.'; @@ -4723,6 +5535,13 @@ function renderAdminPanel() { Zeitanzeige aktiv +
    +
    8-Augen-GesprÀche
    + +
    Multi-Tisch-Timer fĂŒr 3er/4er/5er/6er-Tische. Zeit und sichtbare Tische im Vollbild einstellbar.
    +
    @@ -4955,6 +5774,7 @@ if (activeChapter) applyChapterUI(activeChapter.config); renderCurrentView(); updateTimerDisplays(); checkLicenseAndUpdates(); +if (activeChapter) fetchChapterAgendaFromGitea(); // Keyboard shortcut hint if (getView() === 'control') { diff --git a/screenshots/admin-panel-mitglieder.png b/screenshots/admin-panel-mitglieder.png new file mode 100644 index 0000000..298305e Binary files /dev/null and b/screenshots/admin-panel-mitglieder.png differ diff --git a/screenshots/agenda-admin-liste.png b/screenshots/agenda-admin-liste.png new file mode 100644 index 0000000..c203852 Binary files /dev/null and b/screenshots/agenda-admin-liste.png differ diff --git a/screenshots/agenda-display.png b/screenshots/agenda-display.png new file mode 100644 index 0000000..7c041d4 Binary files /dev/null and b/screenshots/agenda-display.png differ diff --git a/screenshots/agenda-punkt-bild.png b/screenshots/agenda-punkt-bild.png new file mode 100644 index 0000000..7dce0d6 Binary files /dev/null and b/screenshots/agenda-punkt-bild.png differ diff --git a/screenshots/agenda-punkt-embed.png b/screenshots/agenda-punkt-embed.png new file mode 100644 index 0000000..278615e Binary files /dev/null and b/screenshots/agenda-punkt-embed.png differ diff --git a/screenshots/agenda-steuerung.png b/screenshots/agenda-steuerung.png new file mode 100644 index 0000000..cc83eab Binary files /dev/null and b/screenshots/agenda-steuerung.png differ diff --git a/screenshots/augen-timer.png b/screenshots/augen-timer.png new file mode 100644 index 0000000..55c1411 Binary files /dev/null and b/screenshots/augen-timer.png differ diff --git a/screenshots/besucher-eintragen.png b/screenshots/besucher-eintragen.png new file mode 100644 index 0000000..0ede905 Binary files /dev/null and b/screenshots/besucher-eintragen.png differ diff --git a/screenshots/besucher-vertreterliste.png b/screenshots/besucher-vertreterliste.png new file mode 100644 index 0000000..1309227 Binary files /dev/null and b/screenshots/besucher-vertreterliste.png differ diff --git a/screenshots/display-live.png b/screenshots/display-live.png new file mode 100644 index 0000000..c94d1b1 Binary files /dev/null and b/screenshots/display-live.png differ diff --git a/screenshots/display-ton-aktivieren.png b/screenshots/display-ton-aktivieren.png new file mode 100644 index 0000000..fca46c7 Binary files /dev/null and b/screenshots/display-ton-aktivieren.png differ diff --git a/screenshots/import-export.png b/screenshots/import-export.png new file mode 100644 index 0000000..5007ff4 Binary files /dev/null and b/screenshots/import-export.png differ diff --git a/screenshots/mitglieder-sync.png b/screenshots/mitglieder-sync.png new file mode 100644 index 0000000..9f81861 Binary files /dev/null and b/screenshots/mitglieder-sync.png differ diff --git a/screenshots/steuerung-uebersicht.png b/screenshots/steuerung-uebersicht.png new file mode 100644 index 0000000..c15d44d Binary files /dev/null and b/screenshots/steuerung-uebersicht.png differ diff --git a/screenshots/vertreter-erfassen.png b/screenshots/vertreter-erfassen.png new file mode 100644 index 0000000..a8e1889 Binary files /dev/null and b/screenshots/vertreter-erfassen.png differ diff --git a/screenshots/zeit-pro-mitglied.png b/screenshots/zeit-pro-mitglied.png new file mode 100644 index 0000000..84dd769 Binary files /dev/null and b/screenshots/zeit-pro-mitglied.png differ diff --git a/screenshots/zeitanzeige-aktiv.png b/screenshots/zeitanzeige-aktiv.png new file mode 100644 index 0000000..44b20ae Binary files /dev/null and b/screenshots/zeitanzeige-aktiv.png differ diff --git a/screenshots/zeiteinstellung-global.png b/screenshots/zeiteinstellung-global.png new file mode 100644 index 0000000..a549be8 Binary files /dev/null and b/screenshots/zeiteinstellung-global.png differ diff --git a/version.json b/version.json index 0b44931..bf71032 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "1.5", - "notes": "Zeitanzeige oben ergĂ€nzt. letzte 15 Sekunden gelber Hintergrund, letzte 5 Sekunden roter Hintergrund" -} \ No newline at end of file + "version": "2.0", + "notes": "Agenda-Folien fĂŒr Online-Meetings (Hybrid-Chapter): Bild- oder Live-Einbettungs-Folien frei in die Reihenfolge einsortieren, inkl. Mitglieder-/Besucher-Vorstellungsrunde und neuer Marketing-/Ergebnisrunde (ohne Timer). \"Weiter\" folgt jetzt der Agenda." +}