const state = { token: null, kcId: null, whoami: null, socket: null, activeChannelId: null, pickedUsers: new Set(), pickedGuests: new Set(), }; const $ = (id) => document.getElementById(id); function decodeJwtPayload(token) { try { const [, payload] = token.split('.'); return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); } catch { return null; } } function setStatus(el, message, kind) { el.textContent = message; el.classList.remove('status-ok', 'status-error'); if (kind === 'ok') el.classList.add('status-ok'); if (kind === 'error') el.classList.add('status-error'); } function setConnBadge(online) { const badge = $('conn-badge'); if (!badge) return; badge.textContent = online ? 'angemeldet' : 'nicht angemeldet'; badge.classList.toggle('badge-online', online); badge.classList.toggle('badge-muted', !online); } function clearList(listEl, emptyMessage) { listEl.innerHTML = ''; if (emptyMessage) { const li = document.createElement('li'); li.className = 'list-empty'; li.textContent = emptyMessage; listEl.appendChild(li); } } async function apiFetch(path, options = {}) { const headers = Object.assign({}, options.headers); if (state.token) headers.Authorization = `Bearer ${state.token}`; if (options.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json'; return fetch(`/api${path}`, Object.assign({}, options, { headers })); } /// --- Tabs (Gast / Team-Login) --- document.querySelectorAll('.tab-btn').forEach((btn) => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach((p) => (p.hidden = true)); btn.classList.add('active'); const panel = document.querySelector(`[data-panel="${btn.dataset.tab}"]`); if (panel) panel.hidden = false; }); }); /// --- Login: Gast/Konfi --- $('guest-form').addEventListener('submit', async (event) => { event.preventDefault(); const inviteCode = $('invite-code').value; const firstName = $('first-name').value; const lastName = $('last-name').value; setStatus($('login-status'), 'Wird geprüft…'); let res; try { res = await apiFetch('/auth/guest', { method: 'POST', body: JSON.stringify({ inviteCode, firstName, lastName }), }); } catch (err) { setStatus($('login-status'), 'Netzwerkfehler. Bitte erneut versuchen.', 'error'); return; } if (!res.ok) { setStatus($('login-status'), `Fehler: ${res.status}`, 'error'); return; } const { accessToken } = await res.json(); await onLoggedIn(accessToken); }); /// --- Login: Team (Gemeinde-Name + Passwort) --- $('team-form').addEventListener('submit', async (event) => { event.preventDefault(); const gemeindeName = $('team-gemeinde').value; const password = $('team-password').value; setStatus($('login-status'), 'Wird geprüft…'); let res; try { res = await apiFetch('/auth/team-login', { method: 'POST', body: JSON.stringify({ gemeindeName, password }), }); } catch (err) { setStatus($('login-status'), 'Netzwerkfehler. Bitte erneut versuchen.', 'error'); return; } if (!res.ok) { setStatus($('login-status'), `Fehler: ${res.status}`, 'error'); return; } const { accessToken } = await res.json(); await onLoggedIn(accessToken); }); async function onLoggedIn(accessToken) { state.token = accessToken; const claims = decodeJwtPayload(accessToken); state.kcId = claims?.kcId ?? null; // Fetch identity/role from the API so the UI can show role-appropriate // sections (Gruppenchat creation is LT/Verantwortliche/r only). let me = null; try { const res = await apiFetch('/auth/me'); if (res.ok) me = await res.json(); } catch { // best-effort } state.whoami = me; setStatus($('login-status'), 'Angemeldet.', 'ok'); setConnBadge(true); $('login-section').hidden = true; $('app-section').hidden = false; renderWhoAmI(); } function renderWhoAmI() { const card = $('whoami-card'); const text = $('whoami-text'); const gruppenCard = $('gruppen-card'); const manageCard = $('manage-card'); if (!state.whoami) { card.hidden = true; gruppenCard.hidden = true; manageCard.hidden = true; return; } card.hidden = false; if (state.whoami.kind === 'guest') { text.textContent = `Konfi/Gast (KC ${state.whoami.kcId})`; gruppenCard.hidden = true; manageCard.hidden = true; } else { const roles = (state.whoami.memberships || []).map((m) => m.role).join(', ') || 'keine Rolle'; text.textContent = `${state.whoami.email} — ${roles}`; const canManageGruppen = state.whoami.isLeitungsteam || (state.whoami.memberships || []).some((m) => m.role === 'GEMEINDE_VERANTWORTLICHER'); gruppenCard.hidden = !canManageGruppen; manageCard.hidden = !canManageGruppen; if (state.kcId) $('gruppe-kcid').value = state.kcId; } } $('logout-btn').addEventListener('click', () => { if (state.socket) state.socket.close(); state.token = null; state.kcId = null; state.whoami = null; state.activeChannelId = null; setConnBadge(false); $('app-section').hidden = true; $('login-section').hidden = false; setStatus($('login-status'), ''); }); /// --- Gruppenchat: Kandidaten laden + auswählen --- $('load-candidates').addEventListener('click', async () => { const kcId = $('gruppe-kcid').value.trim(); if (!kcId) { setStatus($('gruppe-status'), 'Bitte KC-ID angeben.', 'error'); return; } let res; try { res = await apiFetch(`/chat/${kcId}/gruppen/participant-candidates`); } catch { setStatus($('gruppe-status'), 'Netzwerkfehler beim Laden.', 'error'); return; } if (!res.ok) { setStatus($('gruppe-status'), `Fehler: ${res.status}`, 'error'); return; } const { users, guests } = await res.json(); state.pickedUsers = new Set(); state.pickedGuests = new Set(); renderCandidateList($('candidate-users'), users, 'userId', (u) => `${u.firstName} ${u.lastName} (${u.role})`, state.pickedUsers); renderCandidateList($('candidate-guests'), guests, 'guestId', (g) => `${g.firstName} ${g.lastName}`, state.pickedGuests); setStatus($('gruppe-status'), `${users.length} Team-Mitglieder, ${guests.length} Konfis geladen.`, 'ok'); }); function renderCandidateList(listEl, items, idKey, labelFn, pickedSet) { clearList(listEl); if (!items.length) { clearList(listEl, 'Keine Kandidaten gefunden.'); return; } listEl.innerHTML = ''; for (const item of items) { const id = item[idKey]; const li = document.createElement('li'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.addEventListener('change', () => { if (checkbox.checked) pickedSet.add(id); else pickedSet.delete(id); li.classList.toggle('picked', checkbox.checked); }); const label = document.createElement('span'); label.textContent = labelFn(item); li.appendChild(checkbox); li.appendChild(label); li.addEventListener('click', (e) => { if (e.target === checkbox) return; checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); }); listEl.appendChild(li); } } $('create-gruppe').addEventListener('click', async () => { const kcId = $('gruppe-kcid').value.trim(); const name = $('gruppe-name').value.trim(); if (!kcId) { setStatus($('gruppe-status'), 'Bitte KC-ID angeben.', 'error'); return; } setStatus($('gruppe-status'), 'Wird erstellt…'); let res; try { res = await apiFetch(`/chat/${kcId}/gruppen`, { method: 'POST', body: JSON.stringify({ type: 'GRUPPE', name, participantUserIds: [...state.pickedUsers], participantGuestIds: [...state.pickedGuests], }), }); } catch { setStatus($('gruppe-status'), 'Netzwerkfehler.', 'error'); return; } if (!res.ok) { setStatus($('gruppe-status'), `Fehler: ${res.status}`, 'error'); return; } const channel = await res.json(); setStatus($('gruppe-status'), `Erstellt: ${channel.id}`, 'ok'); $('manage-channel-id').value = channel.id; $('channel-id').value = channel.id; }); /// --- Teilnehmerverwaltung --- $('add-participant').addEventListener('click', () => manageParticipant('add')); $('remove-participant').addEventListener('click', () => manageParticipant('remove')); async function manageParticipant(action) { const channelId = $('manage-channel-id').value.trim(); const userId = $('manage-user-id').value.trim(); const guestId = $('manage-guest-id').value.trim(); if (!channelId || (!userId && !guestId)) { setStatus($('manage-status'), 'Channel-ID und User- oder Gast-ID angeben.', 'error'); return; } setStatus($('manage-status'), 'Wird gesendet…'); const method = action === 'add' ? 'POST' : 'DELETE'; let res; try { res = await apiFetch(`/chat/gruppen/${channelId}/participants`, { method, body: JSON.stringify({ userId: userId || undefined, guestId: guestId || undefined }), }); } catch { setStatus($('manage-status'), 'Netzwerkfehler.', 'error'); return; } if (!res.ok) { setStatus($('manage-status'), `Fehler: ${res.status}`, 'error'); return; } setStatus($('manage-status'), action === 'add' ? 'Hinzugefügt.' : 'Entfernt.', 'ok'); } /// --- Workshop-Wahl --- $('submit-wahl').addEventListener('click', async () => { const wahlId = $('wahl-id').value; const prioritaeten = $('prioritaeten') .value.split(',') .map((s) => s.trim()) .filter(Boolean); if (!wahlId || prioritaeten.length === 0) { setStatus($('wahl-status'), 'Bitte Wahl-ID und mindestens eine Priorität angeben.', 'error'); return; } setStatus($('wahl-status'), 'Wird gesendet…'); let res; try { res = await apiFetch(`/wahl/${wahlId}/teilnehmer`, { method: 'POST', body: JSON.stringify({ prioritaeten }), }); } catch (err) { setStatus($('wahl-status'), 'Netzwerkfehler. Bitte erneut versuchen.', 'error'); return; } if (res.ok) { setStatus($('wahl-status'), 'Gespeichert.', 'ok'); } else { setStatus($('wahl-status'), `Fehler: ${res.status}`, 'error'); } }); /// --- Dateien --- $('load-files').addEventListener('click', async () => { if (!state.kcId) return; let res; try { res = await apiFetch(`/files/${state.kcId}`); } catch (err) { clearList($('file-list'), 'Fehler beim Laden der Dateien.'); return; } const files = res.ok ? await res.json() : []; const list = $('file-list'); if (!files.length) { clearList(list, 'Keine Dateien verfügbar.'); return; } list.innerHTML = ''; for (const file of files) { const li = document.createElement('li'); li.textContent = file.filename; list.appendChild(li); } }); /// --- Chat: eigene Channels auflisten --- $('load-channels').addEventListener('click', async () => { if (!state.kcId) { setStatus($('login-status'), ''); return; } let res; try { res = await apiFetch(`/chat/${state.kcId}/channels`); } catch { clearList($('channel-list'), 'Fehler beim Laden der Chats.'); return; } const channels = res.ok ? await res.json() : []; const list = $('channel-list'); if (!channels.length) { clearList(list, 'Keine Chats verfügbar.'); return; } list.innerHTML = ''; for (const channel of channels) { const li = document.createElement('li'); const label = document.createElement('span'); label.textContent = channel.name || channel.type; const chip = document.createElement('span'); chip.className = 'chip'; chip.textContent = channel.type; li.appendChild(label); li.appendChild(chip); li.addEventListener('click', () => { $('channel-id').value = channel.id; joinChannel(channel.id); }); list.appendChild(li); } }); /// --- Chat: beitreten + live mitlesen/schreiben --- $('join-channel').addEventListener('click', () => { const channelId = $('channel-id').value; if (!channelId || !state.token) return; joinChannel(channelId); }); function joinChannel(channelId) { if (state.socket) state.socket.close(); state.activeChannelId = channelId; const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; const socket = new WebSocket(`${protocol}://${location.host}/chat?token=${state.token}`); state.socket = socket; const chatLog = $('chat-log'); clearList(chatLog, 'Verbindung wird hergestellt…'); socket.addEventListener('open', () => { socket.send(JSON.stringify({ event: 'chat:join', data: { channelId } })); clearList(chatLog, 'Verbunden. Warte auf Nachrichten…'); }); socket.addEventListener('message', (event) => { const { event: name, data } = JSON.parse(event.data); if (name === 'chat:participants-changed') { if (data.channelId === state.activeChannelId) { setStatus($('manage-status'), 'Teilnehmerliste wurde aktualisiert.', 'ok'); } return; } if (name !== 'chat:message') return; const emptyPlaceholder = chatLog.querySelector('.list-empty'); if (emptyPlaceholder) emptyPlaceholder.remove(); const li = document.createElement('li'); const body = document.createElement('span'); body.className = 'chat-body'; body.textContent = data.body; li.appendChild(body); chatLog.appendChild(li); chatLog.scrollTop = chatLog.scrollHeight; }); socket.addEventListener('close', () => { setConnBadge(!!state.token); }); } $('send-message').addEventListener('click', () => { const input = $('chat-message'); const body = input.value.trim(); if (!body || !state.socket || !state.activeChannelId) return; state.socket.send( JSON.stringify({ event: 'chat:send', data: { channelId: state.activeChannelId, body } }), ); input.value = ''; }); $('chat-message').addEventListener('keydown', (e) => { if (e.key === 'Enter') $('send-message').click(); });