Files
BNI-Vorstellungsrunde/admin.html

1479 lines
78 KiB
HTML
Raw Permalink 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>
<link rel="icon" href="favicon.svg" type="image/svg+xml">
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
<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; }
.coord-metric-row { display:grid; grid-template-columns:1fr 90px; gap:8px; align-items:center; padding:5px 0; border-bottom:1px solid var(--border); }
.coord-metric-row:last-child { border-bottom:none; }
.coord-metric-row input { padding:5px 8px; font-size:0.85rem; text-align:right; }
.coord-section-header { font-size:0.72rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:1px; margin:12px 0 6px; border-top:1px solid var(--border); padding-top:10px; }
</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" onclick="switchAdminTab('agenda')">Agenda</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>
<!-- ============================================================ AGENDA -->
<div id="tab-agenda" 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;">Agenda-Folien (Online-Meeting)</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary btn-sm" onclick="fetchAgendaAdmin()">🔄 Vom Server laden</button>
<button class="btn btn-primary btn-sm" onclick="uploadAgendaAdmin()">📤 Auf Server hochladen</button>
</div>
</div>
<p style="font-size:0.85rem;color:var(--muted);margin-bottom:12px;">
Punkte für den Online-Meeting-Ablauf (z.B. Begrüßung, DSGVO, BNI-Zahlen, Events) – als Bild oder als Live-Einbettung
aus Zoho Show. Die Reihenfolge hier ist nur der Katalog; welche Punkte in welcher Reihenfolge live gezeigt werden,
stellt die Moderatorin frei im Steuerpult (index.html) ein.
</p>
<div class="status-msg" id="agenda-status"></div>
<div id="admin-agenda-list" style="margin-top:10px;"><div style="color:var(--muted);font-size:0.88rem;">Lade…</div></div>
<button class="btn btn-secondary btn-sm w-full" style="margin-top:12px;" onclick="openAddAgendaItem()">+ Agenda-Punkt hinzufügen</button>
</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">Mitgliederkoordinator-Bericht</div>
<div style="display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;">
<div style="flex:1;min-width:140px;">
<label>Chapter</label>
<select id="coord-chapter-select" onchange="loadCoordinatorReportData()">
<option value="">— Chapter auswählen —</option>
</select>
</div>
<div style="width:130px;">
<label>BNI-Jahr</label>
<select id="coord-year-select" onchange="loadCoordinatorReportData()">
<option value="2024-2025">2024 / 2025</option>
<option value="2025-2026" selected>2025 / 2026</option>
<option value="2026-2027">2026 / 2027</option>
</select>
</div>
</div>
<div id="coord-metrics-form" style="display:none;">
<!-- Jahresziele -->
<div class="coord-section-header">Jahresziele</div>
<div style="display:grid;grid-template-columns:1fr 90px;gap:8px;padding-bottom:3px;border-bottom:2px solid var(--border);margin-bottom:2px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;">Kennzahl</div>
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;text-align:right;">Jahresziel</div>
</div>
<div id="coord-targets-rows"></div>
<button class="btn btn-secondary btn-sm" onclick="saveCoordinatorTargets()" style="margin-top:8px;">Ziele speichern</button>
<!-- Monat erfassen -->
<div class="coord-section-header">Monatswerte erfassen</div>
<div style="margin-bottom:8px;">
<label>Monat</label>
<select id="coord-month-select" onchange="loadMonthlyEntryForMonth()">
<option value="">— Monat wählen —</option>
</select>
</div>
<div style="display:grid;grid-template-columns:1fr 90px;gap:8px;padding-bottom:3px;border-bottom:2px solid var(--border);margin-bottom:2px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;">Kennzahl</div>
<div style="font-size:0.72rem;font-weight:700;color:var(--muted);text-transform:uppercase;text-align:right;">Ist-Wert</div>
</div>
<div id="coord-monthly-rows"></div>
<div style="display:flex;gap:8px;margin-top:8px;">
<button class="btn btn-primary btn-sm" onclick="saveMonthlyEntry()">Monat speichern</button>
<button class="btn btn-danger btn-sm" id="coord-delete-month-btn" onclick="deleteMonthlyEntry()" style="display:none;">Löschen</button>
</div>
<div id="coord-entries-summary" style="margin-top:8px;font-size:0.78rem;color:var(--muted);"></div>
</div>
<div class="status-msg" id="coord-status"></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>
<!-- Member Photo Modal -->
<div class="modal-overlay" id="modal-member-photo">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="member-photo-modal-title">Foto bearbeiten</div>
<button class="modal-close" onclick="closeAdminModal('modal-member-photo')">✕</button>
</div>
<input type="hidden" id="member-photo-name">
<div style="display:flex;align-items:center;gap:16px;margin:16px 0;">
<img id="member-photo-thumb" class="photo-thumb" style="width:80px;height:80px;display:none;" src="" alt="">
<div id="member-photo-initials" style="width:80px;height:80px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:white;font-size:1.4rem;font-weight:700;flex-shrink:0;"></div>
<div class="photo-actions" style="flex-direction:column;align-items:flex-start;">
<label class="btn-file-label">📁 Foto wählen<input type="file" id="member-photo-file" accept="image/*" style="display:none" onchange="handleMemberPhotoFile(this)"></label>
<button class="btn btn-danger btn-sm" id="member-photo-remove" onclick="removeMemberPhoto()" style="display:none;margin-top:6px;">✕ Foto entfernen</button>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeAdminModal('modal-member-photo')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveMemberPhoto()">Speichern</button>
</div>
</div>
</div>
<!-- Agenda Item Modal -->
<div class="modal-overlay" id="modal-agenda-item">
<div class="modal">
<div class="modal-header">
<div class="modal-title" id="agenda-item-modal-title">Agenda-Punkt</div>
<button class="modal-close" onclick="closeAdminModal('modal-agenda-item')">✕</button>
</div>
<input type="hidden" id="ai-id">
<label>Bezeichnung</label>
<input type="text" id="ai-label" placeholder="z.B. Begrüßung">
<label>Anzeigeart</label>
<select id="ai-type" onchange="updateAgendaItemTypeUI()">
<option value="image">Bild (Export, wird hochgeladen)</option>
<option value="embed">Live-Einbettung (Zoho Show o.ä.)</option>
</select>
<div id="ai-image-row">
<label>Bild</label>
<div class="photo-actions">
<img id="ai-image-thumb" class="photo-thumb" style="width:80px;height:80px;border-radius:8px;display:none;" src="" alt="">
<label class="btn-file-label">📁 Bild wählen<input type="file" id="ai-image-file" accept="image/*" style="display:none" onchange="handleAgendaImageFile(this)"></label>
</div>
<div id="ai-image-thumb-hint" style="display:none;font-size:0.78rem;color:#d97706;margin-top:6px;"></div>
</div>
<div id="ai-embed-row" style="display:none;">
<label>Einbettungs-URL</label>
<input type="text" id="ai-embed-url" placeholder="https://show.zoho.eu/publish/...">
<p style="font-size:0.78rem;color:var(--muted);margin-top:4px;">In Zoho Show: Datei → Freigeben/Veröffentlichen → Einbetten, Link hier einfügen.</p>
</div>
<label>Notizen — nur im Steuerpult sichtbar, nie auf dem Display</label>
<textarea id="ai-notes" rows="4" placeholder="z.B. Stichpunkte, was hier gesagt werden soll"></textarea>
<div class="modal-footer">
<button class="btn btn-danger btn-sm" id="ai-delete-btn" onclick="deleteAgendaItem()" style="display:none;margin-right:auto;">Löschen</button>
<button class="btn btn-ghost" onclick="closeAdminModal('modal-agenda-item')">Abbrechen</button>
<button class="btn btn-primary" onclick="saveAgendaItem()">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 = 'https://git.digiseed.cloud/api/v1/repos/cheubaum/BNI-Vorstellungsrunde/raw';
const GITEA_API = 'https://git.digiseed.cloud/api/v1/repos/cheubaum/BNI-Vorstellungsrunde';
const CHAPTER_CONFIG_KEY = 'bni-active-chapter';
function giteaUrl(path) { return `${GITEA_RAW}/${path}?ref=main`; }
function giteaDataUrl(path) { return `${GITEA_RAW}/${path}?ref=data`; }
function giteaAuthUrl(path, token, ref) { return `${GITEA_API}/contents/${path}?token=${encodeURIComponent(token)}${ref ? '&ref=' + ref : ''}`; }
// ============================================================
// 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 === 'agenda') renderAgendaAdminList();
if (tab === 'superadmin') { loadAllChapters(); loadCurrentVersion(); }
if (tab === 'einstellungen') renderSettings();
}
// ============================================================
// STAMMVERTRETER
// ============================================================
let _svAdminPhotoB64 = null;
function renderSvList() {
const list = loadStammvertreter().sort((a, b) => (a.name || '').localeCompare(b.name || '', 'de', { sensitivity: 'base' }));
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=data`);
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(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
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(giteaDataUrl(`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(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: 'Stammvertreter aktualisiert', content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(giteaAuthUrl(path, token, 'data'), { method: sha?'PUT':'POST', headers: { '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(giteaDataUrl(`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.style.cursor = 'pointer';
div.title = 'Foto bearbeiten';
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)':'#d97706'};">${photoB64?'✓ Foto':'+ Foto'}</span>`;
div.onclick = () => openMemberPhotoModal(m.name, photoB64);
container.appendChild(div);
});
}
let _memberPhotoB64 = null;
function openMemberPhotoModal(name, currentPhotoB64) {
_memberPhotoB64 = currentPhotoB64 || null;
document.getElementById('member-photo-name').value = name;
document.getElementById('member-photo-modal-title').textContent = name;
updateMemberPhotoUI(_memberPhotoB64, name);
openAdminModal('modal-member-photo');
}
function updateMemberPhotoUI(b64, name) {
_memberPhotoB64 = b64;
const thumb = document.getElementById('member-photo-thumb');
const initials = document.getElementById('member-photo-initials');
const removeBtn = document.getElementById('member-photo-remove');
if (b64) {
thumb.src = b64; thumb.style.display = 'block';
initials.style.display = 'none';
removeBtn.style.display = 'inline-flex';
} else {
thumb.src = ''; thumb.style.display = 'none';
initials.style.display = 'flex';
initials.style.background = getColor(name || '');
initials.textContent = getInitials(name || '');
removeBtn.style.display = 'none';
}
}
function handleMemberPhotoFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const name = document.getElementById('member-photo-name').value;
resizePhotoToBase64(file).then(b64 => updateMemberPhotoUI(b64, name)).catch(e => alert(e.message));
input.value = '';
}
function removeMemberPhoto() {
updateMemberPhotoUI(null, document.getElementById('member-photo-name').value);
}
function saveMemberPhoto() {
const name = document.getElementById('member-photo-name').value;
const extras = loadExtras();
const key = extrasKey(name);
if (!extras[key]) extras[key] = {};
if (_memberPhotoB64) {
extras[key].photoB64 = _memberPhotoB64;
} else {
delete extras[key].photoB64;
delete extras[key].offlinePhotoB64;
}
saveExtras(extras);
closeAdminModal('modal-member-photo');
renderMembersList();
}
async function fetchMembersAdmin() {
const statusEl = document.getElementById('members-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaDataUrl(`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)';
try {
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`;
const url = giteaAuthUrl(path, token, 'data');
console.log('[Upload] URL:', url);
statusEl.textContent = 'Prüfe SHA…';
let sha = null;
const shaRes = await fetch(url);
console.log('[Upload] SHA-Status:', shaRes.status);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Mitglieder aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
statusEl.textContent = sha ? 'Aktualisiere…' : 'Erstelle…';
const res = await fetch(url, { method:sha?'PUT':'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
console.log('[Upload] Upload-Status:', res.status);
if (res.ok) { statusEl.textContent = `✓ ${members.length} Mitglieder hochgeladen`; statusEl.style.color = 'var(--success)'; }
else {
const errText = await res.text();
console.error('[Upload] Fehler:', errText);
statusEl.textContent = `Fehler ${res.status}: ${errText.slice(0,80)}`; statusEl.style.color = '#dc2626';
}
} catch(e) {
console.error('[Upload] Exception:', e);
statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626';
}
}
// ============================================================
// AGENDA-FOLIEN (Online-Meeting-Ablauf)
// ============================================================
let _agendaImageDataUrl = null;
let _agendaImageChanged = false;
let _agendaImageProcessing = null; // Promise, solange ein gewähltes Bild noch verkleinert wird
function slugify(s) {
const map = { ä:'ae', ö:'oe', ü:'ue', ß:'ss' };
return (s || '').toLowerCase().trim()
.replace(/[äöüß]/g, c => map[c])
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '') || 'punkt';
}
function renderAgendaAdminList() {
const state = loadState();
const container = document.getElementById('admin-agenda-list');
const items = state.agendaCatalog || [];
if (!items.length) {
container.innerHTML = `<div style="color:var(--muted);font-size:0.88rem;">Noch keine Agenda-Punkte. Mit "+ Agenda-Punkt hinzufügen" starten.</div>`;
return;
}
container.innerHTML = '';
items.forEach(it => {
const div = document.createElement('div');
div.className = 'member-item';
div.style.cursor = 'pointer';
div.title = 'Bearbeiten';
const typeLabel = it.type === 'embed' ? 'Live-Einbettung' : 'Bild';
div.innerHTML = `
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:0.88rem;">${escHtml(it.label || it.id)}</div>
<div style="font-size:0.75rem;color:var(--muted);">${typeLabel}</div>
</div>
<button class="btn btn-ghost btn-sm">✎</button>
`;
div.onclick = () => editAgendaItem(it.id);
container.appendChild(div);
});
}
function openAddAgendaItem() {
document.getElementById('ai-id').value = '';
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt hinzufügen';
document.getElementById('ai-label').value = '';
document.getElementById('ai-type').value = 'image';
document.getElementById('ai-embed-url').value = '';
document.getElementById('ai-notes').value = '';
document.getElementById('ai-delete-btn').style.display = 'none';
_agendaImageDataUrl = null; _agendaImageChanged = false;
document.getElementById('ai-image-thumb').style.display = 'none';
document.getElementById('ai-image-thumb-hint').style.display = 'none';
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function editAgendaItem(id) {
const state = loadState();
const it = (state.agendaCatalog || []).find(a => a.id === id);
if (!it) return;
document.getElementById('ai-id').value = it.id;
document.getElementById('agenda-item-modal-title').textContent = 'Agenda-Punkt bearbeiten';
document.getElementById('ai-label').value = it.label || '';
document.getElementById('ai-type').value = it.type || 'image';
document.getElementById('ai-embed-url').value = it.embedUrl || '';
document.getElementById('ai-notes').value = it.notes || '';
document.getElementById('ai-delete-btn').style.display = 'inline-flex';
_agendaImageDataUrl = null; _agendaImageChanged = false;
const thumb = document.getElementById('ai-image-thumb');
const thumbHint = document.getElementById('ai-image-thumb-hint');
if (it.imagePath) {
thumb.onerror = () => {
thumb.style.display = 'none';
if (thumbHint) { thumbHint.textContent = '⚠ Bild nicht auf dem Server gefunden – bitte neu hochladen.'; thumbHint.style.display = 'block'; }
};
thumb.onload = () => { if (thumbHint) thumbHint.style.display = 'none'; };
if (thumbHint) thumbHint.style.display = 'none';
thumb.src = it.imagePath.startsWith('http') ? it.imagePath : giteaDataUrl(`chapters/${activeChapter.code}/${it.imagePath}`);
thumb.style.display = 'block';
} else {
thumb.style.display = 'none';
if (thumbHint) thumbHint.style.display = 'none';
}
updateAgendaItemTypeUI();
openAdminModal('modal-agenda-item');
}
function updateAgendaItemTypeUI() {
const type = document.getElementById('ai-type').value;
document.getElementById('ai-image-row').style.display = type === 'image' ? 'block' : 'none';
document.getElementById('ai-embed-row').style.display = type === 'embed' ? 'block' : 'none';
}
function handleAgendaImageFile(input) {
const file = input.files && input.files[0];
if (!file) return;
const hint = document.getElementById('ai-image-thumb-hint');
if (hint) { hint.textContent = 'Bild wird verarbeitet…'; hint.style.color = 'var(--muted)'; hint.style.display = 'block'; }
// Auf eine für Vollbild-Anzeige ausreichende Größe verkleinern/komprimieren, damit der
// Upload sicher unter dem Größenlimit der Gitea-Contents-API bleibt (typischerweise ~1 MB/Datei).
// _agendaImageProcessing merkt sich das laufende Promise, damit saveAgendaItem() nicht zu früh
// speichert, während das Bild noch verarbeitet wird (führte sonst zu einem stillen "kein Bild gewählt").
_agendaImageProcessing = resizePhotoToBase64(file, 1920).then(b64 => {
_agendaImageDataUrl = b64;
_agendaImageChanged = true;
const thumb = document.getElementById('ai-image-thumb');
thumb.src = b64;
thumb.style.display = 'block';
if (hint) {
const kb = Math.round((b64.length * 0.75) / 1024);
hint.textContent = `Für den Upload verkleinert auf max. 1920px, ~${kb} KB.`;
hint.style.color = 'var(--muted)';
hint.style.display = 'block';
}
}).catch(() => alert('Bild konnte nicht gelesen werden.'))
.finally(() => { _agendaImageProcessing = null; });
input.value = '';
}
async function uploadAgendaImage(path, dataUrl, token) {
const content = dataUrl.split(',')[1];
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda-Bild aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (!res.ok) { const t = await res.text(); throw new Error(`${res.status}: ${t.slice(0, 80)}`); }
}
async function saveAgendaItem() {
const label = document.getElementById('ai-label').value.trim();
if (!label) { alert('Bitte eine Bezeichnung eingeben.'); return; }
const type = document.getElementById('ai-type').value;
if (_agendaImageProcessing) {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Bild wird noch verarbeitet…'; statusEl.style.color = 'var(--muted)';
await _agendaImageProcessing;
}
const state = loadState();
if (!state.agendaCatalog) state.agendaCatalog = [];
let id = document.getElementById('ai-id').value;
if (!id) {
const base = slugify(label);
const ids = new Set(state.agendaCatalog.map(a => a.id));
let candidate = base, n = 1;
while (ids.has(candidate)) candidate = `${base}-${++n}`;
id = candidate;
}
const existing = state.agendaCatalog.find(a => a.id === id) || {};
const notes = document.getElementById('ai-notes').value.trim();
const item = { id, label, type };
if (notes) item.notes = notes;
if (type === 'embed') {
const url = document.getElementById('ai-embed-url').value.trim();
if (!url) { alert('Bitte eine Einbettungs-URL eingeben.'); return; }
item.embedUrl = url;
} else {
item.imagePath = existing.imagePath || `agenda/${id}.jpg`;
if (!_agendaImageChanged && !existing.imagePath) { alert('Bitte ein Bild wählen.'); return; }
}
const statusEl = document.getElementById('agenda-status');
if (type === 'image' && _agendaImageChanged && _agendaImageDataUrl) {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
statusEl.textContent = 'Lade Bild hoch…'; statusEl.style.color = 'var(--muted)';
try {
await uploadAgendaImage(`chapters/${activeChapter.code}/${item.imagePath}`, _agendaImageDataUrl, token);
} catch(e) {
statusEl.textContent = `Fehler beim Bild-Upload: ${e.message}`; statusEl.style.color = '#dc2626';
alert(`Bild-Upload fehlgeschlagen: ${e.message}\n\nDer Agenda-Punkt wurde NICHT gespeichert.`);
return;
}
}
const idx = state.agendaCatalog.findIndex(a => a.id === id);
if (idx >= 0) state.agendaCatalog[idx] = item; else state.agendaCatalog.push(item);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
statusEl.textContent = '✓ Lokal gespeichert – "Auf Server hochladen" nicht vergessen, damit es im Steuerpult ankommt.';
statusEl.style.color = 'var(--success)';
}
function deleteAgendaItem() {
const id = document.getElementById('ai-id').value;
if (!id) return;
if (!confirm('Diesen Agenda-Punkt wirklich löschen?')) return;
const state = loadState();
state.agendaCatalog = (state.agendaCatalog || []).filter(a => a.id !== id);
saveState(state);
closeAdminModal('modal-agenda-item');
renderAgendaAdminList();
}
async function fetchAgendaAdmin() {
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lade vom Server…'; statusEl.style.color = 'var(--muted)';
try {
const res = await fetch(giteaDataUrl(`chapters/${activeChapter.code}/agenda.json`), { cache: 'no-store' });
if (!res.ok) { statusEl.textContent = 'Noch keine Agenda auf dem Server.'; return; }
const data = await res.json();
const state = loadState();
state.agendaCatalog = Array.isArray(data.items) ? data.items : [];
saveState(state);
statusEl.textContent = `✓ ${state.agendaCatalog.length} Punkte geladen`; statusEl.style.color = 'var(--success)';
renderAgendaAdminList();
} catch(e) { statusEl.textContent = 'Verbindungsfehler.'; statusEl.style.color = '#dc2626'; }
}
async function uploadAgendaAdmin() {
const token = getAdminToken();
if (!token) { alert('Bitte zuerst Admin-Token in den Einstellungen speichern.'); return; }
const statusEl = document.getElementById('agenda-status');
statusEl.textContent = 'Lädt hoch…'; statusEl.style.color = 'var(--muted)';
try {
const state = loadState();
const data = { items: state.agendaCatalog || [] };
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const path = `chapters/${activeChapter.code}/agenda.json`;
const url = giteaAuthUrl(path, token, 'data');
let sha = null;
const shaRes = await fetch(url);
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message: `Agenda aktualisiert ${new Date().toLocaleDateString('de')}`, content, branch: 'data' };
if (sha) body.sha = sha;
const res = await fetch(url, { method: sha ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
if (res.ok) { statusEl.textContent = `✓ ${data.items.length} Agenda-Punkte hochgeladen`; statusEl.style.color = 'var(--success)'; }
else { const t = await res.text(); statusEl.textContent = `Fehler ${res.status}: ${t.slice(0, 80)}`; statusEl.style.color = '#dc2626'; }
} catch(e) { statusEl.textContent = `Fehler: ${e.message}`; statusEl.style.color = '#dc2626'; }
}
// ============================================================
// SUPERADMIN
// ============================================================
let _allChaptersData = [];
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=data`);
const items = await res.json();
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
_allChaptersData = [];
container.innerHTML = '';
for (const ch of chapters) {
try {
const cfgRes = await fetch(giteaDataUrl(`chapters/${ch.name}/config.json`), { cache: 'no-store' });
const cfg = cfgRes.ok ? await cfgRes.json() : {};
const membersRes = await fetch(giteaDataUrl(`chapters/${ch.name}/members.json`));
const membersData = membersRes.ok ? await membersRes.json() : {};
_allChaptersData.push({ code: ch.name, cfg });
const isEnabled = !!cfg.memberCoordinatorReport;
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>
<span style="font-size:0.72rem;padding:2px 8px;border-radius:20px;background:${isEnabled?'#dcfce7':'#f3f4f6'};color:${isEnabled?'#16a34a':'#6b7280'};font-weight:600;white-space:nowrap;">Bericht: ${isEnabled?'Aktiv':'Inaktiv'}</span>
<button class="btn btn-sm ${isEnabled?'btn-ghost':'btn-secondary'}" onclick="toggleCoordinatorReport('${ch.name}', ${isEnabled})" style="white-space:nowrap;font-size:0.75rem;">${isEnabled?'Deaktivieren':'Freischalten'}</button>`;
container.appendChild(div);
} catch(e) { /* skip */ }
}
if (!container.children.length) container.innerHTML = '<div style="color:var(--muted);">Keine Chapter gefunden.</div>';
_populateCoordChapterDropdown();
} 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(giteaAuthUrl('version.json', token));
if (shaRes.ok) sha = (await shaRes.json()).sha;
const body = { message:`Version ${version}`, content };
if (sha) body.sha = sha;
const res = await fetch(giteaAuthUrl('version.json', token), { method:sha?'PUT':'POST', headers:{'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);
}
// ============================================================
// COORDINATOR REPORT
// ============================================================
const _coordMetrics = [
{ key: 'attendance', label: 'Anwesenheit', unit: '%', cumulative: false },
{ key: 'members', label: 'Mitgliederzahl', unit: '', cumulative: false },
{ key: 'newMembers', label: 'Neue Mitglieder', unit: '', cumulative: true },
{ key: 'guests', label: 'Gäste / Besucher', unit: '', cumulative: true },
{ key: 'oneOnOne', label: '4-Augen-Gespräche', unit: '', cumulative: true },
{ key: 'cte', label: 'CTE', unit: '', cumulative: true },
{ key: 'referrals', label: 'Empfehlungen', unit: '', cumulative: true },
{ key: 'revenueThank', label: 'Umsatzdanke', unit: '', cumulative: true },
];
let _coordReportData = null;
function _getBniYearAdmin() {
const now = new Date();
const m = now.getMonth();
const y = now.getFullYear();
return m >= 3 ? `${y}-${y + 1}` : `${y - 1}-${y}`;
}
function _populateCoordChapterDropdown() {
const sel = document.getElementById('coord-chapter-select');
if (!sel) return;
const prev = sel.value;
sel.innerHTML = '<option value="">— Chapter auswählen —</option>';
for (const ch of _allChaptersData) {
if (!ch.cfg.memberCoordinatorReport) continue;
const opt = document.createElement('option');
opt.value = ch.code;
opt.textContent = ch.cfg.chapterName || ch.code;
if (ch.code === prev) opt.selected = true;
sel.appendChild(opt);
}
// Pre-select current BNI year
const ySel = document.getElementById('coord-year-select');
if (ySel && !ySel.value) ySel.value = _getBniYearAdmin();
if (!sel.value) document.getElementById('coord-metrics-form').style.display = 'none';
}
async function toggleCoordinatorReport(code, currentEnabled) {
const token = getAdminToken();
if (!token) { alert('Bitte Admin-Token in den Einstellungen eintragen.'); return; }
try {
const cfgRes = await fetch(giteaDataUrl(`chapters/${code}/config.json`), { cache: 'no-store' });
if (!cfgRes.ok) { alert('Config nicht gefunden.'); return; }
const cfg = await cfgRes.json();
const shaRes = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'));
if (!shaRes.ok) { alert('SHA-Fetch fehlgeschlagen.'); return; }
const sha = (await shaRes.json()).sha;
cfg.memberCoordinatorReport = !currentEnabled;
const content = btoa(unescape(encodeURIComponent(JSON.stringify(cfg, null, 2))));
const res = await fetch(giteaAuthUrl(`chapters/${code}/config.json`, token, 'data'), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: `Koordinatoren-Bericht ${cfg.memberCoordinatorReport ? 'aktiviert' : 'deaktiviert'} für ${code}`, content, sha, branch: 'data' })
});
if (res.ok) { loadAllChapters(); }
else { alert('Fehler beim Speichern der Konfiguration.'); }
} catch(e) { alert('Verbindungsfehler: ' + e.message); }
}
async function loadCoordinatorReportData() {
const code = document.getElementById('coord-chapter-select').value;
const year = document.getElementById('coord-year-select').value;
const form = document.getElementById('coord-metrics-form');
if (!code) { form.style.display = 'none'; _coordReportData = null; return; }
form.style.display = 'block';
_coordReportData = null;
try {
const res = await fetch(giteaDataUrl(`chapters/${code}/coordinator-report-${year}.json`), { cache: 'no-store' });
if (res.ok) _coordReportData = await res.json();
} catch(e) {}
if (!_coordReportData) {
_coordReportData = { year, metrics: _coordMetrics.map(m => ({ key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] })) };
}
// Ensure all metrics exist
for (const m of _coordMetrics) {
if (!_coordReportData.metrics.find(x => x.key === m.key)) {
_coordReportData.metrics.push({ key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] });
}
}
// Drop malformed entries (missing month) to avoid sort/compare crashes downstream
for (const m of _coordReportData.metrics) {
if (Array.isArray(m.entries)) m.entries = m.entries.filter(e => e && e.month);
}
// Render target rows
const targetsContainer = document.getElementById('coord-targets-rows');
targetsContainer.innerHTML = '';
for (const m of _coordMetrics) {
const existing = _coordReportData.metrics.find(x => x.key === m.key) || {};
const row = document.createElement('div');
row.className = 'coord-metric-row';
row.innerHTML = `<div style="font-size:0.88rem;">${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}</div>
<input type="number" id="coord-target-${m.key}" value="${existing.annualTarget || ''}" placeholder="Ziel" min="0" step="any">`;
targetsContainer.appendChild(row);
}
// Populate month dropdown and select current month
_buildMonthDropdown(year);
loadMonthlyEntryForMonth();
updateEntriesSummary();
}
function _bniMonths(year) {
const [y1, y2] = year.split('-').map(Number);
const DE = ['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'];
return [
{ val: `${y1}-04`, label: `April ${y1}` },
{ val: `${y1}-05`, label: `Mai ${y1}` },
{ val: `${y1}-06`, label: `Juni ${y1}` },
{ val: `${y1}-07`, label: `Juli ${y1}` },
{ val: `${y1}-08`, label: `August ${y1}` },
{ val: `${y1}-09`, label: `September ${y1}` },
{ val: `${y1}-10`, label: `Oktober ${y1}` },
{ val: `${y1}-11`, label: `November ${y1}` },
{ val: `${y1}-12`, label: `Dezember ${y1}` },
{ val: `${y2}-01`, label: `Januar ${y2}` },
{ val: `${y2}-02`, label: `Februar ${y2}` },
{ val: `${y2}-03`, label: `März ${y2}` },
];
}
function _buildMonthDropdown(year) {
const sel = document.getElementById('coord-month-select');
if (!sel) return;
const now = new Date();
const currentMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const months = _bniMonths(year);
sel.innerHTML = months.map(m =>
`<option value="${m.val}"${m.val === currentMonth ? ' selected' : ''}>${m.label}</option>`
).join('');
}
function loadMonthlyEntryForMonth() {
const monthStr = document.getElementById('coord-month-select').value;
const container = document.getElementById('coord-monthly-rows');
const deleteBtn = document.getElementById('coord-delete-month-btn');
container.innerHTML = '';
if (!_coordReportData || !monthStr) return;
const existing = {};
for (const m of _coordReportData.metrics) {
const entry = (m.entries || []).find(e => e.month === monthStr);
if (entry) existing[m.key] = entry.value;
}
const hasEntry = Object.keys(existing).length > 0;
if (deleteBtn) deleteBtn.style.display = hasEntry ? '' : 'none';
for (const m of _coordMetrics) {
const row = document.createElement('div');
row.className = 'coord-metric-row';
row.innerHTML = `<div style="font-size:0.88rem;">${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}</div>
<input type="number" id="coord-monthly-${m.key}" value="${existing[m.key] ?? ''}" placeholder="Ist" min="0" step="any">`;
container.appendChild(row);
}
}
function updateEntriesSummary() {
const el = document.getElementById('coord-entries-summary');
if (!_coordReportData || !el) return;
const allMonths = new Set();
for (const m of _coordReportData.metrics) {
for (const e of (m.entries || [])) allMonths.add(e.month);
}
const months = [...allMonths].sort();
el.textContent = months.length === 0
? 'Noch keine Einträge für dieses Jahr.'
: `${months.length} Monat(e) erfasst – zuletzt: ${months[months.length - 1]}`;
}
function _coordStatus(msg, kind) {
const el = document.getElementById('coord-status');
if (!el) return;
el.textContent = msg;
el.style.color = kind === 'error' ? '#dc2626' : kind === 'success' ? 'var(--success)' : 'var(--muted)';
}
async function saveCoordinatorTargets() {
const code = document.getElementById('coord-chapter-select').value;
if (!code) { _coordStatus('✗ Bitte zuerst ein Chapter auswählen.', 'error'); return; }
if (!_coordReportData) { _coordStatus('✗ Bericht nicht geladen – Chapter neu auswählen.', 'error'); return; }
try {
for (const m of _coordMetrics) {
const metric = _coordReportData.metrics.find(x => x.key === m.key);
const inputEl = document.getElementById(`coord-target-${m.key}`);
if (metric && inputEl) metric.annualTarget = parseFloat(inputEl.value) || 0;
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
} catch(e) {
_coordStatus('✗ Fehler: ' + (e && e.message ? e.message : e), 'error');
}
}
async function saveMonthlyEntry() {
const code = document.getElementById('coord-chapter-select').value;
const monthStr = document.getElementById('coord-month-select').value;
if (!code) { _coordStatus('✗ Bitte zuerst ein Chapter auswählen.', 'error'); return; }
if (!monthStr) { _coordStatus('✗ Bitte Monat auswählen.', 'error'); return; }
if (!_coordReportData) { _coordStatus('✗ Bericht nicht geladen – Chapter neu auswählen.', 'error'); return; }
try {
for (const m of _coordMetrics) {
const input = document.getElementById(`coord-monthly-${m.key}`);
if (!input || input.value === '') continue;
const value = parseFloat(input.value);
if (isNaN(value)) continue;
let metric = _coordReportData.metrics.find(x => x.key === m.key);
if (!metric) { metric = { key: m.key, label: m.label, unit: m.unit, annualTarget: 0, entries: [] }; _coordReportData.metrics.push(metric); }
if (!metric.entries) metric.entries = [];
const idx = metric.entries.findIndex(e => e.month === monthStr);
if (idx >= 0) metric.entries[idx].value = value;
else { metric.entries.push({ month: monthStr, value }); metric.entries.sort((a, b) => (a.month || '').localeCompare(b.month || '')); }
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
loadMonthlyEntryForMonth();
updateEntriesSummary();
} catch(e) {
_coordStatus('✗ Fehler: ' + (e && e.message ? e.message : e), 'error');
}
}
async function deleteMonthlyEntry() {
const code = document.getElementById('coord-chapter-select').value;
const monthStr = document.getElementById('coord-month-select').value;
if (!code || !monthStr || !_coordReportData) return;
if (!confirm(`Eintrag für ${monthStr} löschen?`)) return;
for (const m of _coordReportData.metrics) {
m.entries = (m.entries || []).filter(e => e.month !== monthStr);
}
_coordReportData.updatedAt = new Date().toISOString().slice(0, 10);
await _saveCoordinatorReportToGitea(code, _coordReportData);
loadMonthlyEntryForMonth();
updateEntriesSummary();
}
async function _saveCoordinatorReportToGitea(code, data) {
const token = getAdminToken();
if (!token) {
_coordStatus('✗ Admin-Token fehlt – bitte in den Einstellungen eintragen.', 'error');
alert('Bitte Admin-Token in den Einstellungen eintragen.');
return;
}
const year = data.year || document.getElementById('coord-year-select').value;
const path = `chapters/${code}/coordinator-report-${year}.json`;
_coordStatus('Speichere…');
let sha = null;
try {
const shaRes = await fetch(giteaAuthUrl(path, token, 'data'));
if (shaRes.ok) sha = (await shaRes.json()).sha;
} catch(e) {}
const content = btoa(unescape(encodeURIComponent(JSON.stringify(data, null, 2))));
const body = { message: `Update Koordinatoren-Bericht ${year} für ${code}`, content, branch: 'data' };
if (sha) body.sha = sha;
try {
const res = await fetch(giteaAuthUrl(path, token, 'data'), {
method: sha ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
if (res.ok) {
_coordStatus('✓ Gespeichert (' + path + ')', 'success');
setTimeout(() => _coordStatus('', ''), 5000);
} else {
let detail = '';
try { detail = (await res.text()).slice(0, 200); } catch(e) {}
_coordStatus(`✗ Fehler beim Speichern (HTTP ${res.status}). ${detail}`, 'error');
}
} catch(e) {
_coordStatus('✗ Verbindungsfehler: ' + (e && e.message ? e.message : e), 'error');
}
}
// ============================================================
// 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>