Feature: Partner-Dashboard (#partner-dashboard) — operative Daten raus aus dem Profil-Editor
Rene: QR-Stats gehören nicht ins öffentliche Profil, eigene Seite fehlte.
Neue Seite 'Partner-Bereich' (Welten-Chip 🤝 zwischen Moderation und Admin,
role:partner — sichtbar für is_partner + Admin; _mergeDefaults reicht den
Chip an bestehende Welt-Configs nach):
- Einladungscode groß + Link-kopieren-Button
- Kacheln: Registrierungen gesamt / diesen Monat / unbestätigt
- QR-Kontingente mit Einzel-Code-Status (aus partner-profil.js hierher verschoben)
- Profil-Status-Karte (Entwurf/Prüfung/frei) mit Sprung zum Editor
Backend: GET /partner/my-stats (Codes mit Zahlen + Profil-Status).
Settings-Partner-Karte: zwei Buttons (Partner-Bereich primär, Profil sekundär);
Dank-Mail-CTA zeigt auf #partner-dashboard. Suite: 52 passed.
This commit is contained in:
parent
3d7d5dc1c4
commit
0a262989f3
12 changed files with 287 additions and 96 deletions
|
|
@ -27,6 +27,8 @@ window.Page_partner_profil = (() => {
|
|||
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin:0">
|
||||
Richte deine öffentliche Präsenz auf der Partner-Seite ein.
|
||||
Nach dem Absenden prüfen wir dein Profil und schalten es frei.
|
||||
Deine Zahlen und QR-Codes findest du im
|
||||
<a href="#partner-dashboard" class="text-primary">Partner-Bereich</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div id="pp-content">
|
||||
|
|
@ -36,8 +38,6 @@ window.Page_partner_profil = (() => {
|
|||
`;
|
||||
}
|
||||
|
||||
let _qrBatches = [];
|
||||
|
||||
async function _load() {
|
||||
const el = _container.querySelector('#pp-content');
|
||||
try {
|
||||
|
|
@ -45,7 +45,6 @@ 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) {
|
||||
|
|
@ -181,47 +180,6 @@ 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="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 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>`).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">
|
||||
|
|
@ -287,35 +245,6 @@ 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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue