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>
This commit is contained in:
Christian Heubaum
2026-08-19 18:59:22 +02:00
parent b40f384123
commit 580725f2f7
22 changed files with 1527 additions and 152 deletions

View File

@@ -102,6 +102,7 @@
<div class="tabs">
<div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div>
<div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div>
<div class="tab" onclick="switchAdminTab('agenda')">Agenda</div>
<div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div>
<div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div>
</div>
@@ -146,6 +147,27 @@
</div>
</div>
<!-- ============================================================ AGENDA -->
<div id="tab-agenda" class="tab-content">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div class="card-title" style="margin:0;">Agenda-Folien (Online-Meeting)</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchAgendaAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadAgendaAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">
Punkte für den Online-Meeting-Ablauf (z.B. Begrüßung, DSGVO, BNI-Zahlen, Events) – als Bild oder als Live-Einbettung
aus Zoho Show. Die Reihenfolge hier ist nur der Katalog; welche Punkte in welcher Reihenfolge live gezeigt werden,
stellt die Moderatorin frei im Steuerpult (index.html) ein.
</p>
<div class="status-msg" id="agenda-status"></div>
<div id="admin-agenda-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
<button class="btn btn-secondary btn-sm w-full" style="margin-top:12px;" onclick="openAddAgendaItem()">+ Agenda-Punkt hinzufügen</button>
</div>
</div>
<!-- ============================================================ SUPERADMIN -->
<div id="tab-superadmin" class="tab-content">
<div class="card">
@@ -291,6 +313,42 @@
</div>
</div>
<!-- Agenda Item Modal -->
<div class="modal-overlay" id="modal-agenda-item">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="agenda-item-modal-title">Agenda-Punkt</div>
<button class="modal-close" onclick="closeAdminModal('modal-agenda-item')">✕</button>
</div>
<input type="hidden" id="ai-id">
<label>Bezeichnung</label>
<input type="text" id="ai-label" placeholder="z.B. Begrüßung">
<label>Anzeigeart</label>
<select id="ai-type" onchange="updateAgendaItemTypeUI()">
<option value="image">Bild (Export, wird hochgeladen)</option>
<option value="embed">Live-Einbettung (Zoho Show o.ä.)</option>
</select>
<div id="ai-image-row">
<label>Bild</label>
<div class="photo-actions">
<img id="ai-image-thumb" class="photo-thumb" style="width:80px;height:80px;border-radius:8px;display:none;" src="" alt="">
<label class="btn-file-label">📁 Bild wählen<input type="file" id="ai-image-file" accept="image/*" style="display:none" onchange="handleAgendaImageFile(this)"></label>
</div>
<div id="ai-image-thumb-hint" style="display:none;font-size:0.78rem;color:#d97706;margin-top:6px;"></div>
</div>
<div id="ai-embed-row" style="display:none;">
<label>Einbettungs-URL</label>
<input type="text" id="ai-embed-url" placeholder="https://show.zoho.eu/publish/...">
<p style="font-size:0.78rem;color:var(--muted);margin-top:4px;">In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, Link hier einfügen.</p>
</div>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="ai-delete-btn" onclick="deleteAgendaItem()" style="display:none;margin-right:auto;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-agenda-item')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveAgendaItem()">Speichern</button>
</div>
</div>
</div>
<!-- Import from Chapter Modal -->
<div class="modal-overlay" id="modal-import-chapter">
<div class="modal">
@@ -399,6 +457,7 @@ function switchAdminTab(tab) {
document.getElementById(`tab-${tab}`).classList.add('active');
if (tab === 'stammvertreter') renderSvList();
if (tab === 'mitglieder') renderMembersList();
if (tab === 'agenda') renderAgendaAdminList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings();
}
@@ -780,6 +839,241 @@ async function uploadMembersAdmin() {
}
}
// ============================================================
// AGENDA-FOLIEN (Online-Meeting-Ablauf)
// ============================================================
let _agendaImageDataUrl = null;
let _agendaImageChanged = false;
let _agendaImageProcessing = null; // Promise, solange ein gewähltes Bild noch verkleinert wird
function slugify(s) {
const map = { ä:'ae', ö:'oe', ü:'ue', ß:'ss' };
return (s || '').toLowerCase().trim()
.replace(/[äöüß]/g, c => map[c])
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'punkt';
}
function renderAgendaAdminList() {
const state = loadState();
const container = document.getElementById('admin-agenda-list');
const items = state.agendaCatalog || [];
if (!items.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Noch keine Agenda-Punkte. Mit "+ Agenda-Punkt hinzufügen" starten.</div>`;
return;
}
container.innerHTML = '';
items.forEach(it => {
const div = document.createElement('div');
div.className = 'member-item';
div.style.cursor = 'pointer';
div.title = 'Bearbeiten';
const typeLabel = it.type === 'embed' ? 'Live-Einbettung' : 'Bild';
div.innerHTML = `
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(it.label || it.id)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${typeLabel}</div>
</div>
<button class="btn btn-ghost btn-sm">✎</button>
`;
div.onclick = () => editAgendaItem(it.id);
container.appendChild(div);
});
}
function openAddAgendaItem() {
document.getElementById('ai-id').value = '';
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt hinzufügen';
document.getElementById('ai-label').value = '';
document.getElementById('ai-type').value = 'image';
document.getElementById('ai-embed-url').value = '';
document.getElementById('ai-delete-btn').style.display = 'none';
_agendaImageDataUrl = null; _agendaImageChanged = false;
document.getElementById('ai-image-thumb').style.display = 'none';
document.getElementById('ai-image-thumb-hint').style.display = 'none';
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function editAgendaItem(id) {
const state = loadState();
const it = (state.agendaCatalog || []).find(a => a.id === id);
if (!it) return;
document.getElementById('ai-id').value = it.id;
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt bearbeiten';
document.getElementById('ai-label').value = it.label || '';
document.getElementById('ai-type').value = it.type || 'image';
document.getElementById('ai-embed-url').value = it.embedUrl || '';
document.getElementById('ai-delete-btn').style.display = 'inline-flex';
_agendaImageDataUrl = null; _agendaImageChanged = false;
const thumb = document.getElementById('ai-image-thumb');
const thumbHint = document.getElementById('ai-image-thumb-hint');
if (it.imagePath) {
thumb.onerror = () => {
thumb.style.display = 'none';
if (thumbHint) { thumbHint.textContent = '⚠ Bild nicht auf dem Server gefunden – bitte neu hochladen.'; thumbHint.style.display = 'block'; }
};
thumb.onload = () => { if (thumbHint) thumbHint.style.display = 'none'; };
if (thumbHint) thumbHint.style.display = 'none';
thumb.src = it.imagePath.startsWith('http') ? it.imagePath : giteaUrl(`chapters/${activeChapter.code}/${it.imagePath}`);
thumb.style.display = 'block';
} else {
thumb.style.display = 'none';
if (thumbHint) thumbHint.style.display = 'none';
}
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function updateAgendaItemTypeUI() {
const type = document.getElementById('ai-type').value;
document.getElementById('ai-image-row').style.display = type === 'image' ? 'block' : 'none';
document.getElementById('ai-embed-row').style.display = type === 'embed' ? 'block' : 'none';
}
function handleAgendaImageFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const hint = document.getElementById('ai-image-thumb-hint');
if (hint) { hint.textContent = 'Bild wird verarbeitet…'; hint.style.color = 'var(--muted)'; hint.style.display = 'block'; }
// Auf eine für Vollbild-Anzeige ausreichende Größe verkleinern/komprimieren, damit der
// Upload sicher unter dem Größenlimit der Gitea-Contents-API bleibt (typischerweise ~1 MB/Datei).
// _agendaImageProcessing merkt sich das laufende Promise, damit saveAgendaItem() nicht zu früh
// speichert, während das Bild noch verarbeitet wird (führte sonst zu einem stillen "kein Bild gewählt").
_agendaImageProcessing = resizePhotoToBase64(file, 1920).then(b64 => {
_agendaImageDataUrl = b64;
_agendaImageChanged = true;
const thumb = document.getElementById('ai-image-thumb');
thumb.src = b64;
thumb.style.display = 'block';
if (hint) {
const kb = Math.round((b64.length * 0.75) / 1024);
hint.textContent = `Für den Upload verkleinert auf max. 1920px, ~${kb} KB.`;
hint.style.color = 'var(--muted)';
hint.style.display = 'block';
}
}).catch(() => alert('Bild konnte nicht gelesen werden.'))
.finally(() => { _agendaImageProcessing = null; });
input.value = '';
}
async function uploadAgendaImage(path, dataUrl, token) {
const content = dataUrl.split(',')[1];
const url = giteaAuthUrl(path, token);
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda-Bild aktualisiert ${new Date().toLocaleDateString('de')}`, content };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status}: ${t.slice(0, 80)}`); }
}
async function saveAgendaItem() {
const label = document.getElementById('ai-label').value.trim();
if (!label) { alert('Bitte eine Bezeichnung eingeben.'); return; }
const type = document.getElementById('ai-type').value;
if (_agendaImageProcessing) {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Bild wird noch verarbeitet…'; statusEl.style.color = 'var(--muted)';
await _agendaImageProcessing;
}
const state = loadState();
if (!state.agendaCatalog) state.agendaCatalog = [];
let id = document.getElementById('ai-id').value;
if (!id) {
const base = slugify(label);
const ids = new Set(state.agendaCatalog.map(a => a.id));
let candidate = base, n = 1;
while (ids.has(candidate)) candidate = `${base}-${++n}`;
id = candidate;
}
const existing = state.agendaCatalog.find(a => a.id === id) || {};
const item = { id, label, type };
if (type === 'embed') {
const url = document.getElementById('ai-embed-url').value.trim();
if (!url) { alert('Bitte eine Einbettungs-URL eingeben.'); return; }
item.embedUrl = url;
} else {
item.imagePath = existing.imagePath || `agenda/${id}.jpg`;
if (!_agendaImageChanged && !existing.imagePath) { alert('Bitte ein Bild wählen.'); return; }
}
const statusEl = document.getElementById('agenda-status');
if (type === 'image' && _agendaImageChanged && _agendaImageDataUrl) {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
statusEl.textContent = 'Lade Bild hoch…'; statusEl.style.color = 'var(--muted)';
try {
await uploadAgendaImage(`chapters/${activeChapter.code}/${item.imagePath}`, _agendaImageDataUrl, token);
} catch(e) {
statusEl.textContent = `Fehler beim Bild-Upload: ${e.message}`; statusEl.style.color = '#dc2626';
alert(`Bild-Upload fehlgeschlagen: ${e.message}\n\nDer Agenda-Punkt wurde NICHT gespeichert.`);
return;
}
}
const idx = state.agendaCatalog.findIndex(a => a.id === id);
if (idx >= 0) state.agendaCatalog[idx] = item; else state.agendaCatalog.push(item);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
statusEl.textContent = '✓ Lokal gespeichert – "Auf Server hochladen" nicht vergessen, damit es im Steuerpult ankommt.';
statusEl.style.color = 'var(--success)';
}
function deleteAgendaItem() {
const id = document.getElementById('ai-id').value;
if (!id) return;
if (!confirm('Diesen Agenda-Punkt wirklich löschen?')) return;
const state = loadState();
state.agendaCatalog = (state.agendaCatalog || []).filter(a => a.id !== id);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
}
async function fetchAgendaAdmin() {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/agenda.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Noch keine Agenda auf dem Server.'; return; }
const data = await res.json();
const state = loadState();
state.agendaCatalog = Array.isArray(data.items) ? data.items : [];
saveState(state);
statusEl.textContent = `✓ ${state.agendaCatalog.length} Punkte geladen`; statusEl.style.color = 'var(--success)';
renderAgendaAdminList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadAgendaAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
try {
const state = loadState();
const data = { items: state.agendaCatalog || [] };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/agenda.json`;
const url = giteaAuthUrl(path, token);
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda aktualisiert ${new Date().toLocaleDateString('de')}`, content };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${data.items.length} Agenda-Punkte hochgeladen`; statusEl.style.color = 'var(--success)'; }
else { const t = await res.text(); statusEl.textContent = `Fehler ${res.status}: ${t.slice(0, 80)}`; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// SUPERADMIN
// ============================================================