/**
* 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 };
})();