/** * pc-live | Bleeter-Verwaltung * * Was das Bleeter-Fenster selbst nicht kann: Unternehmensprofile einrichten und * Mitarbeiter freigeben. Es hängt an derselben Anmeldung wie das Webhosting – * wer als admin@liveinvader.ls angemeldet ist, ist auch hier der Anbieter. * * Die Anmeldung sitzt unten links in der Seitenleiste, wie in der * Webhosting-App. Beide teilen sich die Sitzung: einmal anmelden genügt. * * Inhalte kommen von Spielern (Handles, Anzeigenamen). Sie werden über * textContent gesetzt, nie über innerHTML. */ const BleeterAdminApp = (() => { const WIN_ID = 'app-bleeteradmin'; const el = ICWebRender.el; /* Muss zu BUSINESS_TYPES in bleeter/server/management.lua passen. */ const PROFILE_TYPES = [ ['company', 'Unternehmen'], ['small_business', 'Kleingewerbe'], ['authority', 'Behörde'], ['lifeinvader', 'Lifeinvader'], ]; const typeLabel = (t) => (PROFILE_TYPES.find(p => p[0] === t) || [t, t])[1]; let session = null; // ic-web-Anmeldung let profiles = []; let provider = false; let winEl = null; let openId = null; // Profil, dessen Mitarbeiter gerade angezeigt werden /* ── Transport ────────────────────────────────────────── */ /* Bleeter antwortet über ein gemeinsames Ereignis, nicht über Anfrage-Ids. Deshalb wird je Aktion auf die nächste Antwort gewartet. */ const waiting = new Map(); function call(op, payload) { return new Promise((resolve) => { const timer = setTimeout(() => { if (waiting.get(op) === resolve) waiting.delete(op); resolve({ ok: false, error: 'Keine Antwort vom Server.' }); }, 10000); waiting.set(op, (data) => { clearTimeout(timer); resolve(data); }); fetchNui('bleeter', { op, payload: payload || {} }); }); } function onResponse(data) { if (!data || !data.action) return; const fn = waiting.get(data.action); if (!fn) return; waiting.delete(data.action); fn(data); } /* ── Gerüst ───────────────────────────────────────────── */ async function open() { const win = WindowManager.create({ id: WIN_ID, title: 'Bleeter-Verwaltung', icon: '🐦', width: 940, height: 640, content: `
`, }); if (!win) return; winEl = win; await refresh(); } function root(id) { if (!winEl || !winEl.isConnected) { winEl = document.querySelector('[data-wid="' + WIN_ID + '"]'); } return winEl ? winEl.querySelector('#' + id) : null; } function setMain(node) { const m = root('ba-main'); if (m) m.replaceChildren(node); } function info(icon, text) { const d = el('div', 'wh-empty'); d.appendChild(el('div', 'wh-empty-icon', icon)); d.appendChild(el('div', null, text)); return d; } function notify(res, okText) { Desktop.showNotification(res && res.ok ? '✔ ' + okText : '⚠ ' + ((res && res.error) || 'Fehlgeschlagen.')); return res && res.ok; } async function refresh() { setMain(info('⏳', 'Lade…')); const s = await ICWebNet.call('session'); session = (s.ok && s.data && s.data.session) || null; const res = await call('listBusinessProfiles'); profiles = (res.ok && res.payload && res.payload.profiles) || []; provider = !!(res.ok && res.payload && res.payload.provider); renderSide(); showProfiles(); } /* ── Seitenleiste mit Anmeldung unten links ───────────── */ function renderSide() { const side = root('ba-side'); if (!side) return; side.replaceChildren(); side.appendChild(el('div', 'wh-side-title', 'Bleeter')); const nav = el('div', 'wh-nav active'); nav.appendChild(el('span', 'wh-nav-icon', '🏢')); nav.appendChild(el('span', null, provider ? 'Alle Profile' : 'Meine Profile')); nav.addEventListener('click', () => showProfiles()); side.appendChild(nav); if (provider) { const add = el('div', 'wh-nav'); add.appendChild(el('span', 'wh-nav-icon', '+')); add.appendChild(el('span', null, 'Profil einrichten')); add.addEventListener('click', () => showCreate()); side.appendChild(add); } /* Anmeldung – unten links, wie im Webhosting. */ const foot = el('div', 'wh-side-user'); if (session) { foot.appendChild(el('div', 'wh-side-username', session.username)); foot.appendChild(el('div', 'wh-side-role', session.role === 'superadmin' ? 'Anbieter (Lifeinvader)' : { admin: 'Domänenadmin', editor: 'Redakteur' }[session.role] || session.role)); const out = el('a', 'wh-side-logout', 'Abmelden'); out.addEventListener('click', async () => { await ICWebNet.call('logout'); Desktop.showNotification('Abgemeldet.'); await refresh(); }); foot.appendChild(out); } else { foot.appendChild(el('div', 'wh-side-role', 'Nicht angemeldet')); const go = el('a', 'wh-side-logout', '🔑 Als Anbieter anmelden'); go.style.color = 'var(--accent)'; go.addEventListener('click', showLogin); foot.appendChild(go); } side.appendChild(foot); } /* ── Anmeldung ────────────────────────────────────────── */ function showLogin() { ICWebRender.modal('Anmelden', (box, close) => { box.appendChild(el('div', 'icweb-modal-sub', 'Zugangsdaten der Lifeinvader-Verwaltung. Es ist dieselbe Anmeldung wie ' + 'beim Webhosting – einmal anmelden genügt für beides.')); const mk = (label, type, value, placeholder) => { const row = el('div', 'wh-field'); row.appendChild(el('label', 'wh-label', label)); const input = el('input', 'icweb-input'); input.setAttribute('type', type); if (placeholder) input.setAttribute('placeholder', placeholder); input.value = value || ''; row.appendChild(input); box.appendChild(row); return input; }; const user = mk('Benutzername', 'text', 'admin@liveinvader.ls', 'name@domäne.ls'); const pass = mk('Passwort', 'password', ''); const err = el('div', 'wh-login-error'); box.appendChild(err); const row = el('div', 'icweb-modal-actions'); const cancel = el('button', 'icweb-btn-ghost', 'Abbrechen'); cancel.addEventListener('click', close); const go = el('button', 'icweb-btn-primary', 'Anmelden'); const submit = async () => { err.textContent = ''; if (!user.value || !pass.value) { err.textContent = 'Bitte Benutzername und Passwort eingeben.'; return; } go.disabled = true; const res = await ICWebNet.call('login', user.value, pass.value); go.disabled = false; pass.value = ''; if (!res.ok) { err.textContent = res.error || 'Anmeldung fehlgeschlagen.'; return; } close(); await refresh(); }; go.addEventListener('click', submit); [user, pass].forEach(i => i.addEventListener('keydown', e => { if (e.key === 'Enter') submit(); })); row.append(cancel, go); box.appendChild(row); setTimeout(() => pass.focus(), 0); }); } /* ── Profilliste ──────────────────────────────────────── */ function header(title, sub) { const h = el('div', 'wh-head'); h.appendChild(el('div', 'wh-head-title', title)); if (sub) h.appendChild(el('div', 'wh-head-sub', sub)); return h; } function showProfiles() { const wrap = el('div', 'wh-page'); wrap.appendChild(header(provider ? 'Unternehmensprofile' : 'Meine Profile', provider ? 'Alle Profile auf Bleeter, die keiner Privatperson gehören.' : 'Profile, für die du Mitarbeiter freigeben darfst.')); if (!session) { wrap.appendChild(info('🔑', 'Melde dich unten links an, um Bleeter zu verwalten.')); return setMain(wrap); } if (!profiles.length) { wrap.appendChild(info('🐦', provider ? 'Noch kein Unternehmensprofil eingerichtet.' : 'Du verwaltest kein Bleeter-Profil.')); return setMain(wrap); } const table = el('div', 'wh-table'); profiles.forEach(p => { const row = el('div', 'wh-row'); const col = el('div'); col.appendChild(el('div', 'wh-row-title', '@' + p.handle)); col.appendChild(el('div', 'wh-row-sub', (p.display_name || '—') + ' · ' + typeLabel(p.profile_type) + ' · ' + (p.member_count || 0) + ' Mitarbeiter')); row.appendChild(col); const right = el('div', 'wh-row-right'); if (p.is_verified) right.appendChild(el('span', 'wh-tag ok', 'verifiziert')); if (p.is_locked) right.appendChild(el('span', 'wh-tag bad', 'gesperrt')); const members = el('button', 'icweb-btn-primary', 'Mitarbeiter'); members.addEventListener('click', () => showMembers(p)); right.appendChild(members); row.appendChild(right); table.appendChild(row); }); wrap.appendChild(table); setMain(wrap); } /* ── Profil einrichten (nur Anbieter) ─────────────────── */ function showCreate() { const wrap = el('div', 'wh-page'); wrap.appendChild(header('Profil einrichten', 'Das Profil gehört keinem Konto – bedient wird es über die freigegebenen ' + 'Mitarbeiter. Deshalb gehört gleich ein Verantwortlicher dazu.')); const form = el('div', 'wh-form'); const mk = (label, placeholder) => { const row = el('div', 'wh-field'); row.appendChild(el('label', 'wh-label', label)); const input = el('input', 'icweb-input'); if (placeholder) input.setAttribute('placeholder', placeholder); row.appendChild(input); form.appendChild(row); return input; }; const handleRow = el('div', 'wh-field'); handleRow.appendChild(el('label', 'wh-label', 'Handle')); const handleWrap = el('div', 'wh-inline'); handleWrap.appendChild(el('span', 'wh-inline-suffix', '@')); const handle = el('input', 'icweb-input'); handle.setAttribute('placeholder', 'weazelnews'); handleWrap.appendChild(handle); handleRow.appendChild(handleWrap); form.appendChild(handleRow); const display = mk('Anzeigename', 'Weazel News'); const mail = mk('Kontaktadresse (optional)', 'presse@weazel-news.ls'); const typeRow = el('div', 'wh-field'); typeRow.appendChild(el('label', 'wh-label', 'Art')); const type = el('select', 'icweb-input'); PROFILE_TYPES.forEach(([v, l]) => { const o = el('option', null, l); o.value = v; type.appendChild(o); }); typeRow.appendChild(type); form.appendChild(typeRow); const ownerRow = el('div', 'wh-field'); ownerRow.appendChild(el('label', 'wh-label', 'Verantwortlich (muss online sein)')); const owner = el('select', 'icweb-input'); // Ohne ausdrücklichen Wert liefert select.value den Text der Option. const noOwner = el('option', null, '— später festlegen —'); noOwner.value = ''; owner.appendChild(noOwner); ownerRow.appendChild(owner); form.appendChild(ownerRow); call('listCandidates', {}).then(res => { const list = (res.ok && res.payload && res.payload.candidates) || []; list.forEach(c => { const o = el('option', null, c.name); o.value = String(c.source); owner.appendChild(o); }); }); const go = el('button', 'icweb-btn-primary', 'Einrichten'); go.addEventListener('click', async () => { go.disabled = true; const res = await call('createBusinessProfile', { handle: handle.value, displayName: display.value, emailContact: mail.value, profileType: type.value, ownerSource: owner.value ? Number(owner.value) : null, }); go.disabled = false; if (notify(res, 'Profil eingerichtet.')) refresh(); }); form.appendChild(go); form.appendChild(el('div', 'wh-hint', 'Erlaubt sind Kleinbuchstaben, Ziffern, Punkt, Unterstrich und Bindestrich. ' + 'Behörden- und Systemnamen sind reserviert.')); wrap.appendChild(form); setMain(wrap); } /* ── Mitarbeiter ──────────────────────────────────────── */ async function showMembers(profile) { openId = profile.id; setMain(info('⏳', 'Lade Mitarbeiter…')); const [membersRes, candRes] = await Promise.all([ call('listProfileMembers', { profileId: profile.id }), call('listCandidates', { profileId: profile.id }), ]); const wrap = el('div', 'wh-page'); const head = el('div', 'wh-domainhead'); const left = el('div'); left.appendChild(el('div', 'wh-card-domain', '@' + profile.handle)); left.appendChild(el('div', 'wh-card-title', (profile.display_name || '') + ' · ' + typeLabel(profile.profile_type))); head.appendChild(left); const back = el('div', 'wh-tab-back', '← Übersicht'); back.addEventListener('click', () => showProfiles()); head.appendChild(back); wrap.appendChild(head); if (!membersRes.ok) { wrap.appendChild(el('div', 'wh-hint', membersRes.error || 'Keine Berechtigung.')); return setMain(wrap); } wrap.appendChild(el('div', 'wh-hint', 'Freigegebene schreiben unter diesem Profil. „Verwalten" gibt zusätzlich das ' + 'Recht, weitere Leute freizugeben – wer nur schreiben darf, kann sich ' + 'nicht selbst hochstufen.')); const members = (membersRes.payload && membersRes.payload.members) || []; const table = el('div', 'wh-table'); if (!members.length) table.appendChild(el('div', 'wh-hint', 'Noch niemand freigegeben.')); members.forEach(m => { const row = el('div', 'wh-row'); const col = el('div'); col.appendChild(el('div', 'wh-row-title', m.name || m.char_id)); col.appendChild(el('div', 'wh-row-sub', m.role || 'member')); row.appendChild(col); const right = el('div', 'wh-row-right'); right.appendChild(el('span', 'wh-tag ' + (m.online ? 'ok' : ''), m.online ? 'online' : 'offline')); if (m.can_post) right.appendChild(el('span', 'wh-tag', 'schreibt')); if (m.can_edit_profile) right.appendChild(el('span', 'wh-tag', 'Profil')); if (m.can_manage_members) right.appendChild(el('span', 'wh-tag warn', 'verwaltet')); const rm = el('button', 'icweb-btn-danger', 'Entfernen'); rm.addEventListener('click', () => { ICWebRender.confirmBox('Freigabe entfernen', (m.name || m.char_id) + ' schreibt dann nicht mehr unter @' + profile.handle + '.', async () => { const res = await call('setProfileMember', { profileId: profile.id, charId: m.char_id, remove: true, }); if (notify(res, 'Freigabe entfernt.')) showMembers(profile); }, { danger: true, okLabel: 'Entfernen' }); }); right.appendChild(rm); row.appendChild(right); table.appendChild(row); }); wrap.appendChild(table); /* Freigeben */ wrap.appendChild(el('div', 'wh-subhead', 'Mitarbeiter freigeben')); const candidates = (candRes.ok && candRes.payload && candRes.payload.candidates) || []; if (!candidates.length) { wrap.appendChild(el('div', 'wh-hint', 'Niemand verfügbar. Die Person muss online sein, um freigegeben zu werden.')); return setMain(wrap); } const form = el('div', 'wh-form wh-form-inline'); const pick = el('div', 'wh-field'); pick.appendChild(el('label', 'wh-label', 'Person')); const sel = el('select', 'icweb-input'); candidates.forEach(c => { const o = el('option', null, c.name); o.value = String(c.source); sel.appendChild(o); }); pick.appendChild(sel); form.appendChild(pick); const roleRow = el('div', 'wh-field'); roleRow.appendChild(el('label', 'wh-label', 'Rechte')); const rights = el('select', 'icweb-input'); [['post', 'Schreiben'], ['edit', 'Schreiben und Profil bearbeiten'], ['manage', 'Schreiben, Profil bearbeiten und Leute freigeben']].forEach(([v, l]) => { const o = el('option', null, l); o.value = v; rights.appendChild(o); }); roleRow.appendChild(rights); form.appendChild(roleRow); const add = el('button', 'icweb-btn-primary', 'Freigeben'); add.addEventListener('click', async () => { const level = rights.value; const res = await call('setProfileMember', { profileId: profile.id, targetSource: Number(sel.value), role: level === 'manage' ? 'manager' : 'member', canPost: true, canEditProfile: level === 'edit' || level === 'manage', canManageMembers: level === 'manage', }); if (notify(res, 'Freigabe gespeichert.')) showMembers(profile); }); form.appendChild(add); wrap.appendChild(form); setMain(wrap); } return { open, onResponse }; })();