Mitgliederkoordinator-Bericht + Anzeige-Verbesserungen
Anzeige (index.html): - Mitgliederkoordinator-Bericht-Modus: 4x2 Kacheln mit Jahreszielen, Sparkline-Trend und Monatswerte-Reihe pro Kennzahl; jüngste Jahres-JSON-Datei wird automatisch gewählt - QR-Code wird auf Abwesenheits- und Bericht-Anzeige ausgeblendet (war zuvor vom letzten Mitglied übrig) - Abwesenheits-Namen skalieren dynamisch (2.2rem bei wenigen Einträgen bis 0.75rem bei vollen Listen) - Fotos für Besucher und Vertreter in Runde 2: getDisplayData fällt für Besucher zusätzlich auf loadExtras() zurück, damit Stammvertreter-Fotos (photoB64) erscheinen; Besucher-Queue nutzt dieselbe Fallback-Kette Admin (admin.html): - Karte "Mitgliederkoordinator-Bericht": Jahresziele und Monatswerte je Chapter und BNI-Jahr erfassen - _coordStatus()-Helfer für einheitliches Speicher-Feedback; explizite Fehlermeldungen bei fehlendem Chapter/Monat, HTTP-Detail bei Server-Fehlern, fehlendes Admin-Token auch im Statusfeld - Sort-Crash bei alten entries-Datensätzen ohne month-Feld behoben (null-sicher) + Bereinigung beim Laden Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
337
admin.html
337
admin.html
@@ -82,6 +82,10 @@
|
||||
.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>
|
||||
@@ -148,6 +152,56 @@
|
||||
<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>
|
||||
@@ -729,6 +783,8 @@ async function uploadMembersAdmin() {
|
||||
// ============================================================
|
||||
// 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>';
|
||||
@@ -736,6 +792,7 @@ async function loadAllChapters() {
|
||||
const res = await fetch(`${GITEA_API}/contents/chapters?ref=main`);
|
||||
const items = await res.json();
|
||||
const chapters = Array.isArray(items) ? items.filter(i => i.type === 'dir') : [];
|
||||
_allChaptersData = [];
|
||||
container.innerHTML = '';
|
||||
for (const ch of chapters) {
|
||||
try {
|
||||
@@ -743,16 +800,21 @@ async function loadAllChapters() {
|
||||
const cfg = cfgRes.ok ? await cfgRes.json() : {};
|
||||
const membersRes = await fetch(giteaUrl(`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>`;
|
||||
</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>';
|
||||
}
|
||||
@@ -819,6 +881,279 @@ function saveSettingsToken() {
|
||||
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(giteaUrl(`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));
|
||||
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), {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: `Koordinatoren-Bericht ${cfg.memberCoordinatorReport ? 'aktiviert' : 'deaktiviert'} für ${code}`, content, sha })
|
||||
});
|
||||
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(giteaUrl(`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));
|
||||
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 };
|
||||
if (sha) body.sha = sha;
|
||||
try {
|
||||
const res = await fetch(giteaAuthUrl(path, token), {
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user