Compare commits

...

125 Commits

Author SHA1 Message Date
baf236ea40 release: Version 2.2 – Gitea über https://git.digiseed.cloud 2026-10-25 13:27:01 +01:00
e9a98e7423 fix: Gitea-API über https://git.digiseed.cloud statt IP-Adresse 2026-10-25 13:20:24 +01:00
b6fb0f519b Agenda aktualisiert 12.10.2026 2026-10-12 17:27:18 +00:00
7893110125 Agenda aktualisiert 12.10.2026 2026-10-12 16:58:37 +00:00
7b024ae919 Agenda aktualisiert 12.10.2026 2026-10-12 16:53:38 +00:00
d6e6809dd7 Agenda aktualisiert 12.10.2026 2026-10-12 16:45:56 +00:00
b0c83b7b0c Agenda aktualisiert 11.10.2026 2026-10-11 16:58:53 +00:00
Christian Heubaum
7c3523959e Favicon (Sonne) auf allen Seiten
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:06:54 +02:00
Christian Heubaum
2bcf6095ab Stammvertreter-Auswahl als A–Z sortiertes Dropdown
Im Vertretungs-Dialog ersetzt ein Dropdown die Kachel-Auswahl; Sidebar- und Admin-Liste ebenfalls nach Name sortiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 00:01:27 +02:00
bb9186d759 Agenda aktualisiert 6.10.2026 2026-10-06 04:48:24 +00:00
29f4e91e38 Agenda aktualisiert 5.10.2026 2026-10-05 16:30:55 +00:00
ae68d34829 Agenda aktualisiert 29.9.2026 2026-09-29 04:32:53 +00:00
22561af0a4 Agenda aktualisiert 28.9.2026 2026-09-28 18:49:48 +00:00
719bb05610 Agenda aktualisiert 28.9.2026 2026-09-28 18:44:42 +00:00
3ef838a58b Agenda aktualisiert 28.9.2026 2026-09-28 18:41:53 +00:00
475aa65bc0 Agenda aktualisiert 28.9.2026 2026-09-28 18:27:13 +00:00
24eee60cf1 Agenda aktualisiert 21.9.2026 2026-09-21 18:22:02 +00:00
bd1575500e Agenda aktualisiert 15.9.2026 2026-09-15 04:31:50 +00:00
4e2dacc50f Agenda aktualisiert 14.9.2026 2026-09-14 17:39:52 +00:00
9d48b427ba Agenda aktualisiert 14.9.2026 2026-09-14 17:16:37 +00:00
294a1be11c Agenda aktualisiert 7.9.2026 2026-09-07 18:08:06 +00:00
958f8e13c7 Agenda aktualisiert 7.9.2026 2026-09-07 17:40:06 +00:00
8aee368200 Stammvertreter aktualisiert 2026-09-07 17:34:12 +00:00
8c26f26e47 Agenda aktualisiert 7.9.2026 2026-09-07 17:31:18 +00:00
b1cd3b1c5f Agenda aktualisiert 7.9.2026 2026-09-07 17:01:14 +00:00
64fb49f4bc Agenda aktualisiert 7.9.2026 2026-09-07 17:00:17 +00:00
6fa3d62d36 Agenda aktualisiert 7.9.2026 2026-09-07 16:25:59 +00:00
f993ac91b2 Agenda aktualisiert 3.9.2026 2026-09-03 16:42:00 +00:00
be377caf51 Agenda aktualisiert 3.9.2026 2026-09-03 16:40:29 +00:00
9df667e7ee Agenda aktualisiert 3.9.2026 2026-09-03 16:40:17 +00:00
b3f4f41bfa Agenda aktualisiert 31.8.2026 2026-08-31 17:25:09 +00:00
Christian Heubaum
5d9427c7a6 chapters/ vorübergehend wieder auf main (Übergangsphase)
Noch nicht aktualisierte Installationen lesen Chapter-Daten über den
alten Pfad (ref=main) und interpretieren das Fehlen als widerrufene
Lizenz — das hätte laufende Chapter sofort ausgesperrt. chapters/
bleibt deshalb vorerst auf main UND data parallel vorhanden, bis alle
Installationen auf die neue, data-Branch-basierte Version aktualisiert
sind. main wird erst danach final bereinigt.
2026-08-31 17:17:35 +02:00
Christian Heubaum
08340b5aec Chapter-Daten auf separaten data-Branch ausgelagert
chapters/ lag bisher im main-Branch, aus dem sich jede lokale
Installation bedient (z.B. per ZIP-Download) — dadurch bekam jedes
Chapter beim Einrichten die Mitgliederdaten aller anderen Chapter mit,
obwohl die App diese Daten ohnehin nie lokal liest, sondern immer live
über die Gitea-API holt.

