Compare commits

...

187 Commits

Author SHA1 Message Date
b29b7317bc Stammvertreter aktualisiert 2026-09-25 03:02:27 +00:00
b62582b796 Stammvertreter aktualisiert 2026-09-25 02:59:08 +00:00
33c6d1e896 Stammvertreter aktualisiert 2026-09-18 04:48:12 +00:00
82e690c447 Mitglieder aktualisiert 16.9.2026 2026-09-16 20:10:05 +00:00
b178523220 Mitglieder aktualisiert 14.9.2026 2026-09-14 14:17:50 +00:00
3fcbd91fd6 Mitglieder aktualisiert 14.9.2026 2026-09-14 14:00:57 +00:00
1ab2e56352 Stammvertreter aktualisiert 2026-09-14 11:39:03 +00:00
1a0b14fac4 Stammvertreter aktualisiert 2026-09-13 19:33:44 +00:00
ae65a78d30 Stammvertreter aktualisiert 2026-09-11 03:31:37 +00:00
b81f677ff9 Agenda aktualisiert 11.9.2026 2026-09-11 03:25:57 +00:00
df2a1c1ecf Agenda-Bild aktualisiert 11.9.2026 2026-09-11 03:25:55 +00:00
e2c257e425 Agenda-Bild aktualisiert 11.9.2026 2026-09-11 03:25:31 +00:00
5bae6b7756 Stammvertreter aktualisiert 2026-09-11 03:17:04 +00:00
2285cb9356 Agenda aktualisiert 11.9.2026 2026-09-11 03:16:36 +00:00
217d6b4c48 Agenda-Bild aktualisiert 11.9.2026 2026-09-11 03:16:26 +00:00
8f98386736 Mitglieder aktualisiert 11.9.2026 2026-09-11 03:16:07 +00:00
e09de044da Mitglieder aktualisiert 10.9.2026 2026-09-10 07:45:56 +00:00
80730ebbb8 Mitglieder aktualisiert 10.9.2026 2026-09-10 04:51:31 +00:00
e55e1b60bf Mitglieder aktualisiert 9.9.2026 2026-09-09 16:15:41 +00:00
036719175c Mitglieder aktualisiert 9.9.2026 2026-09-09 16:14:09 +00:00
2c6454c30f Mitglieder aktualisiert 9.9.2026 2026-09-09 16:12:44 +00:00
93d20afd34 Mitglieder aktualisiert 9.9.2026 2026-09-09 14:49:28 +00:00
62d4f5c6e2 Mitglieder aktualisiert 8.9.2026 2026-09-08 08:25:59 +00:00
a902572772 Mitglieder aktualisiert 8.9.2026 2026-09-08 08:15:11 +00:00
e5e42520cb Mitglieder aktualisiert 8.9.2026 2026-09-08 08:12:11 +00:00
4d161738aa Mitglieder aktualisiert 7.9.2026 2026-09-07 15:36:33 +00:00
64d79924e9 Mitglieder aktualisiert 7.9.2026 2026-09-07 15:34:04 +00:00
0767115801 Stammvertreter aktualisiert 2026-09-07 15:18:34 +00:00
33425de39e Agenda aktualisiert 3.9.2026 2026-09-03 21:30:59 +00:00
30b72fc298 Agenda-Bild aktualisiert 3.9.2026 2026-09-03 21:30:57 +00:00
bfefea0699 Agenda-Bild aktualisiert 3.9.2026 2026-09-03 21:30:36 +00:00
d84577c44d Agenda-Bild aktualisiert 3.9.2026 2026-09-03 21:30:13 +00:00
26d2f79629 Agenda aktualisiert 3.9.2026 2026-09-03 21:24:08 +00:00
cddf087d68 Agenda-Bild aktualisiert 3.9.2026 2026-09-03 21:13:10 +00:00
c6cda48d11 Dateien nach "chapters/helios-2cbws6" hochladen 2026-09-03 19:28:16 +00:00
373e4700de chapters/helios-2cbws6/config.json aktualisiert 2026-09-03 19:27:27 +00:00
ee1b657c13 Stammvertreter aktualisiert 2026-09-03 16:46:22 +00:00
74e3a583eb Stammvertreter aktualisiert 2026-09-03 16:46:11 +00:00
339dc1d187 Stammvertreter aktualisiert 2026-09-03 15:19:10 +00:00
73a45cfcdc Stammvertreter aktualisiert 2026-09-03 08:26:37 +00:00
0ac2345e3d Stammvertreter aktualisiert 2026-09-03 08:24:38 +00:00
Christian Heubaum
8bf74cc992 data-Branch aufgeräumt: nur noch chapters/
Der Branch enthielt bislang eine (mittlerweile veraltete) Kopie der
kompletten App, weil er ursprünglich als Abzweigung von main erstellt
wurde. Da hier nur noch chapters/*-Daten gelesen/geschrieben werden,
sind die App-Dateien überflüssig.
2026-08-31 17:32:17 +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
Christian Heubaum
24808e35f6 Vertretungs-Foto: extras (photoB64 + offlinePhotoB64) berücksichtigen
Foto-Auflösung für Vertretungen prüft nun auch manuelle Uploads
und synchronisierte Server-Fotos aus extras, nicht nur sub.photoUrl
und Mitglieder-Match. Ines-Benedict-Fall (Foto nur als Base64 in
extras) wird jetzt korrekt aufgelöst.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 05:42:14 +02:00
Christian Heubaum
68adca35e5 Version 1.5: Farbwarnung + Glockenton + große Header-Zeitanzeige
- Main-Card wird gelb ab 15s Restzeit, BNI-Rot ab 5s
- Timer-Text kontrastabhängig (schwarz auf gelb, weiß auf rot)
- Fachgebiets-Pille immer in weißer Schrift lesbar
- Glockenton bei Ablauf (bell.mp3 als data-URI in HTML eingebettet)
- Große Sekundenanzeige rechts oben im Display-Header

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 05:42:14 +02:00
491adbaef8 Version 1.5 2026-07-02 22:29:48 +00:00
7fde6df4c1 Mitglieder aktualisiert 2.7.2026 2026-07-02 21:31:09 +00:00
3396ed062e Version 1.3. 2026-05-24 11:06:48 +00:00
50c603e251 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-24 11:05:39 +00:00
66f45fb80d Stammvertreter aktualisiert 2026-05-24 10:59:47 +00:00
Christian Heubaum
0797cc8984 Mitgliederkoordinator-Bericht + Anzeige-Verbesserungen
Anzeige (index.html):
- Mitgliederkoordinator-Bericht-Modus: 4x2 Kacheln mit
  Jahreszielen, Sparkline-Trend und Monatswerte-Reihe pro
  Kennzahl; jüngste Jahres-JSON-Datei wird automatisch gewählt
- QR-Code wird auf Abwesenheits- und Bericht-Anzeige
  ausgeblendet (war zuvor vom letzten Mitglied übrig)
- Abwesenheits-Namen skalieren dynamisch (2.2rem bei wenigen
  Einträgen bis 0.75rem bei vollen Listen)
- Fotos für Besucher und Vertreter in Runde 2: getDisplayData
  fällt für Besucher zusätzlich auf loadExtras() zurück, damit
  Stammvertreter-Fotos (photoB64) erscheinen; Besucher-Queue
  nutzt dieselbe Fallback-Kette

Admin (admin.html):
- Karte "Mitgliederkoordinator-Bericht": Jahresziele und
  Monatswerte je Chapter und BNI-Jahr erfassen
- _coordStatus()-Helfer für einheitliches Speicher-Feedback;
  explizite Fehlermeldungen bei fehlendem Chapter/Monat,
  HTTP-Detail bei Server-Fehlern, fehlendes Admin-Token auch
  im Statusfeld
- Sort-Crash bei alten entries-Datensätzen ohne month-Feld
  behoben (null-sicher) + Bereinigung beim Laden

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 18:07:38 +02:00
8aed9c646c Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-21 21:46:55 +00:00
bd3182aae5 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-05-21 21:38:27 +00:00
dba4ba35ea Update Koordinatoren-Bericht 2025-2026 für sonne-7x4m9k 2026-05-21 20:33:12 +00:00
6717eea965 Update Koordinatoren-Bericht 2025-2026 für sonne-7x4m9k 2026-05-21 20:31:51 +00:00
e7e68417d4 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:29:23 +00:00
a01f0ef1ad Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:27:04 +00:00
859d7d94fa Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 13:25:57 +00:00
ffa951ea53 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:56:07 +00:00
2fe2373665 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:55:17 +00:00
3fd457d413 Update Koordinatoren-Bericht 2026-2027 für sonne-7x4m9k 2026-04-27 11:54:34 +00:00
3c5959bdf7 Update Koordinatoren-Bericht für sonne-7x4m9k 2026-04-27 11:34:45 +00:00
cd51dfa9fd Koordinatoren-Bericht aktiviert für sonne-7x4m9k 2026-04-27 11:32:03 +00:00
2f8caf80bc Stammvertreter aktualisiert 2026-04-27 10:24:07 +00:00
9a97b6d7a4 Mitglieder aktualisiert 27.4.2026 2026-04-27 10:22:21 +00:00
a632e9b057 Mitglieder aktualisiert 27.4.2026 2026-04-27 10:20:30 +00:00
Christian Heubaum
eff28fcc79 Aufräumen: .DS_Store aus Repo entfernt und ignoriert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:39:57 +02:00
Christian Heubaum
f7b819eed4 Aufräumen: Push-Skript aus Repo entfernt, .gitignore ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:39:19 +02:00
Christian Heubaum
331cb08609 Aufräumen: Letztes ungenutztes Profilbild entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:38:41 +02:00
Christian Heubaum
a33a68dacb Aufräumen: Kallisto-Kopie-Ordner entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:36:58 +02:00
Christian Heubaum
bf4c099b62 Aufräumen: Ungenutzte Profilbilder entfernt, Anleitung aktualisiert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:36:19 +02:00
Christian Heubaum
c2946a07f2 Fix: admin.html als Auto-Download, index.html per Speichern-Dialog (v1.2.4)
Zweiter showSaveFilePicker wurde vom Browser blockiert (kein User-Gesture mehr).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:12:34 +02:00
Christian Heubaum
875ef0d42f Fix: admin.html wird beim Update automatisch mitgespeichert (v1.2.3)
Blob-URL-Ansatz rückgängig gemacht (localStorage-Problem), stattdessen
beide Dateien gemeinsam per showSaveFilePicker aktualisieren.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:10:12 +02:00
Christian Heubaum
6dcfcfa4a6 Feature: Admin-Panel wird immer live vom Server geladen (v1.2.2)
Kein manuelles Update von admin.html mehr nötig – immer die aktuelle Version.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 21:03:51 +02:00
Christian Heubaum
f8c405f3c4 Fix: Chapter-Namen in Admin immer frisch vom Server laden
cache: no-store verhindert dass veraltete Chapter-Namen angezeigt werden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:57:42 +02:00
Christian Heubaum
1cf92503ed Fix: Schreibfehler Callisto → Kallisto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:46:36 +02:00
Christian Heubaum
fbb2d471de Fix: Update-Dialog mit showSaveFilePicker (v1.2.1)
Direktes Speichern in den richtigen Ordner statt Download-Ordner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:42:07 +02:00
Christian Heubaum
f97ab2d69f Release v1.2.0: Timer-Schalter im Admin-Menü
Chapter können die Zeitanzeige deaktivieren. Update-Funktion ersetzt Datei direkt im richtigen Ordner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:39:15 +02:00
Christian Heubaum
69824b610c Feature: Timer-Schalter im Admin-Menü (v1.1.2)
Timer kann chapter-weit deaktiviert werden – Display-Ring, Countdown und Zeitbadges werden ausgeblendet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 20:35:58 +02:00
Christian Heubaum
3c37c4ad61 Fix: Abstand unter Member-Card erhöht damit DigiSeed-Logo sichtbar bleibt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 19:57:59 +02:00
Christian Heubaum
ca2e970389 Fix: DigiSeed-Logo aktualisiert und Sichtbarkeit verbessert
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 19:35:18 +02:00
72c7c94ce2 Mitglieder aktualisiert 20.4.2026 2026-04-20 14:42:02 +00:00
21f5e28ca3 Mitglieder aktualisiert 20.4.2026 2026-04-20 14:34:23 +00:00
2254185cfa Mitglieder aktualisiert 20.4.2026 2026-04-20 14:34:04 +00:00
Christian Heubaum
4e77ae1b61 Merge: Remote-Änderungen mit lokalen Chapters zusammenführen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:26:49 +02:00
Christian Heubaum
77ce58243f Feature: Zwei neue Chapter hinzugefügt (Kallisto, Lumora)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 16:24:14 +02:00
Christian Heubaum
3f7e24be32 Feature: Foto-Upload für Mitglieder im Admin-Panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:15:41 +02:00
Christian Heubaum
661d9b117e Debug: Upload-Fehler mit konkreter Fehlermeldung anzeigen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 13:34:08 +02:00
Christian Heubaum
cefa4ca38f Fix: CORS-Preflight bei Auth-Uploads - Token als URL-Parameter statt Header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 13:23:31 +02:00
Christian Heubaum
1138310639 Gitea-Repo umbenannt: BNI-Sonne → BNI-Vorstellungsrunde
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 17:04:57 +02:00
Christian Heubaum
b9fee3ae7c Feature: Stammvertreter-Datenbank + admin.html + Multi-File-Architektur
- Neues admin.html: eigenständiges Admin-Panel mit Tabs für Stammvertreter, Mitglieder, Superadmin, Einstellungen
- Stammvertreter-Feature in index.html: Profilvorrat für schnelle Vertreterauswahl, localStorage-basiert, chapter-spezifisch
- Stammvertreter-Picker im Vertretungs-Modal: Felder werden automatisch befüllt
- Superadmin: alle Chapter einsehen, version.json für Rollout pushen
- Import aus anderen Chaptern: Mitglied aus fremdem Chapter als Stammvertreter übernehmen
- Admin-Tab in index.html vereinfacht (Link zu admin.html)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:56:00 +02:00
Christian Heubaum
5deda366f3 Checkpoint vor Multi-File-Refactoring + Stammvertreter-Feature
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 16:46:07 +02:00
Christian Heubaum
e1c878e6ae Admin: Mitglieder inkl. Fotos auf Gitea hochladen
- buildMembersJson() bettet Offline-Fotos (Base64) aus extras ein
- uploadMembersToGitea() lädt members.json via API hoch
- Admin-Panel: Token-Eingabe + Upload-Button + Status
- fetchChapterMembersFromGitea speichert photoData in extras

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:46:24 +02:00
Christian Heubaum
0d7b9b2d6a Multi-Chapter-Support: Lizenzcode, Gitea-Sync, Auto-Update
- Aktivierungsscreen mit Lizenzcode-Eingabe
- Hintergrund-Lizenzprüfung bei jedem Start
- Admin-Tab mit Mitglieder-Sync und Update-Check
- version.json + chapters/sonne-7x4m9k/ Struktur

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:26:01 +02:00
Christian Heubaum
85dfc53b13 Checkpoint: Profildaten, Profilbilder und index.html Stand April 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 13:12:53 +02:00
Christian Heubaum
ab0397dbe4 Checkpoint: Responsive Display-Skalierung + QR oben rechts + Nächster Sprecher größer 2026-04-16 12:20:19 +02:00
38 changed files with 1857 additions and 5975 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

3
Push
View File

@@ -1,3 +0,0 @@
neues Push, nach Änderungen
cd "/Users/christianheubaum/Documents/Claude/BNI Sonne"
git add -u && git commit -m "Update" && git push

View File

@@ -1,839 +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 Admin-Panel</title>
<style>
:root {
--red: #cc0000; --red-dark: #990000;
--navy: #1a2744; --bg: #f0f2f5;
--card: #ffffff; --text: #1a1a2e;
--muted: #6b7280; --border: #e5e7eb;
--success: #16a34a; --warning: #d97706;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
.header { background:var(--navy); color:white; padding:14px 24px; display:flex; align-items:center; gap:16px; }
.header img { height:40px; border-radius:6px; object-fit:contain; }
.header-title { font-size:1.1rem; font-weight:700; }
.header-chapter { font-size:0.82rem; color:rgba(255,255,255,0.6); }
.header-back { margin-left:auto; color:rgba(255,255,255,0.7); text-decoration:none; font-size:0.85rem; }
.header-back:hover { color:white; }
.tabs { background:white; border-bottom:1px solid var(--border); display:flex; padding:0 24px; }
.tab { padding:12px 20px; font-size:0.85rem; font-weight:600; color:var(--muted); cursor:pointer; border-bottom:2px solid transparent; }
.tab.active { color:var(--red); border-bottom-color:var(--red); }
.content { padding:24px; max-width:900px; }
.card { background:white; border-radius:12px; border:1px solid var(--border); padding:20px; margin-bottom:16px; }
.card-title { font-weight:700; font-size:0.95rem; margin-bottom:14px; }
.card-subtitle { font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; }
.btn { padding:8px 16px; border-radius:8px; font-size:0.85rem; font-weight:600; cursor:pointer; border:none; display:inline-flex; align-items:center; gap:6px; }
.btn-primary { background:var(--red); color:white; }
.btn-primary:hover { background:var(--red-dark); }
.btn-secondary { background:#f3f4f6; color:var(--text); border:1px solid var(--border); }
.btn-secondary:hover { background:#e5e7eb; }
.btn-ghost { background:transparent; color:var(--muted); border:1px solid var(--border); }
.btn-danger { background:#fee2e2; color:#dc2626; }
.btn-sm { padding:5px 10px; font-size:0.78rem; }
.btn:disabled { opacity:0.5; cursor:not-allowed; }
.w-full { width:100%; justify-content:center; }
label { display:block; font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px; margin-top:10px; }
input[type=text], input[type=password], select, textarea {
width:100%; padding:9px 12px; border:1px solid var(--border); border-radius:8px; font-size:0.88rem; outline:none;
}
input:focus, select:focus { border-color:var(--red); }
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.sv-item { display:flex; align-items:center; gap:10px; padding:10px 0; border-bottom:1px solid var(--border); }
.sv-photo { width:44px; height:44px; border-radius:50%; object-fit:cover; flex-shrink:0; }
.sv-initials { width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-size:0.8rem; font-weight:700; flex-shrink:0; }
.sv-info { flex:1; min-width:0; }
.sv-name { font-weight:600; font-size:0.9rem; }
.sv-meta { font-size:0.78rem; color:var(--muted); }
.member-item { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--border); }
.chapter-item { padding:10px 14px; background:#f8f9fa; border:1px solid var(--border); border-radius:8px; margin-bottom:8px; display:flex; align-items:center; gap:12px; }
.status-msg { font-size:0.83rem; min-height:20px; margin-top:6px; }
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal { background:white; border-radius:16px; padding:24px; width:min(520px,92vw); max-height:90vh; overflow-y:auto; }
.modal-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; }
.modal-title { font-weight:700; font-size:1.05rem; }
.modal-close { background:none; border:none; font-size:1.3rem; cursor:pointer; color:var(--muted); }
.modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:16px; padding-top:12px; border-top:1px solid var(--border); }
.photo-thumb { width:64px; height:64px; border-radius:50%; object-fit:cover; border:2px solid var(--border); }
.photo-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:8px; }
.btn-file-label { display:inline-flex; align-items:center; gap:6px; padding:5px 10px; background:#f3f4f6; border:1px solid var(--border); border-radius:8px; font-size:0.78rem; font-weight:600; cursor:pointer; }
.tab-content { display:none; }
.tab-content.active { display:block; }
.import-chapter-list { max-height:280px; overflow-y:auto; border:1px solid var(--border); border-radius:8px; }
.import-member-row { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); cursor:pointer; }
.import-member-row:hover { background:#f8f9fa; }
.import-member-row:last-child { border-bottom:none; }
</style>
</head>
<body>
<div class="header">
<img id="admin-logo" src="logo.jpeg" alt="BNI">
<div>
<div class="header-title">BNI Admin-Panel</div>
<div class="header-chapter" id="admin-chapter-name">—</div>
</div>
<a href="index.html" class="header-back">← Zurück zur Steuerung</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div>
<div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div>
<div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div>
<div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div>
</div>
<div class="content">
<!-- ============================================================ STAMMVERTRETER -->
<div id="tab-stammvertreter" class="tab-content active">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div class="card-title" style="margin:0;">Stammvertreter-Vorrat</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="openImportFromChapter()">⬇ Aus anderem Chapter</button>
<button class="btn btn-primary btn-sm" onclick="openAddSvAdmin()">+ Hinzufügen</button>
</div>
</div>
<div id="admin-sv-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
<div class="card">
<div class="card-title">Gitea-Synchronisation</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Stammvertreter-Liste auf dem Server sichern, damit sie nach einem Browser-Reset wiederherstellbar ist.</p>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="downloadSvFromGitea()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadSvToGitea()">📤 Auf Server sichern</button>
</div>
<div class="status-msg" id="sv-sync-status"></div>
</div>
</div>
<!-- ============================================================ MITGLIEDER -->
<div id="tab-mitglieder" 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;">Mitglieder</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchMembersAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadMembersAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<div class="status-msg" id="members-status"></div>
<div id="admin-members-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
</div>
<!-- ============================================================ SUPERADMIN -->
<div id="tab-superadmin" class="tab-content">
<div class="card">
<div class="card-title">Alle Chapter</div>
<div id="all-chapters-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
<div class="card">
<div class="card-title">Update-Rollout</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Neue Versionsnummer in version.json pushen → alle Nutzer aller Chapter sehen das Update-Banner beim nächsten Start.</p>
<label>Aktuelle Version auf Gitea</label>
<div id="current-version-display" style="font-size:0.88rem;padding:8px 0;color:var(--muted);">—</div>
<label>Neue Version</label>
<div style="display:flex;gap:8px;">
<input type="text" id="new-version-input" placeholder="z.B. 1.2.0" style="flex:1;">
<input type="text" id="new-version-notes" placeholder="Hinweis (optional)" style="flex:2;">
<button class="btn btn-primary btn-sm" onclick="pushVersionUpdate()">Pushen</button>
</div>
<div class="status-msg" id="version-status"></div>
</div>
</div>
<!-- ============================================================ EINSTELLUNGEN -->
<div id="tab-einstellungen" class="tab-content">
<div class="card">
<div class="card-title">Admin-Token (Gitea API)</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:10px;">Wird für alle Uploads auf den Gitea-Server benötigt. Wird nur lokal im Browser gespeichert.</p>
<div style="display:flex;gap:8px;">
<input type="password" id="settings-token" placeholder="Gitea API Token" style="flex:1;">
<button class="btn btn-primary btn-sm" onclick="saveSettingsToken()">Speichern</button>
</div>
<div class="status-msg" id="token-status"></div>
</div>
<div class="card">
<div class="card-title">Chapter-Informationen</div>
<div id="chapter-info-display" style="font-size:0.88rem;color:var(--muted);">Lade…</div>
</div>
</div>
</div><!-- .content -->
<!-- ============================================================ MODALS -->
<!-- SV Add/Edit Modal -->
<div class="modal-overlay" id="modal-sv-admin">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="sv-admin-modal-title">Stammvertreter hinzufügen</div>
<button class="modal-close" onclick="closeAdminModal('modal-sv-admin')">✕</button>
</div>
<input type="hidden" id="sv-admin-id">
<label>Name *</label>
<input type="text" id="sv-admin-name" placeholder="Vor- und Nachname">
<div class="form-row">
<div><label>Firma</label><input type="text" id="sv-admin-company" placeholder="Firma GmbH"></div>
<div><label>Fachgebiet</label><input type="text" id="sv-admin-specialty" placeholder="Fachgebiet"></div>
</div>
<label>Foto</label>
<div style="display:flex;align-items:center;gap:12px;margin-top:8px;">
<img id="sv-admin-photo-thumb" class="photo-thumb" src="" alt="" style="display:none;">
<div class="photo-actions">
<label class="btn-file-label">📁 Datei wählen<input type="file" id="sv-admin-photo-file" accept="image/*" style="display:none" onchange="handleSvAdminPhotoFile(this)"></label>
<button class="btn btn-danger btn-sm" id="sv-admin-remove-photo" onclick="removeSvAdminPhoto()" style="display:none;">✕ Entfernen</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="sv-admin-delete-btn" onclick="deleteSvAdmin()" style="display:none;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-sv-admin')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveSvAdminModal()">Speichern</button>
</div>
</div>
</div>
<!-- Member Photo Modal -->
<div class="modal-overlay" id="modal-member-photo">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="member-photo-modal-title">Foto bearbeiten</div>
<button class="modal-close" onclick="closeAdminModal('modal-member-photo')">✕</button>
</div>
<input type="hidden" id="member-photo-name">
<div style="display:flex;align-items:center;gap:16px;margin:16px 0;">
<img id="member-photo-thumb" class="photo-thumb" style="width:80px;height:80px;display:none;" src="" alt="">
<div id="member-photo-initials" style="width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:1.4rem;font-weight:700;flex-shrink:0;"></div>
<div class="photo-actions" style="flex-direction:column;align-items:flex-start;">
<label class="btn-file-label">📁 Foto wählen<input type="file" id="member-photo-file" accept="image/*" style="display:none" onchange="handleMemberPhotoFile(this)"></label>
<button class="btn btn-danger btn-sm" id="member-photo-remove" onclick="removeMemberPhoto()" style="display:none;margin-top:6px;">✕ Foto entfernen</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeAdminModal('modal-member-photo')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveMemberPhoto()">Speichern</button>
</div>
</div>
</div>
<!-- Import from Chapter Modal -->
<div class="modal-overlay" id="modal-import-chapter">
<div class="modal">
<div class="modal-header">
<div class="modal-title">Aus anderem Chapter importieren</div>
<button class="modal-close" onclick="closeAdminModal('modal-import-chapter')">✕</button>
</div>
<div id="import-chapter-step1">
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Chapter auswählen, aus dem du ein Profil übernehmen möchtest:</p>
<div id="import-chapter-select-list"><div style="color:var(--muted);">Lade Chapter…</div></div>
</div>
<div id="import-chapter-step2" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="showImportStep1()" style="margin-bottom:12px;">← Zurück</button>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:8px;">Mitglied auswählen:</p>
<div id="import-member-list" class="import-chapter-list"></div>
</div>
<div class="status-msg" id="import-status"></div>
</div>
</div>
<script>
// ============================================================
// 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 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)}`; }
// ============================================================
// CHAPTER CONFIG
// ============================================================
function loadChapterConfig() {
try { return JSON.parse(localStorage.getItem(CHAPTER_CONFIG_KEY) || 'null'); } catch(e) { return null; }
}
const activeChapter = loadChapterConfig();
// ============================================================
// STORAGE KEYS
// ============================================================
function storageKey() { return activeChapter ? `bni-chapter-${activeChapter.code}-v2` : 'bni-chapter-default-v2'; }
function extrasKey(name) { return (name||'').toLowerCase().trim(); }
function extrasStorageKey() { return activeChapter ? `bni-extras-${activeChapter.code}` : 'bni-profile-extras'; }
function svStorageKey() { return activeChapter ? `bni-stammvertreter-${activeChapter.code}` : 'bni-stammvertreter'; }
function loadState() {
try { return JSON.parse(localStorage.getItem(storageKey()) || 'null') || defaultState(); } catch(e) { return defaultState(); }
}
function saveState(s) { localStorage.setItem(storageKey(), JSON.stringify(s)); }
function defaultState() { return { members:[], meetingOrder:[], currentIndex:-1, substitutes:{}, absentIds:[], medicalIds:[], visitors:[], visitorsOrder:[], visitorsCurrentIndex:-1, mode:'members', defaultTimer:60, memberTimers:{} }; }
function loadExtras() { try { return JSON.parse(localStorage.getItem(extrasStorageKey())||'{}'); } catch(e) { return {}; } }
function saveExtras(e) { localStorage.setItem(extrasStorageKey(), JSON.stringify(e)); }
function loadStammvertreter() { try { return JSON.parse(localStorage.getItem(svStorageKey())||'[]'); } catch(e) { return []; } }
function saveStammvertreter(list) { localStorage.setItem(svStorageKey(), JSON.stringify(list)); }
function getAdminToken() { return localStorage.getItem('bni-gitea-admin-token'); }
// ============================================================
// UTILS
// ============================================================
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function getInitials(n) { if(!n)return'?'; return n.trim().split(/\s+/).map(w=>w[0]).join('').toUpperCase().slice(0,2); }
function getColor(n) { const c=['#1a2744','#cc0000','#0f3460','#1e3a5f','#4a1942']; let h=0; for(let ch of(n||''))h=(h*31+ch.charCodeAt(0))&0xffffffff; return c[Math.abs(h)%c.length]; }
function resizePhotoToBase64(file, maxPx=400) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
let w=img.width, h=img.height;
if(w>maxPx||h>maxPx){ if(w>h){h=Math.round(h*maxPx/w);w=maxPx;}else{w=Math.round(w*maxPx/h);h=maxPx;} }
const canvas=document.createElement('canvas'); canvas.width=w; canvas.height=h;
canvas.getContext('2d').drawImage(img,0,0,w,h);
resolve(canvas.toDataURL('image/jpeg',0.82));
};
img.onerror=reject; img.src=e.target.result;
};
reader.onerror=reject; reader.readAsDataURL(file);
});
}
function avatarHtml(name, photoB64, photoUrl, size=44) {
const src = photoB64 || photoUrl || '';
if (src) return `<img src="${escHtml(src)}" style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;flex-shrink:0;" onerror="this.replaceWith(makeInitialsEl('${escHtml(name)}',${size}))">`;
const el = document.createElement('div');
el.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;background:${getColor(name)};display:flex;align-items:center;justify-content:center;color:white;font-size:${Math.round(size*0.32)}px;font-weight:700;flex-shrink:0;`;
el.textContent = getInitials(name);
return el.outerHTML;
}
function openAdminModal(id) { document.getElementById(id).classList.add('open'); }
function closeAdminModal(id) { document.getElementById(id).classList.remove('open'); }
// ============================================================
// TABS
// ============================================================
function switchAdminTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelector(`[onclick="switchAdminTab('${tab}')"]`).classList.add('active');
document.getElementById(`tab-${tab}`).classList.add('active');
if (tab === 'stammvertreter') renderSvList();
if (tab === 'mitglieder') renderMembersList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings();
}
// ============================================================
// STAMMVERTRETER
// ============================================================
let _svAdminPhotoB64 = null;
function renderSvList() {
const list = loadStammvertreter();
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>`;
return;
}
container.innerHTML = '';
list.forEach(sv => {
const extras = loadExtras();
const photoB64 = sv.photoB64 || (extras[extrasKey(sv.name)] && extras[extrasKey(sv.name)].photoB64) || null;
const div = document.createElement('div');
div.className = 'sv-item';
div.innerHTML = `${avatarHtml(sv.name, photoB64, null, 44)}
<div class="sv-info">
<div class="sv-name">${escHtml(sv.name)}</div>
<div class="sv-meta">${escHtml(sv.company||'')}${sv.specialty?' · '+escHtml(sv.specialty):''}</div>
</div>
<button class="btn btn-ghost btn-sm" onclick="openEditSvAdmin('${sv.id}')">✏ Bearbeiten</button>`;
container.appendChild(div);
});
}
function openAddSvAdmin() {
document.getElementById('sv-admin-id').value = '';
document.getElementById('sv-admin-modal-title').textContent = 'Stammvertreter hinzufügen';
document.getElementById('sv-admin-name').value = '';
document.getElementById('sv-admin-company').value = '';
document.getElementById('sv-admin-specialty').value = '';
document.getElementById('sv-admin-delete-btn').style.display = 'none';
_svAdminPhotoB64 = null;
updateSvAdminPhotoUI(null);
openAdminModal('modal-sv-admin');
}
function openEditSvAdmin(svId) {
const sv = loadStammvertreter().find(s => s.id === svId);
if (!sv) return;
document.getElementById('sv-admin-id').value = sv.id;
document.getElementById('sv-admin-modal-title').textContent = 'Stammvertreter bearbeiten';
document.getElementById('sv-admin-name').value = sv.name;
document.getElementById('sv-admin-company').value = sv.company || '';
document.getElementById('sv-admin-specialty').value = sv.specialty || '';
document.getElementById('sv-admin-delete-btn').style.display = '';
_svAdminPhotoB64 = sv.photoB64 || null;
updateSvAdminPhotoUI(_svAdminPhotoB64);
openAdminModal('modal-sv-admin');
}
function updateSvAdminPhotoUI(b64) {
_svAdminPhotoB64 = b64;
const thumb = document.getElementById('sv-admin-photo-thumb');
const btn = document.getElementById('sv-admin-remove-photo');
if (b64) { thumb.src = b64; thumb.style.display = 'block'; btn.style.display = 'inline-flex'; }
else { thumb.src = ''; thumb.style.display = 'none'; btn.style.display = 'none'; }
}
function handleSvAdminPhotoFile(input) {
const file = input.files && input.files[0];
if (!file) return;
resizePhotoToBase64(file).then(b64 => updateSvAdminPhotoUI(b64)).catch(e => alert(e.message));
input.value = '';
}
function removeSvAdminPhoto() { updateSvAdminPhotoUI(null); }
function saveSvAdminModal() {
const name = document.getElementById('sv-admin-name').value.trim();
if (!name) { alert('Bitte Namen eingeben.'); return; }
const list = loadStammvertreter();
const existingId = document.getElementById('sv-admin-id').value;
const sv = {
id: existingId || 'sv-' + Date.now(),
name,
company: document.getElementById('sv-admin-company').value.trim(),
specialty: document.getElementById('sv-admin-specialty').value.trim(),
photoB64: _svAdminPhotoB64 || null,
};
if (existingId) {
const idx = list.findIndex(s => s.id === existingId);
if (idx >= 0) list[idx] = sv; else list.push(sv);
} else {
list.push(sv);
}
saveStammvertreter(list);
closeAdminModal('modal-sv-admin');
renderSvList();
}
function deleteSvAdmin() {
const svId = document.getElementById('sv-admin-id').value;
if (!confirm('Stammvertreter löschen?')) return;
saveStammvertreter(loadStammvertreter().filter(s => s.id !== svId));
closeAdminModal('modal-sv-admin');
renderSvList();
}
// ============================================================
// IMPORT FROM OTHER CHAPTER
// ============================================================
async function openImportFromChapter() {
document.getElementById('import-status').textContent = '';
showImportStep1();
openAdminModal('modal-import-chapter');
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 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; }
listEl.innerHTML = '';
for (const ch of chapters) {
if (activeChapter && ch.name === activeChapter.code) continue;
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`));
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const btn = document.createElement('div');
btn.className = 'chapter-item';
btn.style.cursor = 'pointer';
btn.innerHTML = `<div><div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div><div style="font-size:0.78rem;color:var(--muted);">${ch.name}</div></div><span style="margin-left:auto;color:var(--red);font-size:0.85rem;">Mitglieder laden →</span>`;
btn.onclick = () => loadChapterMembersForImport(ch.name, cfg.chapterName || ch.name);
listEl.appendChild(btn);
} catch(e) { /* skip */ }
}
} catch(e) {
listEl.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden der Chapter.</div>';
}
}
function showImportStep1() {
document.getElementById('import-chapter-step1').style.display = '';
document.getElementById('import-chapter-step2').style.display = 'none';
}
async function loadChapterMembersForImport(code, chapterName) {
document.getElementById('import-chapter-step1').style.display = 'none';
document.getElementById('import-chapter-step2').style.display = '';
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`));
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 || [];
if (!members.length) { listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Keine Mitglieder vorhanden.</div>'; return; }
listEl.innerHTML = '';
members.forEach(m => {
const photoB64 = m.photoData || null;
const row = document.createElement('div');
row.className = 'import-member-row';
row.innerHTML = `${avatarHtml(m.name, photoB64, m.photoUrl, 36)}
<div>
<div style="font-weight:600;font-size:0.88rem;">${escHtml(m.name)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${escHtml(m.company||'')}${m.specialty?' · '+escHtml(m.specialty):''}</div>
</div>
<span style="margin-left:auto;font-size:0.75rem;color:var(--muted);">${escHtml(chapterName)}</span>`;
row.onclick = () => importMemberAsStammvertreter(m, chapterName);
listEl.appendChild(row);
});
} catch(e) {
listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Fehler: ' + e.message + '</div>';
}
}
function importMemberAsStammvertreter(member, chapterName) {
const existing = loadStammvertreter();
if (existing.find(s => s.name.toLowerCase() === member.name.toLowerCase())) {
if (!confirm(`"${member.name}" ist bereits in deiner Liste. Trotzdem importieren?`)) return;
}
const sv = {
id: 'sv-' + Date.now(),
name: member.name,
company: member.company || '',
specialty: member.specialty || '',
photoB64: member.photoData || null,
fromChapter: chapterName,
};
existing.push(sv);
saveStammvertreter(existing);
document.getElementById('import-status').textContent = `✓ "${member.name}" übernommen.`;
document.getElementById('import-status').style.color = 'var(--success)';
renderSvList();
}
// ============================================================
// STAMMVERTRETER GITEA SYNC
// ============================================================
async function uploadSvToGitea() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('sv-sync-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
const list = loadStammvertreter();
const data = { lastUpdated: new Date().toISOString().slice(0,10), substitutes: list.map(s => ({ id:s.id, name:s.name, company:s.company, specialty:s.specialty, photoB64: s.photoB64||null, fromChapter:s.fromChapter||null })) };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/stammvertreter.json`;
try {
let sha = null;
const shaRes = await fetch(giteaAuthUrl(path, token));
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: 'Stammvertreter aktualisiert', content };
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) });
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'; }
}
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' });
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)'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// MITGLIEDER
// ============================================================
function renderMembersList() {
const state = loadState();
const extras = loadExtras();
const container = document.getElementById('admin-members-list');
if (!state.members.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Keine Mitglieder vorhanden. Erst Mitglieder über die Steuerung (BNI-Sync) importieren.</div>`;
return;
}
container.innerHTML = '';
state.members.forEach(m => {
const ex = extras[extrasKey(m.name)] || {};
const photoB64 = ex.photoB64 || ex.offlinePhotoB64 || null;
const div = document.createElement('div');
div.className = 'member-item';
div.style.cursor = 'pointer';
div.title = 'Foto bearbeiten';
div.innerHTML = `${avatarHtml(m.name, photoB64, m.photoUrl, 40)}
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(m.name)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${escHtml(m.company||'')}${m.specialty?' · '+escHtml(m.specialty):''}</div>
</div>
<span style="font-size:0.72rem;color:${photoB64?'var(--success)':'#d97706'};">${photoB64?'✓ Foto':'+ Foto'}</span>`;
div.onclick = () => openMemberPhotoModal(m.name, photoB64);
container.appendChild(div);
});
}
let _memberPhotoB64 = null;
function openMemberPhotoModal(name, currentPhotoB64) {
_memberPhotoB64 = currentPhotoB64 || null;
document.getElementById('member-photo-name').value = name;
document.getElementById('member-photo-modal-title').textContent = name;
updateMemberPhotoUI(_memberPhotoB64, name);
openAdminModal('modal-member-photo');
}
function updateMemberPhotoUI(b64, name) {
_memberPhotoB64 = b64;
const thumb = document.getElementById('member-photo-thumb');
const initials = document.getElementById('member-photo-initials');
const removeBtn = document.getElementById('member-photo-remove');
if (b64) {
thumb.src = b64; thumb.style.display = 'block';
initials.style.display = 'none';
removeBtn.style.display = 'inline-flex';
} else {
thumb.src = ''; thumb.style.display = 'none';
initials.style.display = 'flex';
initials.style.background = getColor(name || '');
initials.textContent = getInitials(name || '');
removeBtn.style.display = 'none';
}
}
function handleMemberPhotoFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const name = document.getElementById('member-photo-name').value;
resizePhotoToBase64(file).then(b64 => updateMemberPhotoUI(b64, name)).catch(e => alert(e.message));
input.value = '';
}
function removeMemberPhoto() {
updateMemberPhotoUI(null, document.getElementById('member-photo-name').value);
}
function saveMemberPhoto() {
const name = document.getElementById('member-photo-name').value;
const extras = loadExtras();
const key = extrasKey(name);
if (!extras[key]) extras[key] = {};
if (_memberPhotoB64) {
extras[key].photoB64 = _memberPhotoB64;
} else {
delete extras[key].photoB64;
delete extras[key].offlinePhotoB64;
}
saveExtras(extras);
closeAdminModal('modal-member-photo');
renderMembersList();
}
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' });
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; }
const state = loadState();
const extras = loadExtras();
const map = new Map(state.members.map(m => [m.id, m]));
data.members.forEach(m => {
map.set(m.id, Object.assign({}, map.get(m.id)||{}, m));
if (m.photoData) { const key=extrasKey(m.name); if(!extras[key])extras[key]={}; extras[key].photoB64=m.photoData; }
});
state.members = Array.from(map.values());
const known = new Set(state.meetingOrder);
state.members.forEach(m => { if(!known.has(m.id)) state.meetingOrder.push(m.id); });
saveState(state); saveExtras(extras);
statusEl.textContent = `✓ ${data.members.length} Mitglieder geladen`; statusEl.style.color = 'var(--success)';
renderMembersList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadMembersAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
try {
const state = loadState();
const extras = loadExtras();
const members = state.members.map(m => {
const ex = extras[extrasKey(m.name)] || {};
const entry = { id:m.id, name:m.name, company:m.company, specialty:m.specialty, photoUrl:m.photoUrl||'', profileUrl:m.profileUrl||'' };
if (ex.photoB64 || ex.offlinePhotoB64) entry.photoData = ex.photoB64 || ex.offlinePhotoB64;
return entry;
});
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);
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 };
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) });
console.log('[Upload] Upload-Status:', res.status);
if (res.ok) { statusEl.textContent = `✓ ${members.length} Mitglieder hochgeladen`; statusEl.style.color = 'var(--success)'; }
else {
const errText = await res.text();
console.error('[Upload] Fehler:', errText);
statusEl.textContent = `Fehler ${res.status}: ${errText.slice(0,80)}`; statusEl.style.color = '#dc2626';
}
} catch(e) {
console.error('[Upload] Exception:', e);
statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626';
}
}
// ============================================================
// SUPERADMIN
// ============================================================
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 items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
container.innerHTML = '';
for (const ch of chapters) {
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`));
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const membersRes = await fetch(giteaUrl(`chapters/${ch.name}/members.json`));
const membersData = membersRes.ok ? await membersRes.json() : {};
const div = document.createElement('div');
div.className = 'chapter-item';
div.innerHTML = `<div style="flex:1;">
<div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div>
<div style="font-size:0.78rem;color:var(--muted);">Code: <code>${ch.name}</code> · Mitglieder: ${(membersData.members||[]).length} · Stand: ${membersData.lastUpdated||'—'}</div>
</div>`;
container.appendChild(div);
} catch(e) { /* skip */ }
}
if (!container.children.length) container.innerHTML = '<div style="color:var(--muted);">Keine Chapter gefunden.</div>';
} catch(e) {
container.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden.</div>';
}
}
async function loadCurrentVersion() {
try {
const res = await fetch(giteaUrl('version.json'), { cache:'no-store' });
if (res.ok) {
const data = await res.json();
document.getElementById('current-version-display').textContent = data.version || '—';
document.getElementById('new-version-input').placeholder = data.version ? `z.B. ${data.version.split('.').map((n,i)=>i===2?+n+1:n).join('.')}` : 'z.B. 1.2.0';
}
} catch(e) { /* ignore */ }
}
async function pushVersionUpdate() {
const token = getAdminToken();
if (!token) { alert('Bitte Admin-Token in den Einstellungen eintragen.'); return; }
const version = document.getElementById('new-version-input').value.trim();
const notes = document.getElementById('new-version-notes').value.trim();
if (!version) { alert('Bitte Versionsnummer eingeben.'); return; }
const statusEl = document.getElementById('version-status');
statusEl.textContent = 'Pushe…'; statusEl.style.color = 'var(--muted)';
const content = btoa(unescape(encodeURIComponent(JSON.stringify({ version, notes }, null, 2))));
try {
let sha = null;
const shaRes = await fetch(giteaAuthUrl('version.json', token));
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Version ${version}`, content };
if (sha) body.sha = sha;
const res = await fetch(giteaAuthUrl('version.json', token), { method:sha?'PUT':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
if (res.ok) {
statusEl.textContent = `✓ Version ${version} gepusht – alle Nutzer sehen das Update-Banner`;
statusEl.style.color = 'var(--success)';
document.getElementById('current-version-display').textContent = version;
} else { statusEl.textContent = 'Fehler beim Pushen.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// EINSTELLUNGEN
// ============================================================
function renderSettings() {
const token = getAdminToken();
document.getElementById('settings-token').value = token ? '••••••••' : '';
document.getElementById('settings-token').onfocus = function() { if (this.value==='••••••••') this.value=''; };
if (activeChapter) {
const cfg = activeChapter.config || {};
document.getElementById('chapter-info-display').innerHTML = `
<div><strong>Chapter:</strong> ${escHtml(cfg.chapterName||'—')}</div>
<div><strong>Code:</strong> <code>${activeChapter.code}</code></div>
<div><strong>Version:</strong> ${APP_VERSION}</div>
<div><strong>Aktiviert am:</strong> ${activeChapter.activatedAt ? new Date(activeChapter.activatedAt).toLocaleDateString('de') : '—'}</div>`;
}
}
function saveSettingsToken() {
const val = document.getElementById('settings-token').value;
const realVal = val === '••••••••' ? getAdminToken() : val.trim();
if (realVal) { localStorage.setItem('bni-gitea-admin-token', realVal); document.getElementById('token-status').textContent = '✓ Gespeichert'; document.getElementById('token-status').style.color = 'var(--success)'; }
else { localStorage.removeItem('bni-gitea-admin-token'); document.getElementById('token-status').textContent = 'Token entfernt.'; document.getElementById('token-status').style.color = 'var(--muted)'; }
if (getAdminToken()) document.getElementById('superadmin-tab').style.display = '';
setTimeout(() => { document.getElementById('token-status').textContent = ''; }, 3000);
}
// ============================================================
// INIT
// ============================================================
if (!activeChapter) {
document.body.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100vh;flex-direction:column;gap:16px;color:var(--muted);">
<div style="font-size:1.2rem;font-weight:600;">Kein Chapter aktiviert</div>
<div>Bitte zuerst <a href="index.html" style="color:var(--red);">das Tool öffnen</a> und den Lizenzcode eingeben.</div>
</div>`;
} else {
const cfg = activeChapter.config || {};
document.getElementById('admin-chapter-name').textContent = cfg.chapterName || activeChapter.code;
if (cfg.logoUrl && cfg.logoUrl.startsWith('http')) document.getElementById('admin-logo').src = cfg.logoUrl;
if (getAdminToken()) document.getElementById('superadmin-tab').style.display = '';
renderSvList();
}
</script>
</body>
</html>

View File

@@ -1,500 +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; }
@media print {
.cover { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
.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.1 · 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>
</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 – Mitgliederliste</span>
</div>
<div class="mockup-body">
<div class="mock-tabs">
<div class="mock-tab active">Mitglieder</div>
<div class="mock-tab">Vertretungen</div>
<div class="mock-tab">Besucher</div>
</div>
<div class="mock-panel">
<div class="mock-member">
<div class="mock-avatar" style="background:#1a2744;">AM</div>
<div><div class="mock-name">Anna Müller</div><div class="mock-role">Steuerberatung</div></div>
<div class="mock-badge present">anwesend</div>
</div>
<div class="mock-member" style="opacity:0.5;">
<div class="mock-avatar" style="background:#7f1d1d;">KS</div>
<div><div class="mock-name">Klaus Schmidt</div><div class="mock-role">IT-Systeme</div></div>
<div class="mock-badge absent">abwesend</div>
</div>
<div class="mock-member">
<div class="mock-avatar" style="background:#1e3a5f;">SL</div>
<div><div class="mock-name">Sandra Lang</div><div class="mock-role">Marketing</div></div>
<div class="mock-badge present">anwesend</div>
</div>
</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>
<!-- 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>

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
}

File diff suppressed because one or more lines are too long

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,7 @@
{
"chapterName": "BNI Chapter Helios",
"logoUrl": "BNI Logo.png",
"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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
{
"chapterName": "BNI Chapter Kallisto",
"syncUrl": "https://bni-ulm.com/kallisto/de/memberlist?chapterName=35844&regionIds=43671$isChapterwebsite",
"websiteUrl": "https://bni-ulm.de/kallisto",
"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,82 @@
{
"items": [
{
"id": "willkommen",
"label": "Willkommen",
"type": "embed",
"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",
"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",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0d27561ab34c141a39f981ab6b36fbd03"
},
{
"id": "break-out",
"label": "Break Out",
"type": "embed",
"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",
"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",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0b231f4b1bdf34850a8a8baca4b2d6972"
},
{
"id": "bericht-des-schatzmeisters",
"label": "Bericht des Schatzmeisters",
"type": "embed",
"embedUrl": "https://show.zohopublic.eu/publish/cb5g0249a5f37a97e419db184bef50bcbdb8e"
},
{
"id": "bericht-des-eventkoordinators",
"label": "Bericht des Eventkoordinators",
"type": "embed",
"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,46 @@
{
"items": [
{
"id": "bericht-der-schatzmeisterin",
"label": "Bericht der Schatzmeisterin",
"type": "image",
"imagePath": "agenda/bericht-der-schatzmeisterin.jpg"
},
{
"id": "willkommen",
"label": "Willkommen",
"type": "image",
"imagePath": "agenda/willkommen.jpg"
},
{
"id": "ende-logo",
"label": "Ende - Logo",
"type": "image",
"imagePath": "agenda/ende-logo.jpg"
},
{
"id": "bericht-des-mitgliederkoordinators-besuchertag",
"label": "Bericht des Mitgliederkoordinators Besuchertag",
"type": "image",
"imagePath": "agenda/bericht-des-mitgliederkoordinators-besuchertag.jpg"
},
{
"id": "bericht-des-mitgliederkoordinators-megafruehstueck",
"label": "Bericht des Mitgliederkoordinators Megafrühstück",
"type": "image",
"imagePath": "agenda/bericht-des-mitgliederkoordinators-megafruehstueck.jpg"
},
{
"id": "logo",
"label": "Logo",
"type": "image",
"imagePath": "agenda/logo.jpg"
},
{
"id": "logo-2",
"label": "Logo 2",
"type": "image",
"imagePath": "agenda/logo-2.jpg"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 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

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

View File

@@ -0,0 +1,134 @@
{
"year": "2025-2026",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 98
},
{
"month": "2025-05",
"value": 98
}
]
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 41
},
{
"month": "2025-05",
"value": 42
}
]
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 1
},
{
"month": "2025-05",
"value": 1
}
]
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 1
},
{
"month": "2025-05",
"value": 6
}
]
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 118
},
{
"month": "2025-05",
"value": 72
}
]
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 254
},
{
"month": "2025-05",
"value": 59
}
]
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 70
},
{
"month": "2025-05",
"value": 32
}
]
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"annualTarget": 0,
"entries": [
{
"month": "2025-04",
"value": 106513
},
{
"month": "2025-05",
"value": 231210
}
]
}
],
"updatedAt": "2026-05-21"
}

