Admin: Mitglieder mit Fotos auf Gitea hochladen

This commit is contained in:
2026-04-17 12:45:53 +00:00
parent df4b319b9e
commit 46a26dbc4f

View File

@@ -1668,6 +1668,7 @@
// ============================================================ // ============================================================
const APP_VERSION = '1.1.0'; const APP_VERSION = '1.1.0';
const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Sonne/raw'; const GITEA_RAW = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Sonne/raw';
const GITEA_API = 'http://72.60.134.73:3000/api/v1/repos/cheubaum/BNI-Sonne';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; } function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
const CHAPTER_CONFIG_KEY = 'bni-active-chapter'; const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
@@ -4159,11 +4160,19 @@ async function fetchChapterMembersFromGitea() {
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
if (!data.members || !data.members.length) return; if (!data.members || !data.members.length) return;
const extras = loadExtras();
const mergedMap = new Map(state.members.map(m => [m.id, m])); const mergedMap = new Map(state.members.map(m => [m.id, m]));
data.members.forEach(m => { data.members.forEach(m => {
const ex = mergedMap.get(m.id); const ex = mergedMap.get(m.id);
mergedMap.set(m.id, ex ? Object.assign({}, ex, m) : m); mergedMap.set(m.id, ex ? Object.assign({}, ex, m) : m);
// Embed photoData into extras so existing photo display picks it up
if (m.photoData) {
const key = extrasKey(m.name);
if (!extras[key]) extras[key] = {};
extras[key].offlinePhotoB64 = m.photoData;
}
}); });
saveExtras(extras);
state.members = Array.from(mergedMap.values()); state.members = Array.from(mergedMap.values());
const knownIds = new Set(state.meetingOrder); const knownIds = new Set(state.meetingOrder);
state.members.forEach(m => { if (!knownIds.has(m.id)) state.meetingOrder.push(m.id); }); state.members.forEach(m => { if (!knownIds.has(m.id)) state.meetingOrder.push(m.id); });
@@ -4175,6 +4184,65 @@ async function fetchChapterMembersFromGitea() {
} catch(e) { /* ignore */ } } catch(e) { /* ignore */ }
} }
function buildMembersJson() {
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.offlinePhotoB64) entry.photoData = ex.offlinePhotoB64;
return entry;
});
return { lastUpdated: new Date().toISOString().slice(0, 10), members };
}
function saveAdminToken() {
const val = (document.getElementById('admin-token-input').value || '').trim();
if (val) localStorage.setItem('bni-gitea-admin-token', val);
else localStorage.removeItem('bni-gitea-admin-token');
renderAdminPanel();
}
async function uploadMembersToGitea() {
const token = localStorage.getItem('bni-gitea-admin-token');
if (!token) { alert('Bitte zuerst den Admin-Token speichern.'); return; }
const statusEl = document.getElementById('upload-status');
statusEl.textContent = 'Lädt hoch…';
statusEl.style.color = 'var(--muted)';
const data = buildMembersJson();
const jsonStr = JSON.stringify(data, null, 2);
const content = btoa(unescape(encodeURIComponent(jsonStr)));
const path = `chapters/${activeChapter.code}/members.json`;
try {
// Get current SHA
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) {
activeChapter.lastMemberUpload = new Date().toISOString();
saveChapterConfig(activeChapter);
statusEl.textContent = `✓ ${data.members.length} Mitglieder hochgeladen`;
statusEl.style.color = 'var(--success)';
} else {
statusEl.textContent = 'Fehler beim Upload. Token prüfen.';
statusEl.style.color = '#dc2626';
}
} catch(e) {
statusEl.textContent = 'Verbindungsfehler.';
statusEl.style.color = '#dc2626';
}
}
function renderAdminPanel() { function renderAdminPanel() {
const panel = document.getElementById('admin-panel'); const panel = document.getElementById('admin-panel');
if (!panel) return; if (!panel) return;
@@ -4182,7 +4250,8 @@ function renderAdminPanel() {
const code = activeChapter && activeChapter.code; const code = activeChapter && activeChapter.code;
const lastChecked = activeChapter && activeChapter.lastChecked ? new Date(activeChapter.lastChecked).toLocaleString('de') : '—'; const lastChecked = activeChapter && activeChapter.lastChecked ? new Date(activeChapter.lastChecked).toLocaleString('de') : '—';
const lastSync = activeChapter && activeChapter.lastMemberSync ? new Date(activeChapter.lastMemberSync).toLocaleString('de') : '—'; const lastSync = activeChapter && activeChapter.lastMemberSync ? new Date(activeChapter.lastMemberSync).toLocaleString('de') : '—';
const maskedCode = code ? code.slice(0, -3).replace(/./g, '·') + code.slice(-3) : '—'; const lastUpload = activeChapter && activeChapter.lastMemberUpload ? new Date(activeChapter.lastMemberUpload).toLocaleString('de') : '—';
const hasToken = !!localStorage.getItem('bni-gitea-admin-token');
panel.innerHTML = `<div style="padding:16px;overflow-y:auto;height:100%;"> panel.innerHTML = `<div style="padding:16px;overflow-y:auto;height:100%;">
<div style="font-weight:700;font-size:0.9rem;margin-bottom:14px;">Chapter-Administration</div> <div style="font-weight:700;font-size:0.9rem;margin-bottom:14px;">Chapter-Administration</div>
@@ -4190,13 +4259,26 @@ function renderAdminPanel() {
<div style="background:#f8f9fa;border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:14px;"> <div style="background:#f8f9fa;border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:14px;">
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Aktives Chapter</div> <div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Aktives Chapter</div>
<div style="font-weight:600;margin-bottom:2px;">${cfg.chapterName || '—'}</div> <div style="font-weight:600;margin-bottom:2px;">${cfg.chapterName || '—'}</div>
<div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong></div> <div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong> · Lizenzprüfung: ${lastChecked}</div>
<div style="font-size:0.82rem;color:var(--muted);">Letzte Lizenzprüfung: ${lastChecked}</div>
</div> </div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="fetchChapterMembersFromGitea()">🔄 Mitglieder vom Server laden</button>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzte Synchronisation: ${lastSync}</div> <div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder laden</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="checkForUpdates()">🔍 Auf Updates prüfen</button> <button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:4px;" onclick="fetchChapterMembersFromGitea()">🔄 Vom Server laden</button>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:20px;"></div> <div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Download: ${lastSync}</div>
<div style="font-size:0.78rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Mitglieder hochladen (Admin)</div>
<div style="display:flex;gap:6px;margin-bottom:6px;">
<input id="admin-token-input" type="password" placeholder="Gitea API Token"
value="${hasToken ? '••••••••' : ''}"
style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;font-size:0.82rem;"
onfocus="if(this.value==='••••••••')this.value=''">
<button class="btn btn-secondary btn-sm" onclick="saveAdminToken()">Speichern</button>
</div>
<button class="btn btn-primary btn-sm" style="width:100%;margin-bottom:4px;" onclick="uploadMembersToGitea()">📤 Auf Gitea hochladen (inkl. Fotos)</button>
<div id="upload-status" style="font-size:0.78rem;min-height:18px;margin-bottom:4px;"></div>
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:14px;">Letzter Upload: ${lastUpload}</div>
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:16px;" onclick="checkForUpdates()">🔍 Auf Programm-Updates prüfen</button>
` : `<div style="color:var(--muted);font-size:0.88rem;margin-bottom:20px;">Kein Chapter aktiviert.</div>`} ` : `<div style="color:var(--muted);font-size:0.88rem;margin-bottom:20px;">Kein Chapter aktiviert.</div>`}
<div style="border-top:1px solid var(--border);padding-top:14px;"> <div style="border-top:1px solid var(--border);padding-top:14px;">
<button class="btn btn-ghost btn-sm" style="width:100%;" onclick="confirmSwitchChapter()">↔ Chapter wechseln</button> <button class="btn btn-ghost btn-sm" style="width:100%;" onclick="confirmSwitchChapter()">↔ Chapter wechseln</button>