Alle Lese-/Schreibzugriffe auf chapters/* laufen jetzt gegen den neuen
data-Branch (giteaDataUrl / giteaAuthUrl(..., 'data') inkl. branch:
'data' in den Schreib-Bodies). App-Code (index.html, admin.html,
version.json) bleibt weiterhin auf main. chapters/ ist aus main
entfernt (lokal weiterhin vorhanden, per .gitignore ausgeschlossen)
und liegt vollständig auf dem neuen data-Branch.
2026-08-31 17:16:29 +02:00
Christian Heubaum
8127f87e03 Per-Chapter Start-Dateien aus dem Repo genommen
start-<chapter>.html enthält den Aktivierungscode des jeweiligen
Chapters im Klartext. Da jede Chapter-Installation aus demselben
Repo/Download besteht, würde sonst jedes Chapter die Codes aller
anderen Chapter mitbekommen. Solche Dateien bleiben ab jetzt lokal
(per .gitignore ausgeschlossen) und werden individuell verschickt.
2026-08-31 17:00:42 +02:00
Christian Heubaum
95df518900 Auto-Aktivierung per URL-Parameter + Start-Datei pro Chapter
index.html liest jetzt ?chapter=<code> aus der URL und aktiviert das
Chapter automatisch, ohne dass der Lizenzcode manuell eingegeben
werden muss. start-helios.html ist ein Beispiel: eine kleine
Redirect-Datei, die man statt index.html verschickt/öffnet und die
direkt zum aktivierten Steuerpult des jeweiligen Chapters führt.
2026-08-31 16:57:36 +02:00
Christian Heubaum
f031e94fd8 BNI-Vorstellungsrunde-Anleitung.pdf aus dem Haupt-Repo entfernt (bleibt lokal, Doku läuft über Zoho Desk) 2026-08-31 16:51:58 +02:00
Christian Heubaum
22997c1add Merge remote-tracking branch 'origin/main' 2026-08-31 16:50:28 +02:00
Christian Heubaum
f15dba0786 chapters/.DS_Store aus dem Repo entfernt (Finder-Metadatei, bereits per .gitignore ausgeschlossen) 2026-08-31 16:50:23 +02:00
Christian Heubaum
7841c4854b Screenshots und anleitung.html aus dem Haupt-Repo entfernt
Beide werden von Chapter-Installationen nicht benötigt und bleiben
lokal erhalten. Die Anleitung wird stattdessen in Zoho Desk gepflegt.
2026-08-31 16:49:58 +02:00
3b4c16e85d chapters/.DS_Store gelöscht 2026-08-31 14:46:31 +00:00
9906eb03fe Mitglieder aktualisiert 31.8.2026 2026-08-31 14:40:42 +00:00
Christian Heubaum
760a6aadfa Neues Chapter Helios (Bad Waldsee) angelegt
Basis-Config mit Sync-URL zur BNI-Konstanz-Chapterseite; Mitglieder
werden separat ergänzt.
2026-08-31 16:02:44 +02:00
Christian Heubaum
66134424e2 Merge remote-tracking branch 'origin/main' 2026-08-29 20:24:36 +02:00
Christian Heubaum
706a91b193 Version 2.1: Klicker-Fix für Agenda + Sprechernotizen im Steuerpult
Präsentations-Klicker funktioniert jetzt durchgängig über die gesamte
Agenda-Sequenz, auch wenn der Tastatur-Fokus auf dem Display-Fenster
statt dem Steuerpult liegt (ArrowRight/Leertaste), plus ein dezenter
Klick-Button unten rechts im Display für Klicker mit Mausklick-Signal.

Zusätzlich: manuell gepflegte Sprechernotizen pro Agenda-Punkt, die
ausschließlich im Steuerpult angezeigt werden, nie im Display/Beamer.
2026-08-29 20:20:08 +02:00
ea876fb29d Agenda aktualisiert 29.8.2026 2026-08-29 18:05:43 +00:00
370a3f6cd8 Agenda aktualisiert 29.8.2026 2026-08-29 18:04:50 +00:00
7d96a3b5ad Agenda aktualisiert 29.8.2026 2026-08-29 18:02:07 +00:00
ca1902c7bb Stammvertreter aktualisiert 2026-08-28 03:53:38 +00:00
64a2c5b279 Stammvertreter aktualisiert 2026-08-28 03:52:22 +00:00
de5f560ee7 Agenda aktualisiert 25.8.2026 2026-08-25 06:50:50 +00:00
d526b97873 Agenda-Bild aktualisiert 25.8.2026 2026-08-25 06:50:47 +00:00
9e00549391 Agenda aktualisiert 25.8.2026 2026-08-25 06:43:32 +00:00
9366fa71af Agenda aktualisiert 21.8.2026 2026-08-21 13:01:32 +00:00
45400cd71e Agenda aktualisiert 21.8.2026 2026-08-21 12:58:15 +00:00
c5e5895303 Agenda aktualisiert 21.8.2026 2026-08-21 04:12:11 +00:00
36e001ae97 Agenda-Bild aktualisiert 21.8.2026 2026-08-21 04:12:08 +00:00
c1a5eaf653 Agenda aktualisiert 21.8.2026 2026-08-21 04:08:07 +00:00
11a97b62f0 Agenda aktualisiert 21.8.2026 2026-08-21 04:05:09 +00:00
8a629399ce Agenda aktualisiert 21.8.2026 2026-08-21 04:04:18 +00:00
e2a9423a6a Agenda-Bild aktualisiert 21.8.2026 2026-08-21 04:04:16 +00:00
a309d32d52 Agenda aktualisiert 21.8.2026 2026-08-21 04:03:27 +00:00
6f1800d9a6 Agenda aktualisiert 21.8.2026 2026-08-21 03:50:06 +00:00
5229c614f4 Agenda-Bild aktualisiert 21.8.2026 2026-08-21 03:48:48 +00:00
Christian Heubaum
580725f2f7 Version 2.0: Agenda-Folien für Online-Meetings (Hybrid-Chapter)
Neue Agenda-Punkte im Steuerpult (frei per Drag & Drop sortierbar, wie
die Mitgliederliste), pro Punkt entweder als hochgeladenes Bild oder als
Live-Einbettung aus Zoho Show. Mitglieder-/Besucher-Vorstellungsrunde und
eine neue Marketing-/Ergebnisrunde (gleiche Reihenfolge, ohne Timer) sind
eingebaute Agenda-Punkte und lassen sich genauso einsortieren. Der
"Weiter"-Button folgt jetzt der kompletten Agenda-Reihenfolge statt nur
Mitglieder- und Besucherrunde.

Unterwegs behobene Bugs: unsichtbarer "Anzeigen"-Button (falsche
Button-Klasse), fehlendes chapters/<code>/-Präfix beim Bild-Upload,
Race-Condition beim Speichern eines gerade erst ausgewählten Bildes,
Agenda-Reihenfolge-Duplikate bei bestehenden Chaptern.

Anleitung um Agenda-Abschnitt ergänzt, plus Korrekturen zu Ordnername,
Aktivierungs-Kontakt, Admin-Panel-URL und Offline-Hinweis für
Live-Einbettungen. Screenshots-Ordner (inkl. bisher nie gepushter
Bestandsbilder) erstmals vollständig committet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 18:59:22 +02:00
b40f384123 Agenda aktualisiert 19.8.2026 2026-08-19 10:09:11 +00:00
2b23bf8abc Agenda aktualisiert 19.8.2026 2026-08-19 09:54:06 +00:00
8164e2c7bc Agenda aktualisiert 19.8.2026 2026-08-19 09:49:21 +00:00
443eb138ec Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:23:11 +00:00
7b4b6a3f92 chapters/lumora-567294212/stammvertreter.json aktualisiert 2026-08-19 08:22:48 +00:00
7f2ebd6bb4 Falsch abgelegtes Bild entfernen (Pfad-Bug beim Agenda-Upload) 2026-08-19 08:20:02 +00:00
e66612de93 Stammvertreter aktualisiert 2026-08-19 08:19:58 +00:00
6f70a6bf71 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:10:09 +00:00
1d53f7b41a Agenda aktualisiert 19.8.2026 2026-08-19 08:09:01 +00:00
2865895a67 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 08:08:56 +00:00
d05f1396a2 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:57:29 +00:00
30f0290363 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:56:02 +00:00
a03740c2b4 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:54:11 +00:00
0debe09f64 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:50:16 +00:00
1a89a62149 Agenda aktualisiert 19.8.2026 2026-08-19 07:50:08 +00:00
347b682576 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:50:05 +00:00
9a5a8d1f2b Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:48:47 +00:00
b5d62ce51e Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:48:30 +00:00
2cdb378883 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:46:26 +00:00
e719ccbee0 Agenda aktualisiert 19.8.2026 2026-08-19 07:43:25 +00:00
28b0338a15 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:43:23 +00:00
0651fb4752 Stammvertreter aktualisiert 2026-08-19 07:43:20 +00:00
b11e9afa6a Agenda aktualisiert 19.8.2026 2026-08-19 07:42:34 +00:00
7ad0ab8114 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:42:32 +00:00
c0b9a6cbf8 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:41:26 +00:00
cf94ffd320 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:38:56 +00:00
8df0d03f8f Agenda aktualisiert 19.8.2026 2026-08-19 07:34:35 +00:00
26882ae897 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:34:30 +00:00
1796145bb3 Agenda aktualisiert 19.8.2026 2026-08-19 07:34:18 +00:00
7e19ede38d Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:32:20 +00:00
f20f67ebc0 Agenda aktualisiert 19.8.2026 2026-08-19 07:29:22 +00:00
ad52836ca4 Agenda aktualisiert 19.8.2026 2026-08-19 07:20:30 +00:00
d810ca68a0 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:18:19 +00:00
d4aea00b4d Agenda aktualisiert 19.8.2026 2026-08-19 07:16:28 +00:00
677ba40104 Agenda-Bild aktualisiert 19.8.2026 2026-08-19 07:16:24 +00:00
f9048c5826 Agenda aktualisiert 19.8.2026 2026-08-19 07:06:54 +00:00
e208bd2cf1 Agenda aktualisiert 19.8.2026 2026-08-19 07:05:22 +00:00
3548dea926 Agenda aktualisiert 19.8.2026 2026-08-19 06:34:20 +00:00
f4dfa39b7d Agenda aktualisiert 19.8.2026 2026-08-19 06:29:47 +00:00
d52e28c3d5 Stammvertreter aktualisiert 2026-08-18 10:27:57 +00:00
c9991a4192 Stammvertreter aktualisiert 2026-08-18 10:24:28 +00:00
88d9fac5da Mitglieder aktualisiert 18.8.2026 2026-08-18 10:23:53 +00:00
66de72edc0 Mitglieder aktualisiert 18.8.2026 2026-08-18 09:34:02 +00:00
69670dd58f Stammvertreter aktualisiert 2026-08-18 09:22:15 +00:00
0d16aecf3a Stammvertreter aktualisiert 2026-08-14 04:07:55 +00:00
2d8931d140 Koordinatoren-Bericht deaktiviert für sonne-7x4m9k 2026-08-06 13:21:39 +00:00
5a6778de40 chapters/lumora-567294212/config.json aktualisiert 2026-08-06 11:50:31 +00:00
4506dbf8f6 chapters/lumora-567294212/config.json aktualisiert 2026-08-06 11:50:16 +00:00
c21f5605a2 Dateien nach "chapters/lumora-567294212" hochladen 2026-08-06 11:49:57 +00:00
a336e55759 Stammvertreter aktualisiert 2026-08-06 10:59:29 +00:00
9ce7c00b69 Stammvertreter aktualisiert 2026-08-06 10:54:10 +00:00
462d97b6cf Mitglieder aktualisiert 6.8.2026 2026-08-06 10:48:40 +00:00
c6a682fc62 Stammvertreter aktualisiert 2026-08-06 10:48:28 +00:00
611373b8d2 Mitglieder aktualisiert 6.8.2026 2026-08-06 10:39:20 +00:00
d4303dce98 Stammvertreter aktualisiert 2026-08-06 09:39:07 +00:00
6399f714fb Stammvertreter aktualisiert 2026-08-04 04:21:04 +00:00
f802da4a46 Mitglieder aktualisiert 3.8.2026 2026-08-03 06:14:44 +00:00
2170f3b8a4 Stammvertreter aktualisiert 2026-08-03 06:14:35 +00:00
62b5fcbfd6 Chapter Epsilon anlegen 2026-08-01 17:30:11 +00:00
6429b45fff Chapter Lumora Hybrid anlegen 2026-08-01 17:30:09 +00:00
7e7e1bdff0 Mitglieder aktualisiert 16.7.2026 2026-07-16 19:52:35 +00:00
27 changed files with 2145 additions and 744 deletions

4
.gitignore vendored
View File

@@ -1,2 +1,6 @@
Push
.DS_Store
screenshots/
anleitung.html
BNI-Vorstellungsrunde-Anleitung.pdf
start-*.html

Binary file not shown.

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BNI Admin-Panel</title>
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<style>
:root {
--red: #cc0000; --red-dark: #990000;
@@ -102,6 +105,7 @@
<div class="tabs">
<div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div>
<div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div>
<div class="tab" onclick="switchAdminTab('agenda')">Agenda</div>
<div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div>
<div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div>
</div>
@@ -146,6 +150,27 @@
</div>
</div>
<!-- ============================================================ AGENDA -->
<div id="tab-agenda" class="tab-content">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div class="card-title" style="margin:0;">Agenda-Folien (Online-Meeting)</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchAgendaAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadAgendaAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">
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.
</p>
<div class="status-msg" id="agenda-status"></div>
<div id="admin-agenda-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
<button class="btn btn-secondary btn-sm w-full" style="margin-top:12px;" onclick="openAddAgendaItem()">+ Agenda-Punkt hinzufügen</button>
</div>
</div>
<!-- ============================================================ SUPERADMIN -->
<div id="tab-superadmin" class="tab-content">
<div class="card">
@@ -291,6 +316,44 @@
</div>
</div>
<!-- Agenda Item Modal -->
<div class="modal-overlay" id="modal-agenda-item">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="agenda-item-modal-title">Agenda-Punkt</div>
<button class="modal-close" onclick="closeAdminModal('modal-agenda-item')">✕</button>
</div>
<input type="hidden" id="ai-id">
<label>Bezeichnung</label>
<input type="text" id="ai-label" placeholder="z.B. Begrüßung">
<label>Anzeigeart</label>
<select id="ai-type" onchange="updateAgendaItemTypeUI()">
<option value="image">Bild (Export, wird hochgeladen)</option>
<option value="embed">Live-Einbettung (Zoho Show o.ä.)</option>
</select>
<div id="ai-image-row">
<label>Bild</label>
<div class="photo-actions">
<img id="ai-image-thumb" class="photo-thumb" style="width:80px;height:80px;border-radius:8px;display:none;" src="" alt="">
<label class="btn-file-label">📁 Bild wählen<input type="file" id="ai-image-file" accept="image/*" style="display:none" onchange="handleAgendaImageFile(this)"></label>
</div>
<div id="ai-image-thumb-hint" style="display:none;font-size:0.78rem;color:#d97706;margin-top:6px;"></div>
</div>
<div id="ai-embed-row" style="display:none;">
<label>Einbettungs-URL</label>
<input type="text" id="ai-embed-url" placeholder="https://show.zoho.eu/publish/...">
<p style="font-size:0.78rem;color:var(--muted);margin-top:4px;">In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, Link hier einfügen.</p>
</div>
<label>Notizen — nur im Steuerpult sichtbar, nie auf dem Display</label>
<textarea id="ai-notes" rows="4" placeholder="z.B. Stichpunkte, was hier gesagt werden soll"></textarea>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="ai-delete-btn" onclick="deleteAgendaItem()" style="display:none;margin-right:auto;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-agenda-item')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveAgendaItem()">Speichern</button>
</div>
</div>
</div>
<!-- Import from Chapter Modal -->
<div class="modal-overlay" id="modal-import-chapter">
<div class="modal">
@@ -316,11 +379,12 @@
// SHARED CONSTANTS (copy from index.html)
// ============================================================
const APP_VERSION = '1.1.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';
const GITEA_RAW = 'https://git.digiseed.cloud/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw';
const GITEA_API = 'https://git.digiseed.cloud/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
function giteaAuthUrl(path, token) { return `${GITEA_API}/contents/${path}?token=${encodeURIComponent(token)}`; }
function giteaDataUrl(path) { return `${GITEA_RAW}/${path}?ref=data`; }
function giteaAuthUrl(path, token, ref) { return `${GITEA_API}/contents/${path}?token=${encodeURIComponent(token)}${ref ? '&ref=' + ref : ''}`; }
// ============================================================
// CHAPTER CONFIG
@@ -399,6 +463,7 @@ function switchAdminTab(tab) {
document.getElementById(`tab-${tab}`).classList.add('active');
if (tab === 'stammvertreter') renderSvList();
if (tab === 'mitglieder') renderMembersList();
if (tab === 'agenda') renderAgendaAdminList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings();
}
@@ -409,7 +474,7 @@ function switchAdminTab(tab) {
let _svAdminPhotoB64 = null;
function renderSvList() {
const list = loadStammvertreter();
const list = loadStammvertreter().sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de', { sensitivity: 'base' }));
const container = document.getElementById('admin-sv-list');
if (!list.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;padding:8px 0;">Noch keine Stammvertreter gespeichert.</div>`;
@@ -516,7 +581,7 @@ async function openImportFromChapter() {
const listEl = document.getElementById('import-chapter-select-list');
listEl.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade Chapter…</div>';
try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`);
const res = await fetch(`${GITEA_API}/contents/chapters?ref=data`);
const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
if (!chapters.length) { listEl.innerHTML = '<div style="color:var(--muted);">Keine anderen Chapter gefunden.</div>'; return; }
@@ -524,7 +589,7 @@ async function openImportFromChapter() {
for (const ch of chapters) {
if (activeChapter && ch.name === activeChapter.code) continue;
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfgRes = await fetch(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const btn = document.createElement('div');
btn.className = 'chapter-item';
@@ -550,7 +615,7 @@ async function loadChapterMembersForImport(code, chapterName) {
const listEl = document.getElementById('import-member-list');
listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Lade Mitglieder…</div>';
try {
const res = await fetch(giteaUrl(`chapters/${code}/members.json`));
const res = await fetch(giteaDataUrl(`chapters/${code}/members.json`));
if (!res.ok) { listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Keine Mitglieder gefunden.</div>'; return; }
const data = await res.json();
const members = data.members || [];
@@ -608,11 +673,11 @@ async function uploadSvToGitea() {
const path = `chapters/${activeChapter.code}/stammvertreter.json`;
try {
let sha = null;
const shaRes = await fetch(giteaAuthUrl(path, token));
const shaRes = await fetch(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: 'Stammvertreter aktualisiert', content };
const body = { message: 'Stammvertreter aktualisiert', content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(giteaAuthUrl(path, token), { method: sha?'PUT':'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(body) });
const res = await fetch(giteaAuthUrl(path, token, 'data'), { method: sha?'PUT':'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${list.length} Stammvertreter gesichert`; statusEl.style.color = 'var(--success)'; }
else { statusEl.textContent = 'Fehler beim Upload.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
@@ -622,7 +687,7 @@ async function downloadSvFromGitea() {
const statusEl = document.getElementById('sv-sync-status');
statusEl.textContent = 'Lade…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/stammvertreter.json`), { cache: 'no-store' });
const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/stammvertreter.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Daten auf Server.'; statusEl.style.color = 'var(--muted)'; return; }
const data = await res.json();
if (data.substitutes) { saveStammvertreter(data.substitutes); renderSvList(); statusEl.textContent = `✓ ${data.substitutes.length} Stammvertreter geladen`; statusEl.style.color = 'var(--success)'; }
@@ -719,7 +784,7 @@ async function fetchMembersAdmin() {
const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' });
const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Mitglieder auf Server.'; return; }
const data = await res.json();
if (!data.members || !data.members.length) { statusEl.textContent = 'Liste ist leer.'; return; }
@@ -756,14 +821,14 @@ async function uploadMembersAdmin() {
const data = { lastUpdated: new Date().toISOString().slice(0,10), members };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/members.json`;
const url = giteaAuthUrl(path, token);
const url = giteaAuthUrl(path, token, 'data');
console.log('[Upload] URL:', url);
statusEl.textContent = 'Prüfe SHA…';
let sha = null;
const shaRes = await fetch(url);
console.log('[Upload] SHA-Status:', shaRes.status);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content };
const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
statusEl.textContent = sha ? 'Aktualisiere…' : 'Erstelle…';
const res = await fetch(url, { method:sha?'PUT':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
@@ -780,6 +845,245 @@ async function uploadMembersAdmin() {
}
}
// ============================================================
// AGENDA-FOLIEN (Online-Meeting-Ablauf)
// ============================================================
let _agendaImageDataUrl = null;
let _agendaImageChanged = false;
let _agendaImageProcessing = null; // Promise, solange ein gewähltes Bild noch verkleinert wird
function slugify(s) {
const map = { ä:'ae', ö:'oe', ü:'ue', ß:'ss' };
return (s || '').toLowerCase().trim()
.replace(/[äöüß]/g, c => map[c])
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'punkt';
}
function renderAgendaAdminList() {
const state = loadState();
const container = document.getElementById('admin-agenda-list');
const items = state.agendaCatalog || [];
if (!items.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Noch keine Agenda-Punkte. Mit "+ Agenda-Punkt hinzufügen" starten.</div>`;
return;
}
container.innerHTML = '';
items.forEach(it => {
const div = document.createElement('div');
div.className = 'member-item';
div.style.cursor = 'pointer';
div.title = 'Bearbeiten';
const typeLabel = it.type === 'embed' ? 'Live-Einbettung' : 'Bild';
div.innerHTML = `
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(it.label || it.id)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${typeLabel}</div>
</div>
<button class="btn btn-ghost btn-sm">✎</button>
`;
div.onclick = () => editAgendaItem(it.id);
container.appendChild(div);
});
}
function openAddAgendaItem() {
document.getElementById('ai-id').value = '';
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt hinzufügen';
document.getElementById('ai-label').value = '';
document.getElementById('ai-type').value = 'image';
document.getElementById('ai-embed-url').value = '';
document.getElementById('ai-notes').value = '';
document.getElementById('ai-delete-btn').style.display = 'none';
_agendaImageDataUrl = null; _agendaImageChanged = false;
document.getElementById('ai-image-thumb').style.display = 'none';
document.getElementById('ai-image-thumb-hint').style.display = 'none';
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function editAgendaItem(id) {
const state = loadState();
const it = (state.agendaCatalog || []).find(a => a.id === id);
if (!it) return;
document.getElementById('ai-id').value = it.id;
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt bearbeiten';
document.getElementById('ai-label').value = it.label || '';
document.getElementById('ai-type').value = it.type || 'image';
document.getElementById('ai-embed-url').value = it.embedUrl || '';
document.getElementById('ai-notes').value = it.notes || '';
document.getElementById('ai-delete-btn').style.display = 'inline-flex';
_agendaImageDataUrl = null; _agendaImageChanged = false;
const thumb = document.getElementById('ai-image-thumb');
const thumbHint = document.getElementById('ai-image-thumb-hint');
if (it.imagePath) {
thumb.onerror = () => {
thumb.style.display = 'none';
if (thumbHint) { thumbHint.textContent = '⚠ Bild nicht auf dem Server gefunden – bitte neu hochladen.'; thumbHint.style.display = 'block'; }
};
thumb.onload = () => { if (thumbHint) thumbHint.style.display = 'none'; };
if (thumbHint) thumbHint.style.display = 'none';
thumb.src = it.imagePath.startsWith('http') ? it.imagePath : giteaDataUrl(`chapters/${activeChapter.code}/${it.imagePath}`);
thumb.style.display = 'block';
} else {
thumb.style.display = 'none';
if (thumbHint) thumbHint.style.display = 'none';
}
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function updateAgendaItemTypeUI() {
const type = document.getElementById('ai-type').value;
document.getElementById('ai-image-row').style.display = type === 'image' ? 'block' : 'none';
document.getElementById('ai-embed-row').style.display = type === 'embed' ? 'block' : 'none';
}
function handleAgendaImageFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const hint = document.getElementById('ai-image-thumb-hint');
if (hint) { hint.textContent = 'Bild wird verarbeitet…'; hint.style.color = 'var(--muted)'; hint.style.display = 'block'; }
// Auf eine für Vollbild-Anzeige ausreichende Größe verkleinern/komprimieren, damit der
// Upload sicher unter dem Größenlimit der Gitea-Contents-API bleibt (typischerweise ~1 MB/Datei).
// _agendaImageProcessing merkt sich das laufende Promise, damit saveAgendaItem() nicht zu früh
// speichert, während das Bild noch verarbeitet wird (führte sonst zu einem stillen "kein Bild gewählt").
_agendaImageProcessing = resizePhotoToBase64(file, 1920).then(b64 => {
_agendaImageDataUrl = b64;
_agendaImageChanged = true;
const thumb = document.getElementById('ai-image-thumb');
thumb.src = b64;
thumb.style.display = 'block';
if (hint) {
const kb = Math.round((b64.length * 0.75) / 1024);
hint.textContent = `Für den Upload verkleinert auf max. 1920px, ~${kb} KB.`;
hint.style.color = 'var(--muted)';
hint.style.display = 'block';
}
}).catch(() => alert('Bild konnte nicht gelesen werden.'))
.finally(() => { _agendaImageProcessing = null; });
input.value = '';
}
async function uploadAgendaImage(path, dataUrl, token) {
const content = dataUrl.split(',')[1];
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda-Bild aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status}: ${t.slice(0, 80)}`); }
}
async function saveAgendaItem() {
const label = document.getElementById('ai-label').value.trim();
if (!label) { alert('Bitte eine Bezeichnung eingeben.'); return; }
const type = document.getElementById('ai-type').value;
if (_agendaImageProcessing) {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Bild wird noch verarbeitet…'; statusEl.style.color = 'var(--muted)';
await _agendaImageProcessing;
}
const state = loadState();
if (!state.agendaCatalog) state.agendaCatalog = [];
let id = document.getElementById('ai-id').value;
if (!id) {
const base = slugify(label);
const ids = new Set(state.agendaCatalog.map(a => a.id));
let candidate = base, n = 1;
while (ids.has(candidate)) candidate = `${base}-${++n}`;
id = candidate;
}
const existing = state.agendaCatalog.find(a => a.id === id) || {};
const notes = document.getElementById('ai-notes').value.trim();
const item = { id, label, type };
if (notes) item.notes = notes;
if (type === 'embed') {
const url = document.getElementById('ai-embed-url').value.trim();
if (!url) { alert('Bitte eine Einbettungs-URL eingeben.'); return; }
item.embedUrl = url;
} else {
item.imagePath = existing.imagePath || `agenda/${id}.jpg`;
if (!_agendaImageChanged && !existing.imagePath) { alert('Bitte ein Bild wählen.'); return; }
}
const statusEl = document.getElementById('agenda-status');
if (type === 'image' && _agendaImageChanged && _agendaImageDataUrl) {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
statusEl.textContent = 'Lade Bild hoch…'; statusEl.style.color = 'var(--muted)';
try {
await uploadAgendaImage(`chapters/${activeChapter.code}/${item.imagePath}`, _agendaImageDataUrl, token);
} catch(e) {
statusEl.textContent = `Fehler beim Bild-Upload: ${e.message}`; statusEl.style.color = '#dc2626';
alert(`Bild-Upload fehlgeschlagen: ${e.message}\n\nDer Agenda-Punkt wurde NICHT gespeichert.`);
return;
}
}
const idx = state.agendaCatalog.findIndex(a => a.id === id);
if (idx >= 0) state.agendaCatalog[idx] = item; else state.agendaCatalog.push(item);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
statusEl.textContent = '✓ Lokal gespeichert – "Auf Server hochladen" nicht vergessen, damit es im Steuerpult ankommt.';
statusEl.style.color = 'var(--success)';
}
function deleteAgendaItem() {
const id = document.getElementById('ai-id').value;
if (!id) return;
if (!confirm('Diesen Agenda-Punkt wirklich löschen?')) return;
const state = loadState();
state.agendaCatalog = (state.agendaCatalog || []).filter(a => a.id !== id);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
}
async function fetchAgendaAdmin() {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/agenda.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Noch keine Agenda auf dem Server.'; return; }
const data = await res.json();
const state = loadState();
state.agendaCatalog = Array.isArray(data.items) ? data.items : [];
saveState(state);
statusEl.textContent = `✓ ${state.agendaCatalog.length} Punkte geladen`; statusEl.style.color = 'var(--success)';
renderAgendaAdminList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadAgendaAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
try {
const state = loadState();
const data = { items: state.agendaCatalog || [] };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/agenda.json`;
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${data.items.length} Agenda-Punkte hochgeladen`; statusEl.style.color = 'var(--success)'; }
else { const t = await res.text(); statusEl.textContent = `Fehler ${res.status}: ${t.slice(0, 80)}`; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// SUPERADMIN
// ============================================================
@@ -789,16 +1093,16 @@ async function loadAllChapters() {
const container = document.getElementById('all-chapters-list');
container.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade…</div>';
try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`);
const res = await fetch(`${GITEA_API}/contents/chapters?ref=data`);
const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
_allChaptersData = [];
container.innerHTML = '';
for (const ch of chapters) {
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfgRes = await fetch(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const membersRes = await fetch(giteaUrl(`chapters/${ch.name}/members.json`));
const membersRes = await fetch(giteaDataUrl(`chapters/${ch.name}/members.json`));
const membersData = membersRes.ok ? await membersRes.json() : {};
_allChaptersData.push({ code: ch.name, cfg });
const isEnabled = !!cfg.memberCoordinatorReport;
@@ -927,18 +1231,18 @@ async function toggleCoordinatorReport(code, currentEnabled) {
const token = getAdminToken();
if (!token) { alert('Bitte Admin-Token in den Einstellungen eintragen.'); return; }
try {
const cfgRes = await fetch(giteaUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
const cfgRes = await fetch(giteaDataUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
if (!cfgRes.ok) { alert('Config nicht gefunden.'); return; }
const cfg = await cfgRes.json();
const shaRes = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token));
const shaRes = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'));
if (!shaRes.ok) { alert('SHA-Fetch fehlgeschlagen.'); return; }
const sha = (await shaRes.json()).sha;
cfg.memberCoordinatorReport = !currentEnabled;
const content = btoa(unescape(encodeURIComponent(JSON.stringify(cfg, null, 2))));
const res = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token), {
const res = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: `Koordinatoren-Bericht ${cfg.memberCoordinatorReport ? 'aktiviert' : 'deaktiviert'} für ${code}`, content, sha })
body: JSON.stringify({ message: `Koordinatoren-Bericht ${cfg.memberCoordinatorReport ? 'aktiviert' : 'deaktiviert'} für ${code}`, content, sha, branch: 'data' })
});
if (res.ok) { loadAllChapters(); }
else { alert('Fehler beim Speichern der Konfiguration.'); }
@@ -953,7 +1257,7 @@ async function loadCoordinatorReportData() {
form.style.display = 'block';
_coordReportData = null;
try {
const res = await fetch(giteaUrl(`chapters/${code}/coordinator-report-${year}.json`), { cache: 'no-store' });
const res = await fetch(giteaDataUrl(`chapters/${code}/coordinator-report-${year}.json`), { cache: 'no-store' });
if (res.ok) _coordReportData = await res.json();
} catch(e) {}
if (!_coordReportData) {
@@ -1129,14 +1433,14 @@ async function _saveCoordinatorReportToGitea(code, data) {
_coordStatus('Speichere…');
let sha = null;
try {
const shaRes = await fetch(giteaAuthUrl(path, token));
const shaRes = await fetch(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha;
} catch(e) {}
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const body = { message: `Update Koordinatoren-Bericht ${year} für ${code}`, content };
const body = { message: `Update Koordinatoren-Bericht ${year} für ${code}`, content, branch: 'data' };
if (sha) body.sha = sha;
try {
const res = await fetch(giteaAuthUrl(path, token), {
const res = await fetch(giteaAuthUrl(path, token, 'data'), {
method: sha ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)

View File

@@ -1,598 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BNI Vorstellungsrunde – Anleitung</title>
<style>
:root {
--red: #cc0000; --navy: #1a2744; --bg: #f0f2f5;
--card: #fff; --muted: #6b7280; --border: #e5e7eb;
--success: #16a34a;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:var(--bg); color:#1a1a2e; line-height:1.6; }
.cover { background:linear-gradient(160deg,#0f1f45,#1a2744,#0d1b35); color:white; padding:60px 40px; text-align:center; }
.cover h1 { font-size:2rem; font-weight:800; letter-spacing:-0.5px; }
.cover .subtitle { color:rgba(255,255,255,0.6); margin-top:8px; font-size:1rem; }
.cover .badge { display:inline-block; margin-top:24px; background:var(--red); color:white; padding:6px 18px; border-radius:20px; font-size:0.82rem; font-weight:600; }
.toc { background:white; padding:24px 32px; border-bottom:1px solid var(--border); }
.toc h2 { font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:12px; }
.toc ol { padding-left:18px; }
.toc li { font-size:0.9rem; padding:3px 0; }
.toc a { color:var(--navy); text-decoration:none; }
.toc a:hover { color:var(--red); }
.content { max-width:820px; margin:0 auto; padding:32px 24px 64px; }
.section { margin-bottom:48px; }
.section-header { display:flex; align-items:center; gap:14px; margin-bottom:20px; padding-bottom:12px; border-bottom:2px solid var(--red); }
.step-num { width:36px; height:36px; background:var(--red); color:white; border-radius:50%; display:flex; align-items:center; justify-content:center; font-weight:800; font-size:1rem; flex-shrink:0; }
.section-header h2 { font-size:1.25rem; font-weight:700; color:var(--navy); }
.card { background:white; border-radius:12px; border:1px solid var(--border); padding:20px; margin-bottom:16px; }
.tip { background:#fef3c7; border:1px solid #fcd34d; border-radius:10px; padding:14px 16px; font-size:0.88rem; margin-bottom:16px; }
.tip strong { color:#92400e; }
.info { background:#eff6ff; border:1px solid #bfdbfe; border-radius:10px; padding:14px 16px; font-size:0.88rem; margin-bottom:16px; }
.steps { counter-reset:step; }
.step { display:flex; gap:14px; margin-bottom:12px; align-items:flex-start; }
.step::before { counter-increment:step; content:counter(step); width:24px; height:24px; background:var(--navy); color:white; border-radius:50%; font-size:0.78rem; font-weight:700; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2px; }
.step p { font-size:0.9rem; }
.step code { background:#f3f4f6; padding:1px 6px; border-radius:4px; font-size:0.85rem; }
/* ——— MOCKUPS ——— */
.mockup { background:#1a1a2e; border-radius:12px; overflow:hidden; margin:16px 0; box-shadow:0 4px 20px rgba(0,0,0,0.2); }
.mockup-bar { background:#0f1420; padding:8px 14px; display:flex; align-items:center; gap:8px; }
.mockup-dot { width:10px; height:10px; border-radius:50%; }
.mockup-body { padding:16px; }
.mock-activation { background:linear-gradient(160deg,#0f1f45,#1a2744); padding:32px 20px; text-align:center; border-radius:8px; }
.mock-activation h3 { color:white; font-size:1.1rem; margin-bottom:6px; }
.mock-activation p { color:rgba(255,255,255,0.5); font-size:0.82rem; margin-bottom:20px; }
.mock-input { background:rgba(255,255,255,0.1); border:1px solid rgba(255,255,255,0.2); border-radius:8px; padding:10px 14px; color:white; font-size:0.9rem; text-align:center; margin-bottom:12px; width:100%; max-width:280px; }
.mock-btn-red { background:#cc0000; color:white; border:none; border-radius:8px; padding:10px 24px; font-weight:700; cursor:pointer; font-size:0.9rem; }
.mock-btn-gray { background:#374151; color:white; border:none; border-radius:8px; padding:8px 16px; font-weight:600; cursor:pointer; font-size:0.82rem; }
.mock-panel { background:#1e2235; border-radius:10px; padding:12px; }
.mock-member { display:flex; align-items:center; gap:10px; padding:8px 10px; background:rgba(255,255,255,0.05); border-radius:8px; margin-bottom:6px; }
.mock-avatar { width:36px; height:36px; border-radius:50%; object-fit:cover; flex-shrink:0; display:flex; align-items:center; justify-content:center; font-size:0.75rem; font-weight:700; color:white; }
.mock-name { font-size:0.85rem; font-weight:600; color:white; }
.mock-role { font-size:0.72rem; color:rgba(255,255,255,0.5); }
.mock-badge { padding:2px 8px; border-radius:12px; font-size:0.7rem; font-weight:700; margin-left:auto; }
.mock-badge.absent { background:rgba(204,0,0,0.2); color:#f87171; }
.mock-badge.present { background:rgba(22,163,74,0.2); color:#4ade80; }
.mock-timer { text-align:center; padding:20px; }
.mock-timer-ring { width:100px; height:100px; border-radius:50%; border:6px solid #cc0000; display:flex; align-items:center; justify-content:center; margin:0 auto 12px; }
.mock-timer-num { font-size:2rem; font-weight:800; color:white; }
.mock-timer-name { color:white; font-weight:700; font-size:1rem; }
.mock-timer-company { color:rgba(255,255,255,0.5); font-size:0.8rem; }
.mock-tabs { display:flex; border-bottom:1px solid rgba(255,255,255,0.1); margin-bottom:12px; }
.mock-tab { padding:8px 14px; font-size:0.78rem; color:rgba(255,255,255,0.4); border-bottom:2px solid transparent; }
.mock-tab.active { color:#cc0000; border-bottom-color:#cc0000; }
.mock-sub-modal { background:#2a2f45; border-radius:12px; padding:16px; }
.mock-sub-modal h4 { color:white; font-size:0.9rem; margin-bottom:12px; }
.mock-field { background:rgba(255,255,255,0.1); border-radius:6px; padding:8px 10px; color:rgba(255,255,255,0.7); font-size:0.8rem; margin-bottom:8px; }
.mock-sv-card { display:flex; align-items:center; gap:8px; background:rgba(255,255,255,0.07); border-radius:8px; padding:8px 10px; margin-bottom:6px; border:1px solid transparent; cursor:pointer; }
.mock-sv-card:hover { border-color:rgba(204,0,0,0.5); }
.mock-sv-name { color:white; font-size:0.82rem; font-weight:600; }
.mock-sv-meta { color:rgba(255,255,255,0.45); font-size:0.72rem; }
.admin-mockup { background:#f0f2f5; border-radius:12px; overflow:hidden; border:1px solid var(--border); margin:16px 0; }
.admin-header { background:#1a2744; color:white; padding:12px 16px; font-size:0.9rem; font-weight:700; }
.admin-tabs { background:white; display:flex; border-bottom:1px solid var(--border); padding:0 16px; }
.admin-tab { padding:10px 16px; font-size:0.82rem; font-weight:600; color:var(--muted); border-bottom:2px solid transparent; }
.admin-tab.active { color:var(--red); border-bottom-color:var(--red); }
.admin-body { padding:16px; }
.admin-member-row { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--border); cursor:pointer; }
.admin-member-row:last-child { border-bottom:none; }
.admin-photo-badge { font-size:0.72rem; margin-left:auto; }
.admin-photo-badge.ok { color:var(--success); }
.admin-photo-badge.missing { color:#d97706; }
.highlight-box { border-left:3px solid var(--red); padding:10px 16px; background:white; border-radius:0 8px 8px 0; margin:12px 0; font-size:0.88rem; }
@page { margin: 14mm 14mm 14mm 14mm; }
@media print {
body { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
.cover {
-webkit-print-color-adjust:exact; print-color-adjust:exact;
min-height: 100vh;
display: flex; flex-direction: column; align-items: center; justify-content: center;
page-break-after: always;
}
.toc { page-break-inside: avoid; }
.section { page-break-inside:avoid; }
.mockup, .admin-mockup { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
</style>
</head>
<body>
<div class="cover">
<div style="font-size:2.5rem; margin-bottom:8px;">🌟</div>
<h1>BNI Vorstellungsrunde</h1>
<div class="subtitle">Benutzeranleitung für Chapter-Admins</div>
<div class="badge">Version 1.2 · April 2026</div>
</div>
<div class="toc">
<h2>Inhalt</h2>
<ol>
<li><a href="#start">Tool starten</a></li>
<li><a href="#aktivierung">Erstmalige Aktivierung</a></li>
<li><a href="#ansichten">Ansichten: Display &amp; Steuerung</a></li>
<li><a href="#vorbereitung">Meeting vorbereiten</a></li>
<li><a href="#durchfuehrung">Meeting durchführen</a></li>
<li><a href="#vertreter">Vertreter eintragen</a></li>
<li><a href="#besucher">Besucher eintragen</a></li>
<li><a href="#admin">Admin-Panel: Fotos &amp; Stammvertreter</a></li>
<li><a href="#offline">Offline-Nutzung</a></li>
<li><a href="#update">Updates einspielen</a></li>
<li><a href="#timer-schalter">Zeitanzeige deaktivieren</a></li>
</ol>
</div>
<div class="content">
<!-- ========== 1. START ========== -->
<div class="section" id="start">
<div class="section-header">
<div class="step-num">1</div>
<h2>Tool starten</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Den Ordner <code>BNI Sonne</code> im Finder öffnen.</p></div>
<div class="step"><p>Die Datei <strong>start.command</strong> doppelklicken — es öffnet sich kurz ein Terminal-Fenster, dann startet Chrome automatisch mit dem Tool.</p></div>
<div class="step"><p>Das Terminal-Fenster im Hintergrund lassen (nicht schließen, solange du das Tool nutzt).</p></div>
</div>
</div>
<div class="tip">
<strong>Hinweis:</strong> Beim ersten Doppelklick erscheint eventuell eine Sicherheitswarnung von macOS. Dann: Rechtsklick auf <code>start.command</code> → „Öffnen" → „Öffnen" bestätigen. Ab dem zweiten Mal klappt der Doppelklick direkt.
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Terminal</span>
</div>
<div class="mockup-body">
<p style="color:#4ade80;font-size:0.82rem;font-family:monospace;">$ python3 -m http.server 8181</p>
<p style="color:rgba(255,255,255,0.4);font-size:0.82rem;font-family:monospace;">Serving HTTP on 0.0.0.0 port 8181 …</p>
</div>
</div>
</div>
<!-- ========== 2. AKTIVIERUNG ========== -->
<div class="section" id="aktivierung">
<div class="section-header">
<div class="step-num">2</div>
<h2>Erstmalige Aktivierung</h2>
</div>
<div class="info">
Dieser Schritt ist nur beim <strong>allerersten Start</strong> auf einem neuen Gerät oder Browser nötig. Danach öffnet das Tool direkt.
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Den <strong>Lizenzcode</strong> deines Chapters eingeben (erhältst du vom Administrator).</p></div>
<div class="step"><p>Auf <strong>„Aktivieren"</strong> klicken. Das Tool lädt einmalig die Chapter-Daten vom Server.</p></div>
<div class="step"><p>Nach erfolgreicher Aktivierung öffnet sich direkt die Steuerungsansicht.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">http://localhost:8181</span>
</div>
<div class="mockup-body">
<div class="mock-activation">
<h3>BNI Vorstellungsrunde</h3>
<p>Bitte Chapter-Code eingeben</p>
<div style="margin-bottom:8px;"><input class="mock-input" placeholder="z. B. sonne-7x4m9k" readonly></div>
<button class="mock-btn-red">Aktivieren</button>
</div>
</div>
</div>
</div>
<!-- ========== 3. ANSICHTEN ========== -->
<div class="section" id="ansichten">
<div class="section-header">
<div class="step-num">3</div>
<h2>Ansichten: Display &amp; Steuerung</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;">Das Tool läuft in <strong>zwei Fenstern gleichzeitig</strong>:</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px;">
<div style="background:#f8f9fa;border-radius:8px;padding:14px;border-left:3px solid var(--navy);">
<div style="font-weight:700;font-size:0.88rem;color:var(--navy);margin-bottom:6px;">🖥 Display-Fenster</div>
<div style="font-size:0.82rem;color:var(--muted);">Wird auf dem Präsentationsmonitor / Beamer angezeigt. Zeigt das aktuelle Mitglied groß mit Timer.</div>
</div>
<div style="background:#f8f9fa;border-radius:8px;padding:14px;border-left:3px solid var(--red);">
<div style="font-weight:700;font-size:0.88rem;color:var(--red);margin-bottom:6px;">🎛 Steuerungs-Fenster</div>
<div style="font-size:0.82rem;color:var(--muted);">Dein Laptop-Bildschirm. Hier steuerst du den Ablauf, trägst Vertreter ein und navigierst.</div>
</div>
</div>
</div>
<div class="highlight-box">
Beim Öffnen des Tools wird automatisch ein zweites Tab für das Display geöffnet. Ziehe dieses Tab auf den Beamer-Bildschirm und drücke <strong>F11</strong> für Vollbild.
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Display-Ansicht (Beamer)</span>
</div>
<div class="mockup-body">
<div class="mock-timer">
<div style="color:rgba(255,255,255,0.4);font-size:0.75rem;margin-bottom:10px;">JETZT SPRICHT</div>
<div class="mock-timer-ring">
<span class="mock-timer-num">45</span>
</div>
<div class="mock-timer-name">Maria Mustermann</div>
<div class="mock-timer-company">Muster GmbH · Steuerberatung</div>
<div style="color:rgba(255,255,255,0.3);font-size:0.75rem;margin-top:8px;">Nächster: Hans Müller</div>
</div>
</div>
</div>
</div>
<!-- ========== 4. VORBEREITUNG ========== -->
<div class="section" id="vorbereitung">
<div class="section-header">
<div class="step-num">4</div>
<h2>Meeting vorbereiten</h2>
</div>
<div class="card">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Vor dem Meeting: Abwesenheiten markieren</p>
<div class="steps">
<div class="step"><p>Im Steuerungs-Fenster: Tab <strong>„Mitglieder"</strong> öffnen.</p></div>
<div class="step"><p>Auf den Namen eines abwesenden Mitglieds klicken → kleines Menü öffnet sich.</p></div>
<div class="step"><p><strong>„Abwesend"</strong> wählen — das Mitglied wird aus der Vorstellungsrunde ausgeblendet (roter Punkt).</p></div>
<div class="step"><p>Optional: Reihenfolge per <strong>Drag &amp; Drop</strong> anpassen.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Reihenfolge</span>
</div>
<div class="mockup-body">
<div style="font-size:0.72rem;color:rgba(255,255,255,0.35);font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:10px;">Reihenfolge (Ziehen zum Sortieren)</div>
<!-- Nächster -->
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);border-radius:10px;margin-bottom:6px;">
<span style="color:rgba(255,255,255,0.25);font-size:1rem;">⠿</span>
<div style="width:38px;height:38px;border-radius:50%;background:#1a2744;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;color:white;flex-shrink:0;">AM</div>
<div style="flex:1;"><div style="font-size:0.85rem;font-weight:700;color:white;">Anna Müller</div><div style="font-size:0.72rem;color:rgba(255,255,255,0.45);">Steuerberatung</div></div>
<span style="background:#374151;color:white;font-size:0.68rem;font-weight:700;padding:2px 8px;border-radius:6px;letter-spacing:0.5px;">NÄCHSTER</span>
<button style="background:rgba(255,255,255,0.08);border:none;border-radius:6px;padding:5px 8px;color:rgba(255,255,255,0.6);cursor:pointer;font-size:0.85rem;">↺</button>
<button style="background:rgba(204,0,0,0.15);border:none;border-radius:6px;padding:5px 8px;color:rgba(255,100,100,0.7);cursor:pointer;font-size:0.85rem;">🚫</button>
</div>
<!-- Abwesend -->
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:rgba(204,0,0,0.08);border:1px solid rgba(204,0,0,0.2);border-radius:10px;margin-bottom:6px;opacity:0.6;">
<span style="color:rgba(255,255,255,0.25);font-size:1rem;">⠿</span>
<div style="width:38px;height:38px;border-radius:50%;background:#7f1d1d;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;color:white;flex-shrink:0;">KS</div>
<div style="flex:1;"><div style="font-size:0.85rem;font-weight:700;color:rgba(255,255,255,0.5);">Klaus Schmidt</div><div style="font-size:0.72rem;color:rgba(255,255,255,0.3);">IT-Systeme</div></div>
<button style="background:rgba(255,255,255,0.08);border:none;border-radius:6px;padding:5px 8px;color:rgba(255,255,255,0.4);cursor:pointer;font-size:0.85rem;">↺</button>
<button style="background:rgba(204,0,0,0.3);border:none;border-radius:6px;padding:5px 8px;color:#f87171;cursor:pointer;font-size:0.85rem;">🚫</button>
</div>
<!-- Normal -->
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);border-radius:10px;">
<span style="color:rgba(255,255,255,0.25);font-size:1rem;">⠿</span>
<div style="width:38px;height:38px;border-radius:50%;background:#1e3a5f;display:flex;align-items:center;justify-content:center;font-size:0.75rem;font-weight:700;color:white;flex-shrink:0;">SL</div>
<div style="flex:1;"><div style="font-size:0.85rem;font-weight:700;color:white;">Sandra Lang</div><div style="font-size:0.72rem;color:rgba(255,255,255,0.45);">Marketing</div></div>
<button style="background:rgba(255,255,255,0.08);border:none;border-radius:6px;padding:5px 8px;color:rgba(255,255,255,0.6);cursor:pointer;font-size:0.85rem;">↺</button>
<button style="background:rgba(255,255,255,0.05);border:none;border-radius:6px;padding:5px 8px;color:rgba(255,255,255,0.4);cursor:pointer;font-size:0.85rem;">🚫</button>
</div>
<div style="margin-top:10px;font-size:0.75rem;color:rgba(255,255,255,0.35);">↺ = Vertretung eintragen &nbsp;·&nbsp; 🚫 = Als abwesend markieren</div>
</div>
</div>
</div>
<!-- ========== 5. DURCHFÜHRUNG ========== -->
<div class="section" id="durchfuehrung">
<div class="section-header">
<div class="step-num">5</div>
<h2>Meeting durchführen</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Auf <strong>„Start"</strong> klicken — der Timer startet für das erste Mitglied.</p></div>
<div class="step"><p>Wenn die Zeit abgelaufen ist (Timer rot): <strong>„Weiter →"</strong> drücken für das nächste Mitglied.</p></div>
<div class="step"><p>Mit <strong>„← Zurück"</strong> kannst du jederzeit zum vorherigen Mitglied springen.</p></div>
<div class="step"><p>Timer lässt sich jederzeit mit <strong>„Pause / Fortsetzen"</strong> anhalten.</p></div>
<div class="step"><p>Am Ende erscheint eine Abschluss-Ansicht — mit <strong>„Neu starten"</strong> zurück zum Anfang.</p></div>
</div>
</div>
<div class="tip">
<strong>Tipp:</strong> Die Tastenkombination <strong>→ Pfeiltaste</strong> auf der Tastatur funktioniert als „Weiter" — praktisch für die Fernbedienung.
</div>
</div>
<!-- ========== 6. VERTRETER ========== -->
<div class="section" id="vertreter">
<div class="section-header">
<div class="step-num">6</div>
<h2>Vertreter eintragen</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;margin-bottom:12px;">Wenn ein Mitglied einen Vertreter schickt, wird dieser in der Vorstellungsrunde an Stelle des Mitglieds angezeigt.</p>
<div class="steps">
<div class="step"><p>Tab <strong>„Vertretungen"</strong> öffnen.</p></div>
<div class="step"><p>Mitglied aus der Liste wählen, das vertreten wird.</p></div>
<div class="step"><p>Falls die Person bereits als <strong>Stammvertreter</strong> gespeichert ist: oben aus der Schnellauswahl wählen — alle Felder werden automatisch ausgefüllt.</p></div>
<div class="step"><p>Andernfalls: Name, Firma und Fachgebiet des Vertreters eingeben und ggf. ein Foto hochladen.</p></div>
<div class="step"><p>Auf <strong>„Speichern"</strong> klicken.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Vertretung eintragen</span>
</div>
<div class="mockup-body">
<div class="mock-sub-modal">
<h4>Vertreter für Klaus Schmidt</h4>
<div style="font-size:0.72rem;color:rgba(255,255,255,0.4);margin-bottom:8px;">STAMMVERTRETER WÄHLEN</div>
<div class="mock-sv-card">
<div class="mock-avatar" style="background:#1a2744;width:32px;height:32px;">MF</div>
<div><div class="mock-sv-name">Max Fischer</div><div class="mock-sv-meta">Fischer GmbH · Versicherung</div></div>
</div>
<div class="mock-sv-card">
<div class="mock-avatar" style="background:#4a1942;width:32px;height:32px;">LB</div>
<div><div class="mock-sv-name">Lisa Berger</div><div class="mock-sv-meta">Berger &amp; Co · Marketing</div></div>
</div>
<div style="font-size:0.72rem;color:rgba(255,255,255,0.4);margin:10px 0 6px;">ODER MANUELL EINGEBEN</div>
<div class="mock-field">Name des Vertreters …</div>
<div class="mock-field">Firma …</div>
</div>
</div>
</div>
</div>
<!-- ========== 7. BESUCHER ========== -->
<div class="section" id="besucher">
<div class="section-header">
<div class="step-num">7</div>
<h2>Besucher eintragen</h2>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Tab <strong>„Besucher"</strong> öffnen.</p></div>
<div class="step"><p>Auf <strong>„+ Besucher hinzufügen"</strong> klicken.</p></div>
<div class="step"><p>Name und optional Firma eingeben und speichern.</p></div>
<div class="step"><p>Besucher erscheinen am Ende der Vorstellungsrunde nach allen Mitgliedern.</p></div>
</div>
</div>
<div class="tip">
<strong>Tipp:</strong> Besucher werden nicht dauerhaft gespeichert — sie werden nach dem Meeting automatisch gelöscht, wenn die Runde zurückgesetzt wird.
</div>
</div>
<!-- ========== 8. ADMIN ========== -->
<div class="section" id="admin">
<div class="section-header">
<div class="step-num">8</div>
<h2>Admin-Panel: Fotos &amp; Stammvertreter</h2>
</div>
<div class="info">
Das Admin-Panel öffnest du über <code>http://localhost:8181/admin.html</code> — am einfachsten über den Link im Steuerungs-Fenster unter dem Tab <strong>„Admin"</strong>.
</div>
<div class="card">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Mitgliederfotos hinzufügen</p>
<div class="steps">
<div class="step"><p>Im Admin-Panel Tab <strong>„Mitglieder"</strong> öffnen.</p></div>
<div class="step"><p>Auf ein Mitglied mit <strong>„+ Foto"</strong> klicken.</p></div>
<div class="step"><p>Foto aus dem Finder wählen (JPG/PNG, beliebige Größe — wird automatisch verkleinert).</p></div>
<div class="step"><p>Auf <strong>„Speichern"</strong> klicken — das Foto ist jetzt lokal gespeichert.</p></div>
<div class="step"><p>Nach allen Fotos: <strong>„Auf Server hochladen"</strong> — dann haben alle Geräte die Fotos beim nächsten „Vom Server laden".</p></div>
</div>
</div>
<div class="admin-mockup">
<div class="admin-header">BNI Admin-Panel · BNI Chapter Sonne</div>
<div class="admin-tabs">
<div class="admin-tab">Stammvertreter</div>
<div class="admin-tab active">Mitglieder</div>
<div class="admin-tab">Einstellungen</div>
</div>
<div class="admin-body">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<span style="font-weight:700;font-size:0.9rem;">Mitglieder</span>
<div style="display:flex;gap:8px;">
<span style="background:#f3f4f6;border:1px solid var(--border);border-radius:6px;padding:5px 10px;font-size:0.78rem;font-weight:600;">🔄 Vom Server laden</span>
<span style="background:var(--red);color:white;border-radius:6px;padding:5px 10px;font-size:0.78rem;font-weight:600;">📤 Auf Server hochladen</span>
</div>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#1a2744;width:40px;height:40px;font-size:0.8rem;">AM</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Anna Müller</div><div style="font-size:0.75rem;color:var(--muted);">Steuerberatung</div></div>
<span class="admin-photo-badge ok">✓ Foto</span>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#4a1942;width:40px;height:40px;font-size:0.8rem;">KS</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Klaus Schmidt</div><div style="font-size:0.75rem;color:var(--muted);">IT-Systeme</div></div>
<span class="admin-photo-badge missing">+ Foto</span>
</div>
<div class="admin-member-row">
<div class="mock-avatar" style="background:#1e3a5f;width:40px;height:40px;font-size:0.8rem;">SL</div>
<div style="flex:1;"><div style="font-size:0.88rem;font-weight:600;">Sandra Lang</div><div style="font-size:0.75rem;color:var(--muted);">Marketing</div></div>
<span class="admin-photo-badge ok">✓ Foto</span>
</div>
</div>
</div>
<div class="card" style="margin-top:16px;">
<p style="font-weight:700;font-size:0.9rem;margin-bottom:10px;">Stammvertreter anlegen</p>
<p style="font-size:0.88rem;color:var(--muted);margin-bottom:12px;">Stammvertreter sind Personen, die regelmäßig als Vertreter kommen. Einmal anlegen → bei jeder Vertretung schnell auswählen.</p>
<div class="steps">
<div class="step"><p>Tab <strong>„Stammvertreter"</strong> öffnen.</p></div>
<div class="step"><p>Auf <strong>„+ Hinzufügen"</strong> klicken.</p></div>
<div class="step"><p>Name, Firma, Fachgebiet und Foto eingeben.</p></div>
<div class="step"><p>Optional: <strong>„Aus anderem Chapter"</strong> — Mitglied aus einem anderen Chapter-Verzeichnis als Stammvertreter übernehmen.</p></div>
<div class="step"><p>Auf <strong>„Auf Server sichern"</strong> klicken, damit die Liste nicht verloren geht.</p></div>
</div>
</div>
</div>
<!-- ========== 9. OFFLINE ========== -->
<div class="section" id="offline">
<div class="section-header">
<div class="step-num">9</div>
<h2>Offline-Nutzung</h2>
</div>
<div class="card">
<p style="font-size:0.9rem;margin-bottom:12px;">Das Tool funktioniert <strong>vollständig ohne Internet</strong>, sobald es einmal online eingerichtet wurde.</p>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div style="background:#f0fdf4;border-radius:8px;padding:12px;border:1px solid #bbf7d0;">
<div style="font-weight:700;color:var(--success);font-size:0.85rem;margin-bottom:6px;">✓ Funktioniert offline</div>
<ul style="font-size:0.82rem;color:#166534;padding-left:16px;line-height:1.7;">
<li>Meeting starten &amp; steuern</li>
<li>Timer, Navigation</li>
<li>Abwesenheiten markieren</li>
<li>Vertreter &amp; Besucher eintragen</li>
<li>Stammvertreter verwenden</li>
</ul>
</div>
<div style="background:#fef2f2;border-radius:8px;padding:12px;border:1px solid #fecaca;">
<div style="font-weight:700;color:#dc2626;font-size:0.85rem;margin-bottom:6px;">✗ Braucht Internet</div>
<ul style="font-size:0.82rem;color:#991b1b;padding-left:16px;line-height:1.7;">
<li>Erstmalige Aktivierung</li>
<li>Mitglieder vom Server laden</li>
<li>Fotos auf Server hochladen</li>
<li>Stammvertreter sichern</li>
</ul>
</div>
</div>
</div>
<div class="highlight-box">
<strong>Empfehlung:</strong> Vor jedem Meeting einmal kurz „Vom Server laden" klicken — so hast du immer die aktuellste Mitgliederliste &amp; Fotos, auch wenn beim Meeting kein Internet verfügbar ist.
</div>
</div>
<!-- ========== 10. UPDATE ========== -->
<div class="section" id="update">
<div class="section-header">
<div class="step-num">10</div>
<h2>Updates einspielen</h2>
</div>
<div class="info">
Wenn eine neue Version verfügbar ist, erscheint oben im Steuerungs-Fenster automatisch ein roter Hinweis-Banner.
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Update-Banner</span>
</div>
<div class="mockup-body">
<div style="background:#1a2744;color:white;padding:10px 14px;border-radius:8px;display:flex;align-items:center;gap:12px;font-size:0.85rem;">
<span>🆕 Update 1.2.0 verfügbar – Timer-Schalter im Admin-Menü</span>
<button class="mock-btn-red" style="padding:4px 12px;font-size:0.82rem;white-space:nowrap;">Jetzt laden</button>
</div>
</div>
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Auf <strong>„Jetzt laden"</strong> klicken.</p></div>
<div class="step"><p>Es öffnen sich zwei Speichern-Dialoge nacheinander: erst für <code>index.html</code>, dann wird <code>admin.html</code> automatisch heruntergeladen.</p></div>
<div class="step"><p>Im Speichern-Dialog zum BNI-Ordner navigieren und die bestehende <code>index.html</code> <strong>überschreiben</strong>.</p></div>
<div class="step"><p><code>admin.html</code> aus dem Download-Ordner ebenfalls in den BNI-Ordner verschieben und ersetzen.</p></div>
<div class="step"><p>Seite neu laden — das Update ist eingespielt.</p></div>
</div>
</div>
<div class="tip">
<strong>Hinweis:</strong> Alle gespeicherten Daten (Mitglieder, Fotos, Stammvertreter) bleiben beim Update erhalten.
</div>
</div>
<!-- ========== 11. TIMER-SCHALTER ========== -->
<div class="section" id="timer-schalter">
<div class="section-header">
<div class="step-num">11</div>
<h2>Zeitanzeige deaktivieren</h2>
</div>
<div class="info">
Manche Chapter möchten keine Zeitanzeige verwenden. Diese kann im Admin-Tab der Steuerung dauerhaft ausgeschaltet werden.
</div>
<div class="card">
<div class="steps">
<div class="step"><p>Im Steuerungs-Fenster den Tab <strong>„Admin"</strong> (rechte Seitenleiste) öffnen.</p></div>
<div class="step"><p>Unter <strong>„Einstellungen"</strong> den Haken bei <strong>„Zeitanzeige aktiv"</strong> entfernen.</p></div>
<div class="step"><p>Timer-Ring, Countdown und Zeitbadges verschwinden sofort auf allen Ansichten.</p></div>
</div>
</div>
<div class="mockup">
<div class="mockup-bar">
<div class="mockup-dot" style="background:#ff5f57;"></div>
<div class="mockup-dot" style="background:#febc2e;"></div>
<div class="mockup-dot" style="background:#28c840;"></div>
<span style="color:rgba(255,255,255,0.4);font-size:0.78rem;margin-left:8px;">Steuerung – Admin-Tab</span>
</div>
<div class="mockup-body">
<div style="background:#1e2235;border-radius:10px;padding:14px;">
<div style="color:rgba(255,255,255,0.4);font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:1px;margin-bottom:10px;">Einstellungen</div>
<label style="display:flex;align-items:center;gap:10px;cursor:pointer;font-size:0.85rem;color:white;">
<input type="checkbox" checked style="width:15px;height:15px;">
Zeitanzeige aktiv
</label>
</div>
</div>
</div>
<div class="tip">
<strong>Hinweis:</strong> Die Einstellung wird automatisch gespeichert und bleibt nach einem Neustart erhalten.
</div>
</div>
<!-- FOOTER -->
<div style="text-align:center;padding:32px 0;color:var(--muted);font-size:0.8rem;border-top:1px solid var(--border);margin-top:32px;">
BNI Vorstellungsrunde · Interne Anleitung · Bei Fragen an den Administrator wenden
</div>
</div><!-- .content -->
</body>
</html>

BIN
apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
chapters/.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,6 @@
{
"chapterName": "BNI Chapter Epsilon",
"syncUrl": "https://bni-ulm.com/epsilon/de/memberlist?chapterName=&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/epsilon",
"defaultTimer": 60
}

View File

@@ -0,0 +1,6 @@
{
"chapterName": "BNI Chapter Helios",
"syncUrl": "https://bni-konstanz.de/helios/de/memberlist?chapterName=40832&regionIds=6023&isChapterwebsite=true",
"websiteUrl": "https://bni-konstanz.de/helios",
"defaultTimer": 60
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,90 @@
{
"items": [
{
"id": "willkommen",
"label": "Willkommen",
"type": "embed",
"notes": "wie könnte euer Produkt als Weihnachtsgeschenk vermarktet werden?\n\nVertreter:\nAlexander Kaya für Sabine Welp\nSandra Stengel für Nathalie Musto\nStefan Roth für Jennifer Born\nNiko Gäßler für Thomas Stetter\nAlexander Weimer für Jens Berger\n\n\nWert: Lebenslanges Lernen - was habt ihr dieses Jahr gelernt?",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g063c60ec44c3347ad841c139671232dff"
},
{
"id": "trainingsbeitrag",
"label": "Trainingsbeitrag",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g066608ae9da024f8e87c0e705dbe6852f"
},
{
"id": "visitenkarten",
"label": "Visitenkarten",
"type": "image",
"notes": "07:15",
"imagePath": "agenda/visitenkarten.jpg"
},
{
"id": "verleihung-der-lumora",
"label": "Verleihung der Lumora",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0385ba8f3e8a8422ab051f1591bcf598d"
},
{
"id": "bericht-des-mitgliederkoordinators",
"label": "Bericht des Mitgliederkoordinators",
"type": "embed",
"notes": "07:48",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0d27561ab34c141a39f981ab6b36fbd03"
},
{
"id": "break-out",
"label": "Break Out",
"type": "embed",
"notes": "07:51",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0fbb79876ba15437abf469c098a72b789"
},
{
"id": "naechste-hauptpraesentation",
"label": "Nächste Hauptpräsentation",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0ba90bb948a91426d9bb4bbb59bb5bf9d"
},
{
"id": "marketing-ergebnisrunde-erklaerung",
"label": "Marketing- Ergebnisrunde Erklärung",
"type": "embed",
"notes": "08:12",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0848053a6eea54f18bbaaee7d33861386"
},
{
"id": "feedback-gaeste",
"label": "Feedback Gäste",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0da62ecf51cdf4d9285097db2c9b80b26"
},
{
"id": "ziehung-des-tagespreises",
"label": "Ziehung des Tagespreises",
"type": "embed",
"notes": "08:20",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0b231f4b1bdf34850a8a8baca4b2d6972"
},
{
"id": "bericht-des-schatzmeisters",
"label": "Bericht des Schatzmeisters",
"type": "embed",
"notes": "wer am Termin in Dellmensingen nicht da ist, denk daran das Geld davor an Jens zu geben / überweisen - wir bringen ihn sonst in die blöde Lage, dass er privat das Geld auslegen muss",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0249a5f37a97e419db184bef50bcbdb8e"
},
{
"id": "bericht-des-eventkoordinators",
"label": "Bericht des Eventkoordinators",
"type": "embed",
"notes": "08:23",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0ad05a18b97214634818823cd3bd3ebca"
},
{
"id": "zitat",
"label": "Zitat",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g03d4fc1ad7a8141c098addbee9dfcbffb"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -0,0 +1,7 @@
{
"chapterName": "BNI Chapter Lumora Hybrid",
"logoUrl": "BNI Logo.png",
"syncUrl": "https://bni-ulm.com/lumora/de/memberlist?chapterName=44269&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/lumora",
"defaultTimer": 60
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,41 @@
{
"items": [
{
"id": "bericht-der-schatzmeisterin",
"label": "Bericht der Schatzmeisterin",
"type": "image",
"notes": "Hier sollte was stehen",
"imagePath": "agenda/bericht-der-schatzmeisterin.jpg"
},
{
"id": "beispieldatei",
"label": "Beispieldatei",
"type": "embed",
"embedUrl": "https://workdrive.digiseed.de/writer/open/a5u6x594bfa69700846d88739fa2756082ada"
},
{
"id": "willkommen",
"label": "Willkommen",
"type": "image",
"imagePath": "agenda/willkommen.jpg"
},
{
"id": "ende-logo",
"label": "Ende - Logo",
"type": "image",
"imagePath": "agenda/ende-logo.jpg"
},
{
"id": "witz-des-tages",
"label": "Witz des Tages",
"type": "embed",
"embedUrl": "https://digiseed.de"
},
{
"id": "schatzmeister-offline",
"label": "Schatzmeister Offline",
"type": "image",
"imagePath": "agenda/schatzmeister-offline.jpg"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -3,5 +3,5 @@
"syncUrl": "https://bni-ulm.com/sonne/de/memberlist?chapterName=6035&regionIds=43671&isChapterwebsite=true",
"websiteUrl": "https://bni-ulm.de/sonne",
"defaultTimer": 60,
"memberCoordinatorReport": true
"memberCoordinatorReport": false
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

10
favicon.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<g stroke="#F5D822" stroke-width="3.5" stroke-linecap="round">
<path d="M32 2v8M32 54v8M2 32h8M54 32h8M10.8 10.8l5.7 5.7M47.5 47.5l5.7 5.7M10.8 53.2l5.7-5.7M47.5 16.5l5.7-5.7"/>
</g>
<circle cx="32" cy="32" r="18" fill="#F5D822" stroke="#C70607" stroke-width="3"/>
<g fill="none" stroke="#C70607" stroke-width="2.6" stroke-linecap="round">
<path d="M23 29.5q3-4 6 0M35 29.5q3-4 6 0"/>
<path d="M23.5 36q8.5 8 17 0"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 512 B

1057
index.html

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
{
"version": "1.5",
"notes": "Zeitanzeige oben ergänzt. letzte 15 Sekunden gelber Hintergrund, letzte 5 Sekunden roter Hintergrund"
"version": "2.2",
"notes": "Neue, verschlüsselte Serveradresse. Bitte jetzt aktualisieren – die alte Version kann ab 1. November keine Daten mehr laden oder speichern."
}