Partner-Self-Service: Einzel-Code-Status — welcher Sticker ist verbraucht?

Rene: 'wo sieht der Partner welche QR-Codes er hat und wieviele verbraucht
sind?' Neu in 'Meine QR-Codes':
- Kontingent-Zeile zeigt 'X/Y verbraucht' (Codes mit ≥1 bestätigter Registrierung)
- Listen-Button klappt Einzel-Codes auf: #Nr, Kurz-URL, Scans und Status
  ● verbraucht (mit Registrierungs-Datum) / ◐ gescannt / ○ frei
- Endpoint /partner/my-qr/{id}/codes (owner-gated, keine personenbezogenen
  Daten — nur Zähler + Zeitstempel)
This commit is contained in:
rene 2026-06-07 18:46:54 +02:00
parent 970480c1d6
commit df2f42f8ac
8 changed files with 134 additions and 41 deletions

View file

@ -191,23 +191,34 @@ window.Page_partner_profil = (() => {
darüber wird gezählt so siehst du, was wo funktioniert.
</p>
${_qrBatches.map(b => `
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border)">
<div class="flex-1-min">
<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 style="border-bottom:1px solid var(--c-border)">
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0">
<div class="flex-1-min">
<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)} ·
<span style="color:${b.codes_used > 0 ? 'var(--c-success,#16a34a)' : 'inherit'}">${b.codes_used}/${b.quantity} verbraucht</span>
</div>
</div>
<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: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>
<button class="btn btn-sm btn-ghost pp-qr-codes-btn" data-id="${b.id}" title="Einzel-Codes anzeigen">
${UI.icon('list')}
</button>
<a class="btn btn-sm btn-secondary" href="/api/partner/my-qr/${b.id}/pdf" download>
${UI.icon('file-pdf')} PDF
</a>
</div>
<div style="text-align:center;min-width:48px">
<div style="font-weight:700">${b.scans}</div>
<div class="text-xs-muted">Scans</div>
<div class="hidden" id="pp-qr-codes-${b.id}" style="padding:0 0 var(--space-3)">
<div class="text-sm-muted">Lädt</div>
</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>
${UI.icon('file-pdf')} PDF
</a>
</div>`).join('')}
</div>` : ''}
@ -276,6 +287,35 @@ window.Page_partner_profil = (() => {
});
});
// Einzel-Code-Status eines QR-Kontingents (lazy, .hidden via classList)
el.querySelectorAll('.pp-qr-codes-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const box = el.querySelector(`#pp-qr-codes-${btn.dataset.id}`);
if (!box) return;
box.classList.toggle('hidden');
if (box.classList.contains('hidden') || box.dataset.loaded === '1') return;
try {
const codes = await API.get(`/partner/my-qr/${btn.dataset.id}/codes`);
box.dataset.loaded = '1';
box.innerHTML = codes.map(c => {
const used = c.registrations > 0;
const scanned = c.scans > 0;
return `
<div style="display:flex;align-items:center;gap:var(--space-2);padding:3px 0;font-size:var(--text-xs);border-bottom:1px dashed var(--c-border)">
<span style="font-weight:700;min-width:34px">#${c.seq}</span>
<code class="flex-1-min" style="color:var(--c-text-muted)">banyaro.app/q/${UI.escape(c.token)}</code>
<span class="text-xs-muted" style="min-width:60px;text-align:right">${c.scans} Scan${c.scans === 1 ? '' : 's'}</span>
${used
? `<span class="badge" style="background:#dcfce7;color:#16a34a" title="Registrierung am ${(c.first_registration_at || '').slice(0, 10)}">● verbraucht</span>`
: scanned
? `<span class="badge" style="background:#fef9c3;color:#a16207" title="Gescannt${c.last_scan_at ? ' am ' + c.last_scan_at.slice(0, 10) : ''}, noch keine bestätigte Registrierung">◐ gescannt</span>`
: `<span class="badge" style="background:var(--c-surface-2);color:var(--c-text-muted)">○ frei</span>`}
</div>`;
}).join('');
} catch (err) { UI.toast.error(err.message); }
});
});
// Einreichen
el.querySelector('#pp-submit-btn')?.addEventListener('click', async () => {
const btn = el.querySelector('#pp-submit-btn');