Files
BNI-Vorstellungsrunde/admin.html

744 lines
41 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BNI Admin-Panel</title>
<style>
:root {
--red: #cc0000; --red-dark: #990000;
--navy: #1a2744; --bg: #f0f2f5;
--card: #ffffff; --text: #1a1a2e;
--muted: #6b7280; --border: #e5e7eb;
--success: #16a34a; --warning: #d97706;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
.header { background:var(--navy); color:white; padding:14px 24px; display:flex; align-items:center; gap:16px; }
.header img { height:40px; border-radius:6px; object-fit:contain; }
.header-title { font-size:1.1rem; font-weight:700; }
.header-chapter { font-size:0.82rem; color:rgba(255,255,255,0.6); }
.header-back { margin-left:auto; color:rgba(255,255,255,0.7); text-decoration:none; font-size:0.85rem; }
.header-back:hover { color:white; }
.tabs { background:white; border-bottom:1px solid var(--border); display:flex; padding:0 24px; }
.tab { padding:12px 20px; font-size:0.85rem; font-weight:600; color:var(--muted); cursor:pointer; border-bottom:2px solid transparent; }
.tab.active { color:var(--red); border-bottom-color:var(--red); }
.content { padding:24px; max-width:900px; }
.card { background:white; border-radius:12px; border:1px solid var(--border); padding:20px; margin-bottom:16px; }
.card-title { font-weight:700; font-size:0.95rem; margin-bottom:14px; }
.card-subtitle { font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; }
.btn { padding:8px 16px; border-radius:8px; font-size:0.85rem; font-weight:600; cursor:pointer; border:none; display:inline-flex; align-items:center; gap:6px; }
.btn-primary { background:var(--red); color:white; }
.btn-primary:hover { background:var(--red-dark); }
.btn-secondary { background:#f3f4f6; color:var(--text); border:1px solid var(--border); }
.btn-secondary:hover { background:#e5e7eb; }
.btn-ghost { background:transparent; color:var(--muted); border:1px solid var(--border); }
.btn-danger { background:#fee2e2; color:#dc2626; }
.btn-sm { padding:5px 10px; font-size:0.78rem; }
.btn:disabled { opacity:0.5; cursor:not-allowed; }
.w-full { width:100%; justify-content:center; }
label { display:block; font-size:0.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px; margin-top:10px; }
input[type=text], input[type=password], select, textarea {
width:100%; padding:9px 12px; border:1px solid var(--border); border-radius:8px; font-size:0.88rem; outline:none;
}
input:focus, select:focus { border-color:var(--red); }
.form-row { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.sv-item { display:flex; align-items:center; gap:10px; padding:10px 0; border-bottom:1px solid var(--border); }
.sv-photo { width:44px; height:44px; border-radius:50%; object-fit:cover; flex-shrink:0; }
.sv-initials { width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-size:0.8rem; font-weight:700; flex-shrink:0; }
.sv-info { flex:1; min-width:0; }
.sv-name { font-weight:600; font-size:0.9rem; }
.sv-meta { font-size:0.78rem; color:var(--muted); }
.member-item { display:flex; align-items:center; gap:10px; padding:8px 0; border-bottom:1px solid var(--border); }
.chapter-item { padding:10px 14px; background:#f8f9fa; border:1px solid var(--border); border-radius:8px; margin-bottom:8px; display:flex; align-items:center; gap:12px; }
.status-msg { font-size:0.83rem; min-height:20px; margin-top:6px; }
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
.modal-overlay.open { display:flex; }
.modal { background:white; border-radius:16px; padding:24px; width:min(520px,92vw); max-height:90vh; overflow-y:auto; }
.modal-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:16px; }
.modal-title { font-weight:700; font-size:1.05rem; }
.modal-close { background:none; border:none; font-size:1.3rem; cursor:pointer; color:var(--muted); }
.modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:16px; padding-top:12px; border-top:1px solid var(--border); }
.photo-thumb { width:64px; height:64px; border-radius:50%; object-fit:cover; border:2px solid var(--border); }
.photo-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:8px; }
.btn-file-label { display:inline-flex; align-items:center; gap:6px; padding:5px 10px; background:#f3f4f6; border:1px solid var(--border); border-radius:8px; font-size:0.78rem; font-weight:600; cursor:pointer; }
.tab-content { display:none; }
.tab-content.active { display:block; }
.import-chapter-list { max-height:280px; overflow-y:auto; border:1px solid var(--border); border-radius:8px; }
.import-member-row { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); cursor:pointer; }
.import-member-row:hover { background:#f8f9fa; }
.import-member-row:last-child { border-bottom:none; }
</style>
</head>
<body>
<div class="header">
<img id="admin-logo" src="logo.jpeg" alt="BNI">
<div>
<div class="header-title">BNI Admin-Panel</div>
<div class="header-chapter" id="admin-chapter-name">—</div>
</div>
<a href="index.html" class="header-back">← Zurück zur Steuerung</a>
</div>
<div class="tabs">
<div class="tab active" onclick="switchAdminTab('stammvertreter')">Stammvertreter</div>
<div class="tab" onclick="switchAdminTab('mitglieder')">Mitglieder</div>
<div class="tab" id="superadmin-tab" onclick="switchAdminTab('superadmin')" style="display:none;">Superadmin</div>
<div class="tab" onclick="switchAdminTab('einstellungen')">Einstellungen</div>
</div>
<div class="content">
<!-- ============================================================ STAMMVERTRETER -->
<div id="tab-stammvertreter" class="tab-content active">
<div class="card">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;">
<div class="card-title" style="margin:0;">Stammvertreter-Vorrat</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="openImportFromChapter()">⬇ Aus anderem Chapter</button>
<button class="btn btn-primary btn-sm" onclick="openAddSvAdmin()">+ Hinzufügen</button>
</div>
</div>
<div id="admin-sv-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
<div class="card">
<div class="card-title">Gitea-Synchronisation</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Stammvertreter-Liste auf dem Server sichern, damit sie nach einem Browser-Reset wiederherstellbar ist.</p>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="downloadSvFromGitea()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadSvToGitea()">📤 Auf Server sichern</button>
</div>
<div class="status-msg" id="sv-sync-status"></div>
</div>
</div>
<!-- ============================================================ MITGLIEDER -->
<div id="tab-mitglieder" 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;">Mitglieder</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchMembersAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadMembersAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<div class="status-msg" id="members-status"></div>
<div id="admin-members-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
</div>
<!-- ============================================================ SUPERADMIN -->
<div id="tab-superadmin" class="tab-content">
<div class="card">
<div class="card-title">Alle Chapter</div>
<div id="all-chapters-list"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
</div>
<div class="card">
<div class="card-title">Update-Rollout</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Neue Versionsnummer in version.json pushen → alle Nutzer aller Chapter sehen das Update-Banner beim nächsten Start.</p>
<label>Aktuelle Version auf Gitea</label>
<div id="current-version-display" style="font-size:0.88rem;padding:8px 0;color:var(--muted);">—</div>
<label>Neue Version</label>
<div style="display:flex;gap:8px;">
<input type="text" id="new-version-input" placeholder="z.B. 1.2.0" style="flex:1;">
<input type="text" id="new-version-notes" placeholder="Hinweis (optional)" style="flex:2;">
<button class="btn btn-primary btn-sm" onclick="pushVersionUpdate()">Pushen</button>
</div>
<div class="status-msg" id="version-status"></div>
</div>
</div>
<!-- ============================================================ EINSTELLUNGEN -->
<div id="tab-einstellungen" class="tab-content">
<div class="card">
<div class="card-title">Admin-Token (Gitea API)</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:10px;">Wird für alle Uploads auf den Gitea-Server benötigt. Wird nur lokal im Browser gespeichert.</p>
<div style="display:flex;gap:8px;">
<input type="password" id="settings-token" placeholder="Gitea API Token" style="flex:1;">
<button class="btn btn-primary btn-sm" onclick="saveSettingsToken()">Speichern</button>
</div>
<div class="status-msg" id="token-status"></div>
</div>
<div class="card">
<div class="card-title">Chapter-Informationen</div>
<div id="chapter-info-display" style="font-size:0.88rem;color:var(--muted);">Lade…</div>
</div>
</div>
</div><!-- .content -->
<!-- ============================================================ MODALS -->
<!-- SV Add/Edit Modal -->
<div class="modal-overlay" id="modal-sv-admin">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="sv-admin-modal-title">Stammvertreter hinzufügen</div>
<button class="modal-close" onclick="closeAdminModal('modal-sv-admin')">✕</button>
</div>
<input type="hidden" id="sv-admin-id">
<label>Name *</label>
<input type="text" id="sv-admin-name" placeholder="Vor- und Nachname">
<div class="form-row">
<div><label>Firma</label><input type="text" id="sv-admin-company" placeholder="Firma GmbH"></div>
<div><label>Fachgebiet</label><input type="text" id="sv-admin-specialty" placeholder="Fachgebiet"></div>
</div>
<label>Foto</label>
<div style="display:flex;align-items:center;gap:12px;margin-top:8px;">
<img id="sv-admin-photo-thumb" class="photo-thumb" src="" alt="" style="display:none;">
<div class="photo-actions">
<label class="btn-file-label">📁 Datei wählen<input type="file" id="sv-admin-photo-file" accept="image/*" style="display:none" onchange="handleSvAdminPhotoFile(this)"></label>
<button class="btn btn-danger btn-sm" id="sv-admin-remove-photo" onclick="removeSvAdminPhoto()" style="display:none;">✕ Entfernen</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="sv-admin-delete-btn" onclick="deleteSvAdmin()" style="display:none;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-sv-admin')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveSvAdminModal()">Speichern</button>
</div>
</div>
</div>
<!-- Import from Chapter Modal -->
<div class="modal-overlay" id="modal-import-chapter">
<div class="modal">
<div class="modal-header">
<div class="modal-title">Aus anderem Chapter importieren</div>
<button class="modal-close" onclick="closeAdminModal('modal-import-chapter')">✕</button>
</div>
<div id="import-chapter-step1">
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">Chapter auswählen, aus dem du ein Profil übernehmen möchtest:</p>
<div id="import-chapter-select-list"><div style="color:var(--muted);">Lade Chapter…</div></div>
</div>
<div id="import-chapter-step2" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="showImportStep1()" style="margin-bottom:12px;">← Zurück</button>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:8px;">Mitglied auswählen:</p>
<div id="import-member-list" class="import-chapter-list"></div>
</div>
<div class="status-msg" id="import-status"></div>
</div>
</div>
<script>
// ============================================================
// SHARED CONSTANTS (copy from index.html)
// ============================================================
const APP_VERSION = '1.1.0';
const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw';
const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
// ============================================================
// CHAPTER CONFIG
// ============================================================
function loadChapterConfig() {
try { return JSON.parse(localStorage.getItem(CHAPTER_CONFIG_KEY) || 'null'); } catch(e) { return null; }
}
const activeChapter = loadChapterConfig();
// ============================================================
// STORAGE KEYS
// ============================================================
function storageKey() { return activeChapter ? `bni-chapter-${activeChapter.code}-v2` : 'bni-chapter-default-v2'; }
function extrasKey(name) { return (name||'').toLowerCase().trim(); }
function extrasStorageKey() { return activeChapter ? `bni-extras-${activeChapter.code}` : 'bni-profile-extras'; }
function svStorageKey() { return activeChapter ? `bni-stammvertreter-${activeChapter.code}` : 'bni-stammvertreter'; }
function loadState() {
try { return JSON.parse(localStorage.getItem(storageKey()) || 'null') || defaultState(); } catch(e) { return defaultState(); }
}
function saveState(s) { localStorage.setItem(storageKey(), JSON.stringify(s)); }
function defaultState() { return { members:[], meetingOrder:[], currentIndex:-1, substitutes:{}, absentIds:[], medicalIds:[], visitors:[], visitorsOrder:[], visitorsCurrentIndex:-1, mode:'members', defaultTimer:60, memberTimers:{} }; }
function loadExtras() { try { return JSON.parse(localStorage.getItem(extrasStorageKey())||'{}'); } catch(e) { return {}; } }
function saveExtras(e) { localStorage.setItem(extrasStorageKey(), JSON.stringify(e)); }
function loadStammvertreter() { try { return JSON.parse(localStorage.getItem(svStorageKey())||'[]'); } catch(e) { return []; } }
function saveStammvertreter(list) { localStorage.setItem(svStorageKey(), JSON.stringify(list)); }
function getAdminToken() { return localStorage.getItem('bni-gitea-admin-token'); }
// ============================================================
// UTILS
// ============================================================
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function getInitials(n) { if(!n)return'?'; return n.trim().split(/\s+/).map(w=>w[0]).join('').toUpperCase().slice(0,2); }
function getColor(n) { const c=['#1a2744','#cc0000','#0f3460','#1e3a5f','#4a1942']; let h=0; for(let ch of(n||''))h=(h*31+ch.charCodeAt(0))&0xffffffff; return c[Math.abs(h)%c.length]; }
function resizePhotoToBase64(file, maxPx=400) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = e => {
const img = new Image();
img.onload = () => {
let w=img.width, h=img.height;
if(w>maxPx||h>maxPx){ if(w>h){h=Math.round(h*maxPx/w);w=maxPx;}else{w=Math.round(w*maxPx/h);h=maxPx;} }
const canvas=document.createElement('canvas'); canvas.width=w; canvas.height=h;
canvas.getContext('2d').drawImage(img,0,0,w,h);
resolve(canvas.toDataURL('image/jpeg',0.82));
};
img.onerror=reject; img.src=e.target.result;
};
reader.onerror=reject; reader.readAsDataURL(file);
});
}
function avatarHtml(name, photoB64, photoUrl, size=44) {
const src = photoB64 || photoUrl || '';
if (src) return `<img src="${escHtml(src)}" style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;flex-shrink:0;" onerror="this.replaceWith(makeInitialsEl('${escHtml(name)}',${size}))">`;
const el = document.createElement('div');
el.style.cssText = `width:${size}px;height:${size}px;border-radius:50%;background:${getColor(name)};display:flex;align-items:center;justify-content:center;color:white;font-size:${Math.round(size*0.32)}px;font-weight:700;flex-shrink:0;`;
el.textContent = getInitials(name);
return el.outerHTML;
}
function openAdminModal(id) { document.getElementById(id).classList.add('open'); }
function closeAdminModal(id) { document.getElementById(id).classList.remove('open'); }
// ============================================================
// TABS
// ============================================================
function switchAdminTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
document.querySelector(`[onclick="switchAdminTab('${tab}')"]`).classList.add('active');
document.getElementById(`tab-${tab}`).classList.add('active');
if (tab === 'stammvertreter') renderSvList();
if (tab === 'mitglieder') renderMembersList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings();
}
// ============================================================
// STAMMVERTRETER
// ============================================================
let _svAdminPhotoB64 = null;
function renderSvList() {
const list = loadStammvertreter();
const container = document.getElementById('admin-sv-list');
if (!list.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;padding:8px 0;">Noch keine Stammvertreter gespeichert.</div>`;
return;
}
container.innerHTML = '';
list.forEach(sv => {
const extras = loadExtras();
const photoB64 = sv.photoB64 || (extras[extrasKey(sv.name)] && extras[extrasKey(sv.name)].photoB64) || null;
const div = document.createElement('div');
div.className = 'sv-item';
div.innerHTML = `${avatarHtml(sv.name, photoB64, null, 44)}
<div class="sv-info">
<div class="sv-name">${escHtml(sv.name)}</div>
<div class="sv-meta">${escHtml(sv.company||'')}${sv.specialty?' · '+escHtml(sv.specialty):''}</div>
</div>
<button class="btn btn-ghost btn-sm" onclick="openEditSvAdmin('${sv.id}')">✏ Bearbeiten</button>`;
container.appendChild(div);
});
}
function openAddSvAdmin() {
document.getElementById('sv-admin-id').value = '';
document.getElementById('sv-admin-modal-title').textContent = 'Stammvertreter hinzufügen';
document.getElementById('sv-admin-name').value = '';
document.getElementById('sv-admin-company').value = '';
document.getElementById('sv-admin-specialty').value = '';
document.getElementById('sv-admin-delete-btn').style.display = 'none';
_svAdminPhotoB64 = null;
updateSvAdminPhotoUI(null);
openAdminModal('modal-sv-admin');
}
function openEditSvAdmin(svId) {
const sv = loadStammvertreter().find(s => s.id === svId);
if (!sv) return;
document.getElementById('sv-admin-id').value = sv.id;
document.getElementById('sv-admin-modal-title').textContent = 'Stammvertreter bearbeiten';
document.getElementById('sv-admin-name').value = sv.name;
document.getElementById('sv-admin-company').value = sv.company || '';
document.getElementById('sv-admin-specialty').value = sv.specialty || '';
document.getElementById('sv-admin-delete-btn').style.display = '';
_svAdminPhotoB64 = sv.photoB64 || null;
updateSvAdminPhotoUI(_svAdminPhotoB64);
openAdminModal('modal-sv-admin');
}
function updateSvAdminPhotoUI(b64) {
_svAdminPhotoB64 = b64;
const thumb = document.getElementById('sv-admin-photo-thumb');
const btn = document.getElementById('sv-admin-remove-photo');
if (b64) { thumb.src = b64; thumb.style.display = 'block'; btn.style.display = 'inline-flex'; }
else { thumb.src = ''; thumb.style.display = 'none'; btn.style.display = 'none'; }
}
function handleSvAdminPhotoFile(input) {
const file = input.files && input.files[0];
if (!file) return;
resizePhotoToBase64(file).then(b64 => updateSvAdminPhotoUI(b64)).catch(e => alert(e.message));
input.value = '';
}
function removeSvAdminPhoto() { updateSvAdminPhotoUI(null); }
function saveSvAdminModal() {
const name = document.getElementById('sv-admin-name').value.trim();
if (!name) { alert('Bitte Namen eingeben.'); return; }
const list = loadStammvertreter();
const existingId = document.getElementById('sv-admin-id').value;
const sv = {
id: existingId || 'sv-' + Date.now(),
name,
company: document.getElementById('sv-admin-company').value.trim(),
specialty: document.getElementById('sv-admin-specialty').value.trim(),
photoB64: _svAdminPhotoB64 || null,
};
if (existingId) {
const idx = list.findIndex(s => s.id === existingId);
if (idx >= 0) list[idx] = sv; else list.push(sv);
} else {
list.push(sv);
}
saveStammvertreter(list);
closeAdminModal('modal-sv-admin');
renderSvList();
}
function deleteSvAdmin() {
const svId = document.getElementById('sv-admin-id').value;
if (!confirm('Stammvertreter löschen?')) return;
saveStammvertreter(loadStammvertreter().filter(s => s.id !== svId));
closeAdminModal('modal-sv-admin');
renderSvList();
}
// ============================================================
// IMPORT FROM OTHER CHAPTER
// ============================================================
async function openImportFromChapter() {
document.getElementById('import-status').textContent = '';
showImportStep1();
openAdminModal('modal-import-chapter');
const listEl = document.getElementById('import-chapter-select-list');
listEl.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade Chapter…</div>';
try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`);
const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
if (!chapters.length) { listEl.innerHTML = '<div style="color:var(--muted);">Keine anderen Chapter gefunden.</div>'; return; }
listEl.innerHTML = '';
for (const ch of chapters) {
if (activeChapter && ch.name === activeChapter.code) continue;
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`));
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const btn = document.createElement('div');
btn.className = 'chapter-item';
btn.style.cursor = 'pointer';
btn.innerHTML = `<div><div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div><div style="font-size:0.78rem;color:var(--muted);">${ch.name}</div></div><span style="margin-left:auto;color:var(--red);font-size:0.85rem;">Mitglieder laden →</span>`;
btn.onclick = () => loadChapterMembersForImport(ch.name, cfg.chapterName || ch.name);
listEl.appendChild(btn);
} catch(e) { /* skip */ }
}
} catch(e) {
listEl.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden der Chapter.</div>';
}
}
function showImportStep1() {
document.getElementById('import-chapter-step1').style.display = '';
document.getElementById('import-chapter-step2').style.display = 'none';
}
async function loadChapterMembersForImport(code, chapterName) {
document.getElementById('import-chapter-step1').style.display = 'none';
document.getElementById('import-chapter-step2').style.display = '';
const listEl = document.getElementById('import-member-list');
listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Lade Mitglieder…</div>';
try {
const res = await fetch(giteaUrl(`chapters/${code}/members.json`));
if (!res.ok) { listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Keine Mitglieder gefunden.</div>'; return; }
const data = await res.json();
const members = data.members || [];
if (!members.length) { listEl.innerHTML = '<div style="padding:12px;color:var(--muted);">Keine Mitglieder vorhanden.</div>'; return; }
listEl.innerHTML = '';
members.forEach(m => {
const photoB64 = m.photoData || null;
const row = document.createElement('div');
row.className = 'import-member-row';
row.innerHTML = `${avatarHtml(m.name, photoB64, m.photoUrl, 36)}
<div>
<div style="font-weight:600;font-size:0.88rem;">${escHtml(m.name)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${escHtml(m.company||'')}${m.specialty?' · '+escHtml(m.specialty):''}</div>
</div>
<span style="margin-left:auto;font-size:0.75rem;color:var(--muted);">${escHtml(chapterName)}</span>`;
row.onclick = () => importMemberAsStammvertreter(m, chapterName);
listEl.appendChild(row);
});
} catch(e) {
listEl.innerHTML = '<div style="padding:12px;color:#dc2626;">Fehler: ' + e.message + '</div>';
}
}
function importMemberAsStammvertreter(member, chapterName) {
const existing = loadStammvertreter();
if (existing.find(s => s.name.toLowerCase() === member.name.toLowerCase())) {
if (!confirm(`"${member.name}" ist bereits in deiner Liste. Trotzdem importieren?`)) return;
}
const sv = {
id: 'sv-' + Date.now(),
name: member.name,
company: member.company || '',
specialty: member.specialty || '',
photoB64: member.photoData || null,
fromChapter: chapterName,
};
existing.push(sv);
saveStammvertreter(existing);
document.getElementById('import-status').textContent = `✓ "${member.name}" übernommen.`;
document.getElementById('import-status').style.color = 'var(--success)';
renderSvList();
}
// ============================================================
// STAMMVERTRETER GITEA SYNC
// ============================================================
async function uploadSvToGitea() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('sv-sync-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
const list = loadStammvertreter();
const data = { lastUpdated: new Date().toISOString().slice(0,10), substitutes: list.map(s => ({ id:s.id, name:s.name, company:s.company, specialty:s.specialty, photoB64: s.photoB64||null, fromChapter:s.fromChapter||null })) };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/stammvertreter.json`;
try {
let sha = null;
const shaRes = await fetch(`${GITEA_API}/contents/${path}`, { headers: { Authorization: `token ${token}` } });
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: 'Stammvertreter aktualisiert', content };
if (sha) body.sha = sha;
const res = await fetch(`${GITEA_API}/contents/${path}`, { method: sha?'PUT':'POST', headers: { Authorization:`token ${token}`, 'Content-Type':'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${list.length} Stammvertreter gesichert`; statusEl.style.color = 'var(--success)'; }
else { statusEl.textContent = 'Fehler beim Upload.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function downloadSvFromGitea() {
const statusEl = document.getElementById('sv-sync-status');
statusEl.textContent = 'Lade…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/stammvertreter.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Daten auf Server.'; statusEl.style.color = 'var(--muted)'; return; }
const data = await res.json();
if (data.substitutes) { saveStammvertreter(data.substitutes); renderSvList(); statusEl.textContent = `✓ ${data.substitutes.length} Stammvertreter geladen`; statusEl.style.color = 'var(--success)'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// MITGLIEDER
// ============================================================
function renderMembersList() {
const state = loadState();
const extras = loadExtras();
const container = document.getElementById('admin-members-list');
if (!state.members.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Keine Mitglieder vorhanden. Erst Mitglieder über die Steuerung (BNI-Sync) importieren.</div>`;
return;
}
container.innerHTML = '';
state.members.forEach(m => {
const ex = extras[extrasKey(m.name)] || {};
const photoB64 = ex.photoB64 || ex.offlinePhotoB64 || null;
const div = document.createElement('div');
div.className = 'member-item';
div.innerHTML = `${avatarHtml(m.name, photoB64, m.photoUrl, 40)}
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(m.name)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${escHtml(m.company||'')}${m.specialty?' · '+escHtml(m.specialty):''}</div>
</div>
<span style="font-size:0.72rem;color:${photoB64?'var(--success)':'var(--muted)'};">${photoB64?'✓ Foto':'kein Foto'}</span>`;
container.appendChild(div);
});
}
async function fetchMembersAdmin() {
const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Keine Mitglieder auf Server.'; return; }
const data = await res.json();
if (!data.members || !data.members.length) { statusEl.textContent = 'Liste ist leer.'; return; }
const state = loadState();
const extras = loadExtras();
const map = new Map(state.members.map(m => [m.id, m]));
data.members.forEach(m => {
map.set(m.id, Object.assign({}, map.get(m.id)||{}, m));
if (m.photoData) { const key=extrasKey(m.name); if(!extras[key])extras[key]={}; extras[key].photoB64=m.photoData; }
});
state.members = Array.from(map.values());
const known = new Set(state.meetingOrder);
state.members.forEach(m => { if(!known.has(m.id)) state.meetingOrder.push(m.id); });
saveState(state); saveExtras(extras);
statusEl.textContent = `✓ ${data.members.length} Mitglieder geladen`; statusEl.style.color = 'var(--success)';
renderMembersList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadMembersAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
const state = loadState();
const extras = loadExtras();
const members = state.members.map(m => {
const ex = extras[extrasKey(m.name)] || {};
const entry = { id:m.id, name:m.name, company:m.company, specialty:m.specialty, photoUrl:m.photoUrl||'', profileUrl:m.profileUrl||'' };
if (ex.photoB64 || ex.offlinePhotoB64) entry.photoData = ex.photoB64 || ex.offlinePhotoB64;
return entry;
});
const data = { lastUpdated: new Date().toISOString().slice(0,10), members };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/members.json`;
try {
let sha = null;
const shaRes = await fetch(`${GITEA_API}/contents/${path}`, { headers: { Authorization:`token ${token}` } });
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content };
if (sha) body.sha = sha;
const res = await fetch(`${GITEA_API}/contents/${path}`, { method:sha?'PUT':'POST', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'}, body:JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${members.length} Mitglieder hochgeladen`; statusEl.style.color = 'var(--success)'; }
else { statusEl.textContent = 'Fehler beim Upload.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// SUPERADMIN
// ============================================================
async function loadAllChapters() {
const container = document.getElementById('all-chapters-list');
container.innerHTML = '<div style="color:var(--muted);font-size:0.85rem;">Lade…</div>';
try {
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`);
const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
container.innerHTML = '';
for (const ch of chapters) {
try {
const cfgRes = await fetch(giteaUrl(`chapters/${ch.name}/config.json`));
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const membersRes = await fetch(giteaUrl(`chapters/${ch.name}/members.json`));
const membersData = membersRes.ok ? await membersRes.json() : {};
const div = document.createElement('div');
div.className = 'chapter-item';
div.innerHTML = `<div style="flex:1;">
<div style="font-weight:600;">${escHtml(cfg.chapterName||ch.name)}</div>
<div style="font-size:0.78rem;color:var(--muted);">Code: <code>${ch.name}</code> · Mitglieder: ${(membersData.members||[]).length} · Stand: ${membersData.lastUpdated||'—'}</div>
</div>`;
container.appendChild(div);
} catch(e) { /* skip */ }
}
if (!container.children.length) container.innerHTML = '<div style="color:var(--muted);">Keine Chapter gefunden.</div>';
} catch(e) {
container.innerHTML = '<div style="color:#dc2626;">Fehler beim Laden.</div>';
}
}
async function loadCurrentVersion() {
try {
const res = await fetch(giteaUrl('version.json'), { cache:'no-store' });
if (res.ok) {
const data = await res.json();
document.getElementById('current-version-display').textContent = data.version || '—';
document.getElementById('new-version-input').placeholder = data.version ? `z.B. ${data.version.split('.').map((n,i)=>i===2?+n+1:n).join('.')}` : 'z.B. 1.2.0';
}
} catch(e) { /* ignore */ }
}
async function pushVersionUpdate() {
const token = getAdminToken();
if (!token) { alert('Bitte Admin-Token in den Einstellungen eintragen.'); return; }
const version = document.getElementById('new-version-input').value.trim();
const notes = document.getElementById('new-version-notes').value.trim();
if (!version) { alert('Bitte Versionsnummer eingeben.'); return; }
const statusEl = document.getElementById('version-status');
statusEl.textContent = 'Pushe…'; statusEl.style.color = 'var(--muted)';
const content = btoa(unescape(encodeURIComponent(JSON.stringify({ version, notes }, null, 2))));
try {
let sha = null;
const shaRes = await fetch(`${GITEA_API}/contents/version.json`, { headers:{Authorization:`token ${token}`} });
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Version ${version}`, content };
if (sha) body.sha = sha;
const res = await fetch(`${GITEA_API}/contents/version.json`, { method:sha?'PUT':'POST', headers:{Authorization:`token ${token}`,'Content-Type':'application/json'}, body:JSON.stringify(body) });
if (res.ok) {
statusEl.textContent = `✓ Version ${version} gepusht – alle Nutzer sehen das Update-Banner`;
statusEl.style.color = 'var(--success)';
document.getElementById('current-version-display').textContent = version;
} else { statusEl.textContent = 'Fehler beim Pushen.'; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// EINSTELLUNGEN
// ============================================================
function renderSettings() {
const token = getAdminToken();
document.getElementById('settings-token').value = token ? '••••••••' : '';
document.getElementById('settings-token').onfocus = function() { if (this.value==='••••••••') this.value=''; };
if (activeChapter) {
const cfg = activeChapter.config || {};
document.getElementById('chapter-info-display').innerHTML = `
<div><strong>Chapter:</strong> ${escHtml(cfg.chapterName||'—')}</div>
<div><strong>Code:</strong> <code>${activeChapter.code}</code></div>
<div><strong>Version:</strong> ${APP_VERSION}</div>
<div><strong>Aktiviert am:</strong> ${activeChapter.activatedAt ? new Date(activeChapter.activatedAt).toLocaleDateString('de') : '—'}</div>`;
}
}
function saveSettingsToken() {
const val = document.getElementById('settings-token').value;
const realVal = val === '••••••••' ? getAdminToken() : val.trim();
if (realVal) { localStorage.setItem('bni-gitea-admin-token', realVal); document.getElementById('token-status').textContent = '✓ Gespeichert'; document.getElementById('token-status').style.color = 'var(--success)'; }
else { localStorage.removeItem('bni-gitea-admin-token'); document.getElementById('token-status').textContent = 'Token entfernt.'; document.getElementById('token-status').style.color = 'var(--muted)'; }
if (getAdminToken()) document.getElementById('superadmin-tab').style.display = '';
setTimeout(() => { document.getElementById('token-status').textContent = ''; }, 3000);
}
// ============================================================
// INIT
// ============================================================
if (!activeChapter) {
document.body.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100vh;flex-direction:column;gap:16px;color:var(--muted);">
<div style="font-size:1.2rem;font-weight:600;">Kein Chapter aktiviert</div>
<div>Bitte zuerst <a href="index.html" style="color:var(--red);">das Tool öffnen</a> und den Lizenzcode eingeben.</div>
</div>`;
} else {
const cfg = activeChapter.config || {};
document.getElementById('admin-chapter-name').textContent = cfg.chapterName || activeChapter.code;
if (cfg.logoUrl && cfg.logoUrl.startsWith('http')) document.getElementById('admin-logo').src = cfg.logoUrl;
if (getAdminToken()) document.getElementById('superadmin-tab').style.display = '';
renderSvList();
}
</script>
</body>
</html>