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:
294
admin.html
294
admin.html
@@ -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
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user