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:
291
index.html
291
index.html
@@ -1147,6 +1147,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mitgliederkoordinator-Bericht -->
|
||||
<div id="display-coordinator-report" style="display:none;position:fixed;inset:0;padding:clamp(60px,8vh,100px) clamp(30px,5vw,80px) clamp(24px,4vh,50px);flex-direction:column;gap:clamp(10px,1.8vh,20px);overflow:hidden;z-index:20;">
|
||||
<div style="font-size:clamp(0.9rem,2vw,1.4rem);font-weight:700;color:rgba(255,255,255,0.55);letter-spacing:2px;text-transform:uppercase;">Mitgliederkoordinator-Bericht</div>
|
||||
<div id="coord-report-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:clamp(8px,1.5vw,18px);flex:1;min-height:0;"></div>
|
||||
<div style="font-size:0.7rem;color:rgba(255,255,255,0.25);text-align:right;" id="coord-report-updated"></div>
|
||||
</div>
|
||||
|
||||
<!-- Startbild: außerhalb der Karte, damit es unabhängig angezeigt werden kann -->
|
||||
<div id="display-empty" style="position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:10;">
|
||||
<img id="display-logo-empty" src="logo.jpeg" alt="BNI" style="width:min(520px,72vw);height:auto;border-radius:24px;box-shadow:0 8px 60px rgba(0,0,0,0.5);">
|
||||
@@ -1227,6 +1234,7 @@
|
||||
<button class="btn btn-secondary btn-sm" onclick="openMemberSync()">🔄 Mitglieder aktualisieren</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="openImportExport()">⬇ Import / Export</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="window.open(location.href.split('#')[0] + '#display', '_blank', 'noopener')">🖥 Anzeige öffnen</button>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-coordinator-report" onclick="showCoordinatorReport()" style="display:none;">📊 Koordinatoren-Bericht</button>
|
||||
<button class="btn btn-next" id="btn-next" onclick="goNext()" disabled>
|
||||
Weiter <span id="btn-next-name"></span> →
|
||||
</button>
|
||||
@@ -1757,10 +1765,11 @@ function defaultState() {
|
||||
visitors: [], // { id, name, company, specialty, photoUrl }
|
||||
visitorsOrder: [], // visitor IDs in order
|
||||
visitorsCurrentIndex: -1,
|
||||
mode: 'members', // 'members' | 'absent-summary' | 'visitors'
|
||||
mode: 'members', // 'members' | 'absent-summary' | 'visitors' | 'coordinator-report'
|
||||
defaultTimer: 60, // seconds (standard speaking time)
|
||||
memberTimers: {}, // { memberId: customSeconds }
|
||||
showTimer: true, // false = timer completely hidden
|
||||
coordinatorReportData: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2176,6 +2185,28 @@ function renderDisplay() {
|
||||
const mainCard = document.getElementById('main-card');
|
||||
const displayHeader = document.querySelector('#view-display .display-header');
|
||||
const absentSummaryEl = document.getElementById('display-absent-summary');
|
||||
const coordReportEl = document.getElementById('display-coordinator-report');
|
||||
|
||||
// COORDINATOR REPORT MODE
|
||||
if (state.mode === 'coordinator-report') {
|
||||
emptyEl.style.display = 'none';
|
||||
mainCard.style.display = 'none';
|
||||
contentEl.style.display = 'none';
|
||||
nextBar.style.display = 'none';
|
||||
nextBarEnd.style.display = 'none';
|
||||
roundBadge.classList.remove('visible');
|
||||
progressText.textContent = '';
|
||||
if (displayHeader) displayHeader.style.display = '';
|
||||
if (absentSummaryEl) absentSummaryEl.style.display = 'none';
|
||||
const qrWrap = document.getElementById('qr-wrap');
|
||||
if (qrWrap) qrWrap.style.display = 'none';
|
||||
if (coordReportEl) {
|
||||
coordReportEl.style.display = 'flex';
|
||||
renderCoordinatorReport(state.coordinatorReportData);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (coordReportEl) coordReportEl.style.display = 'none';
|
||||
|
||||
// ABSENT SUMMARY MODE
|
||||
if (state.mode === 'absent-summary') {
|
||||
@@ -2188,6 +2219,9 @@ function renderDisplay() {
|
||||
if (displayHeader) displayHeader.style.display = '';
|
||||
absentSummaryEl.style.display = 'flex';
|
||||
|
||||
const qrWrap = document.getElementById('qr-wrap');
|
||||
if (qrWrap) qrWrap.style.display = 'none';
|
||||
|
||||
const medicalIds = state.medicalIds || [];
|
||||
const medicalMembers = medicalIds.map(id => state.members.find(m => m.id === id)).filter(Boolean);
|
||||
const absentMembers = state.absentIds.map(id => state.members.find(m => m.id === id)).filter(Boolean);
|
||||
@@ -2200,11 +2234,12 @@ function renderDisplay() {
|
||||
const total = medicalMembers.length + absentMembers.length;
|
||||
|
||||
// Scale font size down as list grows
|
||||
const fontSize = total <= 8 ? '1.35rem'
|
||||
: total <= 14 ? '1.1rem'
|
||||
: total <= 22 ? '0.95rem'
|
||||
: total <= 32 ? '0.82rem'
|
||||
: '0.7rem';
|
||||
const fontSize = total <= 4 ? '2.2rem'
|
||||
: total <= 8 ? '1.7rem'
|
||||
: total <= 14 ? '1.35rem'
|
||||
: total <= 22 ? '1.1rem'
|
||||
: total <= 32 ? '0.9rem'
|
||||
: '0.75rem';
|
||||
absentSummaryEl.style.setProperty('--absent-name-size', fontSize);
|
||||
|
||||
// Auto-columns within each section
|
||||
@@ -2546,6 +2581,13 @@ function getDisplayData(presenter) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stammvertreter-Foto: photoB64 is stored in extras keyed by name
|
||||
// (set by pickStammvertreter; see ~line 1948). Use it if nothing else is set.
|
||||
if (!photoUrl) {
|
||||
const ex = loadExtras()[extrasKey(name)];
|
||||
if (ex && ex.photoB64) photoUrl = ex.photoB64;
|
||||
}
|
||||
|
||||
// If still no photo, try matching by name in members list
|
||||
if (!photoUrl) {
|
||||
const match = state.members.find(m =>
|
||||
@@ -2750,8 +2792,9 @@ function renderVisitorQueue() {
|
||||
else if (isNext) badge = `<span class="queue-badge badge-next">Nächster</span>`;
|
||||
else if (isDone) badge = `<span class="queue-badge badge-done">✓</span>`;
|
||||
|
||||
const photoHtml = visitor.photoUrl
|
||||
? `<img src="${escHtml(visitor.photoUrl)}" alt="" style="width:44px;height:44px;border-radius:50%;object-fit:cover;">`
|
||||
const visitorDisplay = getDisplayData(visitor);
|
||||
const photoHtml = visitorDisplay.photoUrl
|
||||
? `<img src="${escHtml(visitorDisplay.photoUrl)}" alt="" style="width:44px;height:44px;border-radius:50%;object-fit:cover;">`
|
||||
: `<div class="queue-initials" style="background:${getColor(visitor.name)}">${getInitials(visitor.name)}</div>`;
|
||||
|
||||
const subTag = visitor.isSubstituteVisitor
|
||||
@@ -2892,6 +2935,12 @@ function updateNextButton() {
|
||||
const nameEl = document.getElementById('btn-next-name');
|
||||
if (!btn) return;
|
||||
|
||||
if (state.mode === 'coordinator-report') {
|
||||
btn.disabled = false;
|
||||
nameEl.textContent = '↩ Schließen';
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.mode === 'round-transition') {
|
||||
btn.disabled = false;
|
||||
nameEl.textContent = '2. Runde';
|
||||
@@ -2952,6 +3001,15 @@ function updateNextButton() {
|
||||
// ============================================================
|
||||
// ACTIONS
|
||||
// ============================================================
|
||||
function _coordReportClose() {
|
||||
state.mode = state._coordPrevMode || 'members';
|
||||
delete state._coordPrevMode;
|
||||
state.coordinatorReportData = null;
|
||||
saveState();
|
||||
renderControl();
|
||||
renderDisplay();
|
||||
}
|
||||
|
||||
function startRoundTransition() {
|
||||
state.mode = 'round-transition';
|
||||
saveState();
|
||||
@@ -2960,6 +3018,10 @@ function startRoundTransition() {
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
if (state.mode === 'coordinator-report') {
|
||||
_coordReportClose();
|
||||
return;
|
||||
}
|
||||
if (state.mode === 'round-transition') {
|
||||
state.mode = 'visitors';
|
||||
state.visitorsCurrentIndex = 0;
|
||||
@@ -4287,6 +4349,8 @@ function applyChapterUI(config) {
|
||||
syncLink.href = config.syncUrl;
|
||||
syncLink.textContent = name + ' – Mitgliederliste';
|
||||
}
|
||||
const coordBtn = document.getElementById('btn-coordinator-report');
|
||||
if (coordBtn) coordBtn.style.display = config.memberCoordinatorReport ? '' : 'none';
|
||||
}
|
||||
|
||||
async function checkLicenseAndUpdates() {
|
||||
@@ -4529,6 +4593,217 @@ function confirmSwitchChapter() {
|
||||
renderCurrentView();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// COORDINATOR REPORT
|
||||
// ============================================================
|
||||
const _COORD_CUMULATIVE = {
|
||||
attendance: false, members: false,
|
||||
newMembers: true, guests: true,
|
||||
oneOnOne: true, cte: true,
|
||||
referrals: true, revenueThank: true,
|
||||
};
|
||||
|
||||
function _getBniYear() {
|
||||
const now = new Date();
|
||||
const m = now.getMonth();
|
||||
const y = now.getFullYear();
|
||||
return m >= 3 ? `${y}-${y + 1}` : `${y - 1}-${y}`;
|
||||
}
|
||||
|
||||
function _monthToIdx(monthStr, y1) {
|
||||
const [y, m] = monthStr.split('-').map(Number);
|
||||
return y === y1 ? m - 4 : m + 8; // Apr=0 … Dec=8, Jan=9 … Mar=11
|
||||
}
|
||||
|
||||
const _MON_DE = ['Jan','Feb','Mär','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'];
|
||||
function _monthAbbr(monthStr) {
|
||||
const m = parseInt((monthStr || '').split('-')[1], 10);
|
||||
return _MON_DE[m - 1] || '';
|
||||
}
|
||||
|
||||
function makeSparklineSvg(entries, annualTarget, yearStr, isCumulative) {
|
||||
const W = 200, H = 56;
|
||||
const y1 = parseInt((yearStr || '2025-2026').split('-')[0]);
|
||||
|
||||
// Today's month index (0–11 in BNI year)
|
||||
const now = new Date();
|
||||
const todayIdx = _monthToIdx(
|
||||
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, y1
|
||||
);
|
||||
const todayX = ((Math.max(0, Math.min(11, todayIdx)) + 0.5) / 12 * W).toFixed(1);
|
||||
|
||||
// Filter to entries with `month` field (new format)
|
||||
const sorted = [...(entries || [])]
|
||||
.filter(e => e.month)
|
||||
.sort((a, b) => a.month.localeCompare(b.month));
|
||||
|
||||
if (sorted.length === 0) {
|
||||
return `<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:100%;" preserveAspectRatio="none">
|
||||
<line x1="${todayX}" y1="0" x2="${todayX}" y2="${H}" stroke="rgba(255,255,255,0.1)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<text x="${W/2}" y="${H/2+4}" text-anchor="middle" fill="rgba(255,255,255,0.2)" font-size="9" font-family="sans-serif">Keine Daten</text></svg>`;
|
||||
}
|
||||
|
||||
const toX = monthStr => ((Math.max(0, Math.min(11, _monthToIdx(monthStr, y1))) + 0.5) / 12 * W);
|
||||
const vals = sorted.map(e => e.value);
|
||||
const yMax = Math.max(...vals, annualTarget || 0, 1) * 1.08;
|
||||
const toY = v => H - (v / yMax) * H * 0.9;
|
||||
|
||||
const pts = sorted.map(e => [toX(e.month), toY(e.value)]);
|
||||
const linePath = pts.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
||||
const areaPath = `${linePath} L${pts[pts.length-1][0].toFixed(1)},${H} L${pts[0][0].toFixed(1)},${H}Z`;
|
||||
|
||||
// Dynamic target line: starts from first actual value, ends at annualTarget in March
|
||||
let targetPath, currentTarget;
|
||||
if (isCumulative) {
|
||||
const firstIdx = _monthToIdx(sorted[0].month, y1);
|
||||
const firstVal = sorted[0].value;
|
||||
const span = 11 - firstIdx;
|
||||
const startX = ((firstIdx + 0.5) / 12 * W).toFixed(1);
|
||||
const endX = ((11.5) / 12 * W).toFixed(1);
|
||||
targetPath = `M${startX},${toY(firstVal).toFixed(1)} L${endX},${toY(annualTarget).toFixed(1)}`;
|
||||
const clampedToday = Math.max(firstIdx, Math.min(11, todayIdx));
|
||||
currentTarget = span > 0
|
||||
? firstVal + (annualTarget - firstVal) * ((clampedToday - firstIdx) / span)
|
||||
: annualTarget;
|
||||
} else {
|
||||
const ty = toY(annualTarget).toFixed(1);
|
||||
targetPath = `M0,${ty} L${W},${ty}`;
|
||||
currentTarget = annualTarget;
|
||||
}
|
||||
|
||||
const latestVal = sorted[sorted.length - 1].value;
|
||||
const onTrack = currentTarget > 0 ? latestVal >= currentTarget : latestVal > 0;
|
||||
const col = onTrack ? '#22c55e' : '#ef4444';
|
||||
const lp = pts[pts.length - 1];
|
||||
|
||||
return `<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:100%;" preserveAspectRatio="none">
|
||||
<path d="${areaPath}" fill="${col}" fill-opacity="0.12"/>
|
||||
<path d="${targetPath}" stroke="rgba(255,255,255,0.28)" stroke-width="1.5" stroke-dasharray="4,3" fill="none"/>
|
||||
<line x1="${todayX}" y1="0" x2="${todayX}" y2="${H}" stroke="rgba(255,255,255,0.12)" stroke-width="1" stroke-dasharray="3,3"/>
|
||||
<path d="${linePath}" stroke="${col}" stroke-width="2.5" fill="none" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="${lp[0].toFixed(1)}" cy="${lp[1].toFixed(1)}" r="3.5" fill="${col}"/>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function renderCoordinatorReport(data) {
|
||||
const grid = document.getElementById('coord-report-grid');
|
||||
const updatedEl = document.getElementById('coord-report-updated');
|
||||
if (!grid) return;
|
||||
if (!data || !data.metrics || !data.metrics.length) {
|
||||
grid.innerHTML = '<div style="color:rgba(255,255,255,0.4);grid-column:1/-1;display:flex;align-items:center;justify-content:center;font-size:1.1rem;">Keine Berichtsdaten vorhanden.</div>';
|
||||
if (updatedEl) updatedEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
const yearStr = data.year || _getBniYear();
|
||||
const y1 = parseInt(yearStr.split('-')[0]);
|
||||
const now = new Date();
|
||||
const todayMonthIdx = _monthToIdx(
|
||||
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, y1
|
||||
);
|
||||
if (updatedEl) updatedEl.textContent = `BNI-Jahr ${yearStr.replace('-', '/')}${data.updatedAt ? ' · Stand: ' + data.updatedAt : ''}`;
|
||||
|
||||
grid.innerHTML = '';
|
||||
for (const m of data.metrics) {
|
||||
const isCumulative = _COORD_CUMULATIVE[m.key] !== false;
|
||||
const annualTarget = m.annualTarget || 0;
|
||||
const entries = m.entries || [];
|
||||
const sorted = [...entries].filter(e => e.month).sort((a, b) => a.month.localeCompare(b.month));
|
||||
const latestEntry = sorted[sorted.length - 1];
|
||||
const latestVal = latestEntry ? latestEntry.value : null;
|
||||
// Dynamic target: linear from first actual value to annualTarget by March
|
||||
let currentTarget, monthlyNeeded = null;
|
||||
if (isCumulative && sorted.length > 0) {
|
||||
const firstVal = sorted[0].value;
|
||||
const firstIdx = _monthToIdx(sorted[0].month, y1);
|
||||
const span = 11 - firstIdx;
|
||||
const clampedToday = Math.max(firstIdx, Math.min(11, todayMonthIdx));
|
||||
currentTarget = span > 0
|
||||
? firstVal + (annualTarget - firstVal) * ((clampedToday - firstIdx) / span)
|
||||
: annualTarget;
|
||||
if (latestVal !== null && annualTarget > 0) {
|
||||
const latestIdx = _monthToIdx(latestEntry.month, y1);
|
||||
const monthsLeft = 11 - latestIdx;
|
||||
if (monthsLeft > 0) monthlyNeeded = (annualTarget - latestVal) / monthsLeft;
|
||||
}
|
||||
} else {
|
||||
currentTarget = annualTarget;
|
||||
}
|
||||
const onTrack = latestVal !== null && currentTarget > 0 ? latestVal >= currentTarget : null;
|
||||
const valColor = onTrack === null ? 'rgba(255,255,255,0.5)' : onTrack ? '#22c55e' : '#ef4444';
|
||||
const progressPct = annualTarget > 0 && latestVal !== null ? Math.round((latestVal / annualTarget) * 100) : null;
|
||||
|
||||
const monthlyHtml = sorted.length > 0
|
||||
? `<div style="display:flex;flex-wrap:wrap;gap:clamp(4px,0.8vw,12px);font-size:clamp(0.55rem,0.85vw,0.78rem);color:rgba(255,255,255,0.7);justify-content:center;line-height:1.25;">
|
||||
${sorted.map(e => `<span><span style="color:rgba(255,255,255,0.35);font-weight:600;">${_monthAbbr(e.month)}</span> <span style="font-weight:700;">${e.value}${m.unit || ''}</span></span>`).join('')}
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
const tile = document.createElement('div');
|
||||
tile.style.cssText = 'background:rgba(255,255,255,0.07);border-radius:12px;padding:clamp(8px,1.2vw,14px);display:flex;flex-direction:column;gap:clamp(4px,0.6vh,8px);overflow:hidden;';
|
||||
tile.innerHTML = `
|
||||
<div style="font-size:clamp(0.5rem,0.8vw,0.7rem);color:rgba(255,255,255,0.45);font-weight:700;text-transform:uppercase;letter-spacing:0.5px;">${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}</div>
|
||||
<div style="flex:1;min-height:0;">${makeSparklineSvg(entries, annualTarget, yearStr, isCumulative)}</div>
|
||||
${monthlyHtml}
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:4px;">
|
||||
<div style="font-size:clamp(0.95rem,2vw,1.7rem);font-weight:700;color:${valColor};">${latestVal !== null ? latestVal + (m.unit || '') : '—'}</div>
|
||||
<div style="font-size:clamp(0.5rem,0.85vw,0.72rem);color:rgba(255,255,255,0.28);text-align:right;line-height:1.3;">
|
||||
Ziel: ${annualTarget}${m.unit || ''}${progressPct !== null ? `<br><span style="color:${valColor};">${progressPct}%</span>` : ''}${monthlyNeeded !== null ? `<br><span style="color:rgba(255,255,255,0.45);">${monthlyNeeded >= 0 ? '+' : ''}${monthlyNeeded.toFixed(1)}/Mo</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
grid.appendChild(tile);
|
||||
}
|
||||
}
|
||||
|
||||
async function showCoordinatorReport() {
|
||||
if (!activeChapter) return;
|
||||
if (state.mode === 'coordinator-report') { _coordReportClose(); return; }
|
||||
const btn = document.getElementById('btn-coordinator-report');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Lade…'; }
|
||||
|
||||
// Try current BNI year and the two preceding ones, plus legacy unsuffixed file.
|
||||
// Pick whichever file has the most recent entry month — protects against
|
||||
// stale data sitting in a year file the user hasn't updated.
|
||||
const currentYear = _getBniYear();
|
||||
const y1 = parseInt(currentYear.split('-')[0]);
|
||||
const candidates = [
|
||||
`coordinator-report-${currentYear}.json`,
|
||||
`coordinator-report-${y1 - 1}-${y1}.json`,
|
||||
`coordinator-report-${y1 - 2}-${y1 - 1}.json`,
|
||||
'coordinator-report.json',
|
||||
];
|
||||
const datas = await Promise.all(candidates.map(async f => {
|
||||
try {
|
||||
const res = await fetch(giteaUrl(`chapters/${activeChapter.code}/${f}`), { cache: 'no-store' });
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch(e) { return null; }
|
||||
}));
|
||||
|
||||
let reportData = null;
|
||||
let bestLatestMonth = '';
|
||||
for (const d of datas) {
|
||||
if (!d || !Array.isArray(d.metrics)) continue;
|
||||
let latestMonth = '';
|
||||
for (const m of d.metrics) {
|
||||
for (const e of (m.entries || [])) {
|
||||
if (e.month && e.month > latestMonth) latestMonth = e.month;
|
||||
}
|
||||
}
|
||||
if (latestMonth && latestMonth > bestLatestMonth) {
|
||||
bestLatestMonth = latestMonth;
|
||||
reportData = d;
|
||||
}
|
||||
}
|
||||
|
||||
if (btn) { btn.disabled = false; btn.textContent = '📊 Koordinatoren-Bericht'; }
|
||||
state._coordPrevMode = state.mode;
|
||||
state.mode = 'coordinator-report';
|
||||
state.coordinatorReportData = reportData;
|
||||
saveState();
|
||||
renderControl();
|
||||
renderDisplay();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// INIT
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user