Feature: QR-Kontingente für Partner — Bestellung, Übergabe, Rückverfolgung
Partner verteilen gedruckte QR-Codes (Sticker/Flyer); jeder physische Code
ist einzeln rückverfolgbar von Scan bis Registrierung.
Backend:
- partner_qr_batches + partner_qr_codes (Token 8-stellig, ohne 0/O/1/l/I),
users.referred_qr, partner_codes.owner_user_id (+Backfill über referred_by)
- /q/{token}: Scan zählen (scans, first/last_scan_at) → Redirect
/?ref=CODE&qr=TOKEN — dockt am bestehenden Referral-Flow an
- Registrierung: qr_token wird nur zugeordnet, wenn er zum eingelösten
Partner-Code gehört (Manipulationsschutz)
- Admin: Kontingent bestellen (max 500), Liste mit Scans/Registrierungen,
Löschen (Zweiklick), druckfertiges A4-PDF (segno+fpdf2, 3×4 Grid mit
Kurz-URL + laufender Nummer), Code-Besitzer zuordnen
- Partner-Self-Service: /partner/my-qr (+PDF) für Code-Besitzer
Frontend:
- Admin-Partner-Tab: Karte 'QR-Kontingente' (Bestellung, Stats, PDF, Besitzer)
- Partner-Profil: 'Meine QR-Codes' mit Scans/Registrierungen + PDF-Download
- boot.js/app.js speichern ?qr=, Registrierung schickt qr_token mit
Neu: segno==1.6.6 (pure-python QR). Tests: 5 neue (PDF, Scan-Zählung,
Attribution, Fremd-Token-Schutz, Self-Service). Suite: 51 passed.
This commit is contained in:
parent
cadfb24a8d
commit
f604ab7c4f
16 changed files with 621 additions and 23 deletions
|
|
@ -36,6 +36,8 @@ window.Page_partner_profil = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
let _qrBatches = [];
|
||||
|
||||
async function _load() {
|
||||
const el = _container.querySelector('#pp-content');
|
||||
try {
|
||||
|
|
@ -43,6 +45,7 @@ window.Page_partner_profil = (() => {
|
|||
_profile = d.profile || {};
|
||||
_profile._storage_mb = d.storage_mb || 0;
|
||||
_profile._storage_limit_mb = d.storage_limit_mb || 200;
|
||||
_qrBatches = (await API.get('/partner/my-qr').catch(() => [])) || [];
|
||||
el.innerHTML = _renderEditor();
|
||||
_bindEvents(el);
|
||||
} catch (e) {
|
||||
|
|
@ -178,6 +181,35 @@ window.Page_partner_profil = (() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${_qrBatches.length ? `
|
||||
<!-- QR-Kontingente: gedruckte Codes mit Scan-/Registrierungs-Stats -->
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
|
||||
<div style="font-size:var(--text-xs);font-weight:700;text-transform:uppercase;
|
||||
letter-spacing:.06em;color:var(--c-text-muted);margin-bottom:var(--space-2)">Meine QR-Codes</div>
|
||||
<p class="text-xs-muted" style="margin:0 0 var(--space-3)">
|
||||
Deine gedruckten QR-Codes (Sticker, Flyer). Jeder Scan und jede Registrierung
|
||||
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>
|
||||
<div style="text-align:center;min-width:54px">
|
||||
<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 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>` : ''}
|
||||
|
||||
<!-- Absenden -->
|
||||
<div style="display:flex;gap:var(--space-3);justify-content:flex-end;margin-top:var(--space-4)">
|
||||
<button id="pp-submit-btn" class="btn btn-primary">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue