QR-Stats: Registrierungen (bestätigt) vs. Versuche (unbestätigt) + Account-Detail-Liste

Rene: Statistik zählte alles in einen Topf (3 statt 2) und zeigte nicht,
WER sich registriert hat. Jetzt:
- registrations = email_verified=1, attempts = unbestätigte Versuche —
  Versuche werden bei späterer Bestätigung automatisch zu Registrierungen
- Admin: 👥-Button pro Kontingent klappt Account-Liste auf (Name, E-Mail,
  Datum, ✓ bestätigt/ Versuch, Sticker-Nr #seq) — lazy geladen, admin-only
  (personenbezogene Daten); Partner sehen weiter nur Zahlen (Registr. +N)
- Test deckt Versuch→Bestätigung-Übergang und Detail-Endpoint ab
This commit is contained in:
rene 2026-06-07 18:43:18 +02:00
parent f604ab7c4f
commit 970480c1d6
9 changed files with 110 additions and 26 deletions

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1257'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1258'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -2431,7 +2431,8 @@ window.Page_admin = (() => {
<th style="text-align:left;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)">Kontingent</th>
<th style="text-align:center;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)">Stk.</th>
<th style="text-align:center;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)">Scans</th>
<th style="text-align:center;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)">Registr.</th>
<th style="text-align:center;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)" title="E-Mail bestätigt">Registr.</th>
<th style="text-align:center;padding:var(--space-2) var(--space-3);color:var(--c-text-muted);font-weight:600;font-size:var(--text-xs)" title="Registriert, aber E-Mail (noch) unbestätigt">Versuche</th>
<th style="padding:var(--space-2) var(--space-3)"></th>
</tr>
</thead>
@ -2443,7 +2444,12 @@ window.Page_admin = (() => {
<td style="padding:var(--space-2) var(--space-3);text-align:center">${b.quantity}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-weight:600">${b.scans}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-weight:600;color:${b.registrations > 0 ? 'var(--c-success,#16a34a)' : 'inherit'}">${b.registrations}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:center;color:${b.attempts > 0 ? 'var(--c-warning,#e65100)' : 'var(--c-text-muted)'}">${b.attempts}</td>
<td style="padding:var(--space-2) var(--space-3);text-align:right;white-space:nowrap">
${b.registrations + b.attempts > 0 ? `
<button class="btn btn-ghost btn-sm adm-qr-detail" data-id="${b.id}" title="Accounts anzeigen">
${UI.icon('users')}
</button>` : ''}
<a class="btn btn-sm btn-secondary" href="/api/admin/partner/qr-batches/${b.id}/pdf" download>
${UI.icon('file-pdf')} PDF
</a>
@ -2451,6 +2457,11 @@ window.Page_admin = (() => {
${UI.icon('trash')}
</button>
</td>
</tr>
<tr class="hidden" id="adm-qr-detail-${b.id}">
<td colspan="7" style="padding:0 var(--space-3) var(--space-3);background:var(--c-surface-2)">
<div class="text-sm-muted" style="padding:var(--space-3) 0">Lädt</div>
</td>
</tr>`).join('')}
</tbody>
</table>`}
@ -2585,6 +2596,35 @@ window.Page_admin = (() => {
});
});
// QR-Detail: Accounts hinter einem Kontingent (lazy laden, .hidden via classList)
el.querySelectorAll('.adm-qr-detail').forEach(btn => {
btn.addEventListener('click', async () => {
const row = el.querySelector(`#adm-qr-detail-${btn.dataset.id}`);
if (!row) return;
row.classList.toggle('hidden');
if (row.classList.contains('hidden') || row.dataset.loaded === '1') return;
try {
const regs = await API.get(`/admin/partner/qr-batches/${btn.dataset.id}/registrations`);
row.dataset.loaded = '1';
const cell = row.querySelector('td');
cell.innerHTML = !regs.length
? `<div class="text-sm-muted" style="padding:var(--space-3) 0">Keine Accounts.</div>`
: regs.map(u => `
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border);font-size:var(--text-sm)">
<div class="flex-1-min">
<span style="font-weight:600">${UI.escape(u.name)}</span>
<span class="text-xs-muted">· ${UI.escape(u.email)}</span>
</div>
<span class="text-xs-muted" title="Über welchen Einzel-Code (Sticker-Nr.)">#${u.seq}</span>
<span class="text-xs-muted">${(u.created_at || '').slice(0, 16).replace(' ', ' · ')}</span>
${u.email_verified
? `<span class="badge" style="background:#dcfce7;color:#16a34a">✓ bestätigt</span>`
: `<span class="badge" style="background:#fef9c3;color:#a16207" title="Registriert, E-Mail noch nicht bestätigt">⏳ Versuch</span>`}
</div>`).join('');
} catch (err) { UI.toast.error(err.message); }
});
});
// QR-Kontingent löschen (Zweiklick-Pattern statt confirm — Memory: kein Modal-in-Modal)
el.querySelectorAll('.adm-qr-del').forEach(btn => {
btn.addEventListener('click', async () => {

View file

@ -196,12 +196,13 @@ window.Page_partner_profil = (() => {
<div style="font-weight:600;font-size:var(--text-sm)">${UI.escape(b.label)}</div>
<div class="text-xs-muted">${b.quantity} Codes · ${(b.created_at || '').slice(0, 10)}</div>
</div>
<div style="text-align:center;min-width:54px">
<div style="text-align:center;min-width:48px">
<div style="font-weight:700">${b.scans}</div>
<div class="text-xs-muted">Scans</div>
</div>
<div style="text-align:center;min-width:54px">
<div style="font-weight:700;color:${b.registrations > 0 ? 'var(--c-success,#16a34a)' : 'inherit'}">${b.registrations}</div>
<div style="text-align:center;min-width:48px"
title="Registriert und E-Mail bestätigt${b.attempts ? ` — dazu ${b.attempts} unbestätigte` : ''}">
<div style="font-weight:700;color:${b.registrations > 0 ? 'var(--c-success,#16a34a)' : 'inherit'}">${b.registrations}${b.attempts ? `<span class="text-xs-muted" style="font-weight:400"> +${b.attempts}</span>` : ''}</div>
<div class="text-xs-muted">Registr.</div>
</div>
<a class="btn btn-sm btn-secondary" href="/api/partner/my-qr/${b.id}/pdf" download>