View File

@@ -0,0 +1,134 @@
{
"year": "2026-2027",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"annualTarget": 95,
"entries": [
{
"month": "2026-04",
"value": 96
},
{
"month": "2026-05",
"value": 97
}
]
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"annualTarget": 49,
"entries": [
{
"month": "2026-04",
"value": 42
},
{
"month": "2026-05",
"value": 42
}
]
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"annualTarget": 7,
"entries": [
{
"month": "2026-04",
"value": 1
},
{
"month": "2026-05",
"value": 1
}
]
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"annualTarget": 10.5,
"entries": [
{
"month": "2026-04",
"value": 0
},
{
"month": "2026-05",
"value": 6
}
]
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"annualTarget": 168,
"entries": [
{
"month": "2026-04",
"value": 56
},
{
"month": "2026-05",
"value": 72
}
]
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"annualTarget": 705.6,
"entries": [
{
"month": "2026-04",
"value": 208
},
{
"month": "2026-05",
"value": 59
}
]
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"annualTarget": 100.8,
"entries": [
{
"month": "2026-04",
"value": 34
},
{
"month": "2026-05",
"value": 32
}
]
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"annualTarget": 146034,
"entries": [
{
"month": "2026-04",
"value": 26259
},
{
"month": "2026-05",
"value": 231210
}
]
}
],
"updatedAt": "2026-05-24"
}

