Code-Karte: nur noch Registrierungen gesamt + diesen Monat, mit Hinweis dass alle Wege zählen (Link, eingetippter Code, QR) — erklärt warum Code-Zahl > QR-Zahl sein kann. QR-Kontingente: 'X von Y genutzt' statt Scans/Registr./ Versuche; Einzel-Code-Liste nur noch ● genutzt (N×) / ○ frei. Admin behält die Detail-Sicht (Scans, Versuche, Account-Liste).
198 lines
9.2 KiB
JavaScript
198 lines
9.2 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Partner-Dashboard
|
||
Operative Daten für Partner: Code + Einladungslink, Statistik,
|
||
QR-Kontingente mit Einzel-Code-Status, Profil-Status.
|
||
(Die öffentliche Präsenz wird in partner-profil.js gepflegt.)
|
||
============================================================ */
|
||
|
||
window.Page_partner_dashboard = (() => {
|
||
|
||
let _container = null;
|
||
let _stats = null;
|
||
let _qrBatches = [];
|
||
|
||
async function init(container) {
|
||
_container = container;
|
||
_render();
|
||
await _load();
|
||
}
|
||
|
||
function refresh() { _load(); }
|
||
function onDogChange() {}
|
||
|
||
function _render() {
|
||
_container.innerHTML = `
|
||
<div style="max-width:640px;margin:0 auto;padding:var(--space-4)">
|
||
<div style="margin-bottom:var(--space-5)">
|
||
<h1 style="font-size:var(--text-xl);font-weight:800;margin:0 0 var(--space-1)">
|
||
${UI.icon('handshake')} Partner-Bereich
|
||
</h1>
|
||
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin:0">
|
||
Dein Code, deine Zahlen, deine QR-Kontingente.
|
||
</p>
|
||
</div>
|
||
<div id="pd-content">
|
||
<div style="text-align:center;padding:var(--space-8);color:var(--c-text-muted)">Lade…</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function _load() {
|
||
const el = _container.querySelector('#pd-content');
|
||
try {
|
||
_stats = await API.get('/partner/my-stats');
|
||
_qrBatches = (await API.get('/partner/my-qr').catch(() => [])) || [];
|
||
el.innerHTML = _renderDashboard();
|
||
_bindEvents(el);
|
||
} catch (e) {
|
||
el.innerHTML = `<p class="text-danger">${UI.escape(e.message || 'Fehler beim Laden.')}</p>`;
|
||
}
|
||
}
|
||
|
||
function _renderDashboard() {
|
||
const codes = _stats?.codes || [];
|
||
return `
|
||
${codes.length === 0 ? `
|
||
<div class="card" style="padding:var(--space-5);text-align:center;margin-bottom:var(--space-3)">
|
||
<p class="text-sm-secondary" style="margin:0">
|
||
Dir ist noch kein Partner-Code zugeordnet.<br>
|
||
Melde dich bei <a href="mailto:partner@banyaro.app" class="text-primary">partner@banyaro.app</a> — wir richten ihn ein.
|
||
</p>
|
||
</div>` : codes.map(c => _renderCodeCard(c)).join('')}
|
||
|
||
${_renderQrSection()}
|
||
${_renderProfileCard()}
|
||
`;
|
||
}
|
||
|
||
function _renderCodeCard(c) {
|
||
const link = `https://banyaro.app/?ref=${encodeURIComponent(c.code)}`;
|
||
return `
|
||
<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)">Dein Einladungscode</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3)">
|
||
<code style="font-size:var(--text-lg);font-weight:800;letter-spacing:.1em;color:var(--c-primary)">${UI.escape(c.code)}</code>
|
||
<button class="btn btn-sm btn-secondary pd-copy" data-link="${UI.escape(link)}">
|
||
${UI.icon('copy')} Link kopieren
|
||
</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-2);text-align:center">
|
||
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xl);font-weight:800;color:${c.registrations > 0 ? 'var(--c-success,#16a34a)' : 'var(--c-text)'}">${c.registrations}</div>
|
||
<div class="text-xs-muted">Registrierungen</div>
|
||
</div>
|
||
<div style="background:var(--c-surface-2);border-radius:var(--radius-md);padding:var(--space-3)">
|
||
<div style="font-size:var(--text-xl);font-weight:800">${c.registrations_month}</div>
|
||
<div class="text-xs-muted">diesen Monat</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-xs-muted" style="margin-top:var(--space-2)">
|
||
Zählt alle Wege: geteilter Link, eingetippter Code und deine gedruckten QR-Codes.
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function _renderQrSection() {
|
||
if (!_qrBatches.length) return '';
|
||
return `
|
||
<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) — und wie viele davon schon
|
||
neue Hundefreunde gebracht haben.
|
||
</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)}</div>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<div style="font-weight:700;color:${b.codes_used > 0 ? 'var(--c-success,#16a34a)' : 'inherit'}">${b.codes_used} von ${b.quantity}</div>
|
||
<div class="text-xs-muted" title="Codes mit mindestens einer bestätigten Registrierung">genutzt</div>
|
||
</div>
|
||
<button class="btn btn-sm btn-ghost pd-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="pd-qr-codes-${b.id}" style="padding:0 0 var(--space-3)">
|
||
<div class="text-sm-muted">Lädt…</div>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
}
|
||
|
||
function _renderProfileCard() {
|
||
const p = _stats?.profile || {};
|
||
let badge;
|
||
if (p.approved === 1) badge = `<span class="badge" style="background:#dcfce7;color:#16a34a">✓ Öffentlich sichtbar</span>`;
|
||
else if (p.approved === -1) badge = `<span class="badge" style="background:#fee2e2;color:#dc2626">✗ Abgelehnt</span>`;
|
||
else if (p.submitted_at) badge = `<span class="badge" style="background:#fef9c3;color:#a16207">⏳ In Prüfung</span>`;
|
||
else if (p.exists) badge = `<span class="badge">Entwurf</span>`;
|
||
else badge = `<span class="badge">Noch nicht angelegt</span>`;
|
||
return `
|
||
<div class="card" style="padding:var(--space-4)">
|
||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||
<div class="flex-1-min">
|
||
<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-1)">Öffentliches Profil</div>
|
||
${badge}
|
||
</div>
|
||
<button class="btn btn-sm btn-secondary" id="pd-edit-profile">
|
||
${UI.icon('pencil-simple')} Bearbeiten
|
||
</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function _bindEvents(el) {
|
||
// Einladungslink kopieren
|
||
el.querySelectorAll('.pd-copy').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(btn.dataset.link);
|
||
UI.toast.success('Einladungslink kopiert.');
|
||
} catch {
|
||
UI.toast.info(btn.dataset.link);
|
||
}
|
||
});
|
||
});
|
||
|
||
// Einzel-Code-Status (lazy, .hidden via classList)
|
||
el.querySelectorAll('.pd-qr-codes-btn').forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
const box = el.querySelector(`#pd-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;
|
||
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>
|
||
${used
|
||
? `<span class="badge" style="background:#dcfce7;color:#16a34a" title="Erste Registrierung am ${(c.first_registration_at || '').slice(0, 10)}">● genutzt${c.registrations > 1 ? ` (${c.registrations}×)` : ''}</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); }
|
||
});
|
||
});
|
||
|
||
el.querySelector('#pd-edit-profile')?.addEventListener('click', () => App.navigate('partner-profil'));
|
||
}
|
||
|
||
return { init, refresh, onDogChange };
|
||
|
||
})();
|