Admin: Mitglieder inkl. Fotos auf Gitea hochladen
- buildMembersJson() bettet Offline-Fotos (Base64) aus extras ein - uploadMembersToGitea() lädt members.json via API hoch - Admin-Panel: Token-Eingabe + Upload-Button + Status - fetchChapterMembersFromGitea speichert photoData in extras Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
152
index.html
152
index.html
@@ -1336,6 +1336,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
ACTIVATION VIEW
|
||||
============================================================ -->
|
||||
<div id="view-activation" class="view" style="background:linear-gradient(160deg,#0f1f45 0%,#1a2744 40%,#0d1b35 100%);align-items:center;justify-content:center;flex-direction:column;">
|
||||
<img id="activation-logo" src="logo.jpeg" alt="BNI" style="height:80px;margin-bottom:32px;border-radius:8px;object-fit:contain;">
|
||||
<div style="background:rgba(255,255,255,0.08);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:40px;width:min(420px,90vw);color:white;">
|
||||
<div style="font-size:1.4rem;font-weight:700;margin-bottom:6px;">BNI Vorstellungsrunde</div>
|
||||
<div style="font-size:0.88rem;color:rgba(255,255,255,0.6);margin-bottom:24px;">Bitte gib deinen Lizenzcode ein, um dein Chapter freizuschalten.</div>
|
||||
<input id="activation-code-input" type="text" placeholder="chapter-lizenzcode"
|
||||
style="width:100%;padding:12px 14px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);border-radius:10px;color:white;font-size:1rem;outline:none;margin-bottom:12px;letter-spacing:1px;"
|
||||
onkeydown="if(event.key==='Enter')activateChapter()"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
<div id="activation-status" style="font-size:0.85rem;min-height:20px;margin-bottom:12px;"></div>
|
||||
<button id="activation-btn" onclick="activateChapter()"
|
||||
style="width:100%;padding:12px;background:#cc0000;color:white;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;">
|
||||
Aktivieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
MODALS
|
||||
============================================================ -->
|
||||
@@ -1647,7 +1667,9 @@
|
||||
// CONSTANTS & CHAPTER CONFIG
|
||||
// ============================================================
|
||||
const APP_VERSION = '1.1.0';
|
||||
const GITEA_RAW = 'http://72.60.134.73:3000/cheubaum/BNI-Sonne/raw/branch/main';
|
||||
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`; }
|
||||
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
|
||||
|
||||
function loadChapterConfig() {
|
||||
@@ -4009,7 +4031,7 @@ async function activateChapter() {
|
||||
status.textContent = 'Überprüfe Lizenzcode…';
|
||||
status.style.color = 'rgba(255,255,255,0.6)';
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${code}/config.json`, { cache: 'no-store' });
|
||||
const res = await fetch(giteaUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
|
||||
if (!res.ok) {
|
||||
status.textContent = 'Ungültiger oder abgelaufener Lizenzcode.';
|
||||
status.style.color = '#f87171';
|
||||
@@ -4050,7 +4072,7 @@ function applyChapterUI(config) {
|
||||
if (ctrlName) ctrlName.textContent = `${name} – Steuerung`;
|
||||
if (config.logoUrl && activeChapter) {
|
||||
const src = config.logoUrl.startsWith('http') ? config.logoUrl
|
||||
: `${GITEA_RAW}/chapters/${activeChapter.code}/${config.logoUrl}`;
|
||||
: giteaUrl(`chapters/${activeChapter.code}/${config.logoUrl}`);
|
||||
['display-logo','display-logo-empty','ctrl-logo'].forEach(id => {
|
||||
const img = document.getElementById(id);
|
||||
if (img) img.src = src;
|
||||
@@ -4066,7 +4088,7 @@ function applyChapterUI(config) {
|
||||
async function checkLicenseAndUpdates() {
|
||||
if (!activeChapter) return;
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
|
||||
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/config.json`), { cache: 'no-store' });
|
||||
if (!res.ok) {
|
||||
clearChapterData();
|
||||
state = defaultState();
|
||||
@@ -4089,7 +4111,7 @@ async function checkLicenseAndUpdates() {
|
||||
|
||||
async function checkForUpdates() {
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/version.json`, { cache: 'no-store' });
|
||||
const res = await fetch(giteaUrl('version.json'), { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (data.version && data.version > APP_VERSION) showUpdateBanner(data.version, data.notes);
|
||||
@@ -4110,7 +4132,7 @@ function showUpdateBanner(version, notes) {
|
||||
|
||||
async function downloadUpdate(version) {
|
||||
try {
|
||||
const licRes = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/config.json`, { cache: 'no-store' });
|
||||
const licRes = await fetch(giteaUrl(`chapters/${activeChapter.code}/config.json`), { cache: 'no-store' });
|
||||
if (!licRes.ok) {
|
||||
clearChapterData();
|
||||
state = defaultState();
|
||||
@@ -4120,7 +4142,7 @@ async function downloadUpdate(version) {
|
||||
}
|
||||
} catch(e) { alert('Keine Serververbindung möglich.'); return; }
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/index.html`, { cache: 'no-store' });
|
||||
const res = await fetch(giteaUrl('index.html'), { cache: 'no-store' });
|
||||
const html = await res.text();
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const a = document.createElement('a');
|
||||
@@ -4134,15 +4156,23 @@ async function downloadUpdate(version) {
|
||||
async function fetchChapterMembersFromGitea() {
|
||||
if (!activeChapter) return;
|
||||
try {
|
||||
const res = await fetch(`${GITEA_RAW}/chapters/${activeChapter.code}/members.json`, { cache: 'no-store' });
|
||||
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/members.json`), { cache: 'no-store' });
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data.members || !data.members.length) return;
|
||||
const extras = loadExtras();
|
||||
const mergedMap = new Map(state.members.map(m => [m.id, m]));
|
||||
data.members.forEach(m => {
|
||||
const ex = mergedMap.get(m.id);
|
||||
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());
|
||||
const knownIds = new Set(state.meetingOrder);
|
||||
state.members.forEach(m => { if (!knownIds.has(m.id)) state.meetingOrder.push(m.id); });
|
||||
@@ -4154,6 +4184,65 @@ async function fetchChapterMembersFromGitea() {
|
||||
} 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() {
|
||||
const panel = document.getElementById('admin-panel');
|
||||
if (!panel) return;
|
||||
@@ -4161,7 +4250,8 @@ function renderAdminPanel() {
|
||||
const code = activeChapter && activeChapter.code;
|
||||
const lastChecked = activeChapter && activeChapter.lastChecked ? new Date(activeChapter.lastChecked).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%;">
|
||||
<div style="font-weight:700;font-size:0.9rem;margin-bottom:14px;">Chapter-Administration</div>
|
||||
@@ -4169,13 +4259,26 @@ function renderAdminPanel() {
|
||||
<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-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);">Letzte Lizenzprüfung: ${lastChecked}</div>
|
||||
<div style="font-size:0.82rem;color:var(--muted);">Version: <strong>${APP_VERSION}</strong> · Lizenzprüfung: ${lastChecked}</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>
|
||||
<button class="btn btn-secondary btn-sm" style="width:100%;margin-bottom:6px;" onclick="checkForUpdates()">🔍 Auf Updates prüfen</button>
|
||||
<div style="font-size:0.78rem;color:var(--muted);margin-bottom:20px;"></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:4px;" onclick="fetchChapterMembersFromGitea()">🔄 Vom Server laden</button>
|
||||
<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="border-top:1px solid var(--border);padding-top:14px;">
|
||||
<button class="btn btn-ghost btn-sm" style="width:100%;" onclick="confirmSwitchChapter()">↔ Chapter wechseln</button>
|
||||
@@ -4204,25 +4307,6 @@ if (getView() === 'control') {
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- ============================================================
|
||||
ACTIVATION VIEW
|
||||
============================================================ -->
|
||||
<div id="view-activation" class="view" style="background:linear-gradient(160deg,#0f1f45 0%,#1a2744 40%,#0d1b35 100%);align-items:center;justify-content:center;flex-direction:column;">
|
||||
<img id="activation-logo" src="logo.jpeg" alt="BNI" style="height:80px;margin-bottom:32px;border-radius:8px;object-fit:contain;">
|
||||
<div style="background:rgba(255,255,255,0.08);backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.15);border-radius:20px;padding:40px;width:min(420px,90vw);color:white;">
|
||||
<div style="font-size:1.4rem;font-weight:700;margin-bottom:6px;">BNI Vorstellungsrunde</div>
|
||||
<div style="font-size:0.88rem;color:rgba(255,255,255,0.6);margin-bottom:24px;">Bitte gib deinen Lizenzcode ein, um dein Chapter freizuschalten.</div>
|
||||
<input id="activation-code-input" type="text" placeholder="chapter-lizenzcode"
|
||||
style="width:100%;padding:12px 14px;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);border-radius:10px;color:white;font-size:1rem;outline:none;margin-bottom:12px;letter-spacing:1px;"
|
||||
onkeydown="if(event.key==='Enter')activateChapter()"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
||||
<div id="activation-status" style="font-size:0.85rem;min-height:20px;margin-bottom:12px;"></div>
|
||||
<button id="activation-btn" onclick="activateChapter()"
|
||||
style="width:100%;padding:12px;background:#cc0000;color:white;border:none;border-radius:10px;font-size:1rem;font-weight:700;cursor:pointer;">
|
||||
Aktivieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user