View File

@@ -0,0 +1,61 @@
{
"updatedAt": "2026-04-27",
"metrics": [
{
"key": "attendance",
"label": "Anwesenheit",
"unit": "%",
"target": 95,
"current": 96
},
{
"key": "members",
"label": "Mitgliederzahl",
"unit": "",
"target": 42,
"current": 42
},
{
"key": "newMembers",
"label": "Neue Mitglieder",
"unit": "",
"target": 1.3,
"current": 1
},
{
"key": "guests",
"label": "Gäste / Besucher",
"unit": "",
"target": 10.5,
"current": 0
},
{
"key": "oneOnOne",
"label": "4-Augen-Gespräche",
"unit": "",
"target": 168,
"current": 56
},
{
"key": "cte",
"label": "CTE",
"unit": "",
"target": 705.6,
"current": 208
},
{
"key": "referrals",
"label": "Empfehlungen",
"unit": "",
"target": 100.8,
"current": 34
},
{
"key": "revenueThank",
"label": "Umsatzdanke",
"unit": "",
"target": 146034,
"current": 26259
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4491
index.html

File diff suppressed because it is too large Load Diff

BIN
logo.jpeg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -1,4 +0,0 @@
{
"version": "1.1.0",
"notes": "Multi-Chapter-Support mit Lizenzcode"
}