diff --git a/admin.html b/admin.html
index 61f63e2..9dde665 100644
--- a/admin.html
+++ b/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; }
@@ -148,6 +152,56 @@
Keine Chapter gefunden.
';
+ _populateCoordChapterDropdown();
} catch(e) {
container.innerHTML = 'Fehler beim Laden.
';
}
@@ -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 = '

@@ -1227,6 +1234,7 @@
+
@@ -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 = `
Nächster`;
else if (isDone) badge = `
✓`;
- const photoHtml = visitor.photoUrl
- ? `
})
`
+ const visitorDisplay = getDisplayData(visitor);
+ const photoHtml = visitorDisplay.photoUrl
+ ? `
})
`
: `
${getInitials(visitor.name)}
`;
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 `
`;
+ }
+
+ 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 `
`;
+}
+
+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 = '
Keine Berichtsdaten vorhanden.
';
+ 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
+ ? `
+ ${sorted.map(e => `${_monthAbbr(e.month)} ${e.value}${m.unit || ''}`).join('')}
+
`
+ : '';
+
+ 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 = `
+
${escHtml(m.label)}${m.unit ? ' (' + m.unit + ')' : ''}
+
${makeSparklineSvg(entries, annualTarget, yearStr, isCumulative)}
+ ${monthlyHtml}
+
+
${latestVal !== null ? latestVal + (m.unit || '') : '—'}
+
+ Ziel: ${annualTarget}${m.unit || ''}${progressPct !== null ? `
${progressPct}%` : ''}${monthlyNeeded !== null ? `
${monthlyNeeded >= 0 ? '+' : ''}${monthlyNeeded.toFixed(1)}/Mo` : ''}
+
+
`;
+ 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
// ============================================================