diff --git a/VERSION b/VERSION
index 50a143c..15448f1 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1260
\ No newline at end of file
+1261
\ No newline at end of file
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index c0e7e5a..db5683d 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -466,11 +466,11 @@ def _notify_partner_registration(user_id: int):
+ (f"\n{qr_line}\n" if qr_line else "")
+ f"\nDeine Bilanz mit dem Code {pc['code']}: {total} bestätigte Registrierungen insgesamt, {month} in diesem Monat.\n"
+ (f"{founder_line}\n" if founder_line else "")
- + f"\nDeine Statistik: {_APP_URL}/#partner-profil\n")
+ + f"\nDein Partner-Bereich: {_APP_URL}/#partner-dashboard\n")
try:
from routes.outreach import _send_smtp
from mailer import email_html
- html = email_html(body_html, cta_url=f"{_APP_URL}/#partner-profil", cta_label="Meine Partner-Statistik")
+ html = email_html(body_html, cta_url=f"{_APP_URL}/#partner-dashboard", cta_label="Mein Partner-Bereich")
_send_smtp(pc["owner_email"], subject, plain, "partner", html=html)
except Exception as exc:
_log_smtp_failure(pc["owner_email"], subject, plain, exc, context="partner_thank_you")
diff --git a/backend/routes/partner.py b/backend/routes/partner.py
index 23f0bcd..f7a61df 100644
--- a/backend/routes/partner.py
+++ b/backend/routes/partner.py
@@ -750,6 +750,37 @@ def qr_batch_pdf_admin(batch_id: int, user=Depends(require_admin)):
headers={"Content-Disposition": f'attachment; filename="banyaro-qr-{batch_id}.pdf"'})
+@router.get("/partner/my-stats")
+def my_partner_stats(user=Depends(require_partner)):
+ """Dashboard-Zahlen für den Partner: eigene Codes mit Registrierungen/Versuchen
+ + Status des öffentlichen Profils."""
+ with db() as conn:
+ codes = conn.execute(
+ """SELECT pc.id, pc.code, pc.label, pc.uses, pc.grants_founder,
+ (SELECT COUNT(*) FROM users u
+ WHERE u.referred_by = -pc.id AND u.email_verified = 1) AS registrations,
+ (SELECT COUNT(*) FROM users u
+ WHERE u.referred_by = -pc.id AND u.email_verified = 0) AS attempts,
+ (SELECT COUNT(*) FROM users u
+ WHERE u.referred_by = -pc.id AND u.email_verified = 1
+ AND strftime('%Y-%m', u.created_at) = strftime('%Y-%m', 'now')) AS registrations_month
+ FROM partner_codes pc
+ WHERE pc.owner_user_id = ?
+ ORDER BY pc.created_at""",
+ (user["id"],)
+ ).fetchall()
+ profile = _pp_get_or_empty(conn, user["id"])
+ return {
+ "codes": [dict(c) for c in codes],
+ "profile": {
+ "exists": bool(profile),
+ "approved": profile.get("approved", 0),
+ "submitted_at": profile.get("submitted_at"),
+ "display_name": profile.get("display_name"),
+ },
+ }
+
+
@router.get("/partner/my-qr")
def my_qr_batches(user=Depends(require_partner)):
"""Übergabe/Self-Service: eigene Kontingente mit Stats (Code-Besitzer)."""
diff --git a/backend/static/index.html b/backend/static/index.html
index 4ed0115..af4939e 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,14 +86,14 @@
Ban Yaro
-
+
-
-
-
-
-
+
+
+
+
+
@@ -612,11 +612,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -626,7 +626,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 573ad40..96cee9d 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '1260'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1261'; // ← 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;
@@ -81,6 +81,7 @@ const App = (() => {
gruender: { title: '100 Gründer', module: null },
partner: { title: 'Unsere Partner', module: null },
'partner-profil': { title: 'Partner-Profil', module: null, requiresAuth: true },
+ 'partner-dashboard': { title: 'Partner-Bereich', module: null, requiresAuth: true },
jobs: { title: 'Wir suchen dich', module: null },
expenses: { title: 'Ausgaben', module: null, requiresAuth: true },
recalls: { title: 'Rückrufe', module: null },
diff --git a/backend/static/js/pages/partner-dashboard.js b/backend/static/js/pages/partner-dashboard.js
new file mode 100644
index 0000000..22e21aa
--- /dev/null
+++ b/backend/static/js/pages/partner-dashboard.js
@@ -0,0 +1,212 @@
+/* ============================================================
+ 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 = `
+
+
+
+ ${UI.icon('handshake')} Partner-Bereich
+
+
+ Dein Code, deine Zahlen, deine QR-Kontingente.
+
+
+
+
+ `;
+ }
+
+ 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 = `${UI.escape(e.message || 'Fehler beim Laden.')}
`;
+ }
+ }
+
+ function _renderDashboard() {
+ const codes = _stats?.codes || [];
+ return `
+ ${codes.length === 0 ? `
+
+
+ Dir ist noch kein Partner-Code zugeordnet.
+ Melde dich bei partner@banyaro.app — wir richten ihn ein.
+
+
` : codes.map(c => _renderCodeCard(c)).join('')}
+
+ ${_renderQrSection()}
+ ${_renderProfileCard()}
+ `;
+ }
+
+ function _renderCodeCard(c) {
+ const link = `https://banyaro.app/?ref=${encodeURIComponent(c.code)}`;
+ return `
+
+
Dein Einladungscode
+
+ ${UI.escape(c.code)}
+
+ ${UI.icon('copy')} Link kopieren
+
+
+
+
+
${c.registrations}
+
Registrierungen
+
+
+
${c.registrations_month}
+
diesen Monat
+
+
+
${c.attempts}
+
unbestätigt
+
+
+
`;
+ }
+
+ function _renderQrSection() {
+ if (!_qrBatches.length) return '';
+ return `
+
+
Meine QR-Codes
+
+ Deine gedruckten QR-Codes (Sticker, Flyer). Jeder Scan und jede Registrierung
+ darüber wird gezählt — so siehst du, was wo funktioniert.
+
+ ${_qrBatches.map(b => `
+
+
+
+
${UI.escape(b.label)}
+
+ ${b.quantity} Codes · ${(b.created_at || '').slice(0, 10)} ·
+ ${b.codes_used}/${b.quantity} verbraucht
+
+
+
+
+
${b.registrations}${b.attempts ? ` +${b.attempts} ` : ''}
+
Registr.
+
+
+ ${UI.icon('list')}
+
+
+ ${UI.icon('file-pdf')} PDF
+
+
+
+
`).join('')}
+
`;
+ }
+
+ function _renderProfileCard() {
+ const p = _stats?.profile || {};
+ let badge;
+ if (p.approved === 1) badge = `✓ Öffentlich sichtbar `;
+ else if (p.approved === -1) badge = `✗ Abgelehnt `;
+ else if (p.submitted_at) badge = `⏳ In Prüfung `;
+ else if (p.exists) badge = `Entwurf `;
+ else badge = `Noch nicht angelegt `;
+ return `
+
+
+
+
Öffentliches Profil
+ ${badge}
+
+
+ ${UI.icon('pencil-simple')} Bearbeiten
+
+
+
`;
+ }
+
+ 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;
+ const scanned = c.scans > 0;
+ return `
+
+ #${c.seq}
+ banyaro.app/q/${UI.escape(c.token)}
+ ${c.scans} Scan${c.scans === 1 ? '' : 's'}
+ ${used
+ ? `● verbraucht `
+ : scanned
+ ? `◐ gescannt `
+ : `○ frei `}
+
`;
+ }).join('');
+ } catch (err) { UI.toast.error(err.message); }
+ });
+ });
+
+ el.querySelector('#pd-edit-profile')?.addEventListener('click', () => App.navigate('partner-profil'));
+ }
+
+ return { init, refresh, onDogChange };
+
+})();
diff --git a/backend/static/js/pages/partner-profil.js b/backend/static/js/pages/partner-profil.js
index da834ec..a2bf015 100644
--- a/backend/static/js/pages/partner-profil.js
+++ b/backend/static/js/pages/partner-profil.js
@@ -27,6 +27,8 @@ window.Page_partner_profil = (() => {
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
+ Partner-Bereich .
@@ -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 = (() => {
- ${_qrBatches.length ? `
-
-
-
Meine QR-Codes
-
- Deine gedruckten QR-Codes (Sticker, Flyer). Jeder Scan und jede Registrierung
- darüber wird gezählt — so siehst du, was wo funktioniert.
-
- ${_qrBatches.map(b => `
-
-
-
-
${UI.escape(b.label)}
-
- ${b.quantity} Codes · ${(b.created_at || '').slice(0, 10)} ·
- ${b.codes_used}/${b.quantity} verbraucht
-
-
-
-
-
${b.registrations}${b.attempts ? ` +${b.attempts} ` : ''}
-
Registr.
-
-
- ${UI.icon('list')}
-
-
- ${UI.icon('file-pdf')} PDF
-
-
-
-
`).join('')}
-
` : ''}
-
@@ -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 `
-
- #${c.seq}
- banyaro.app/q/${UI.escape(c.token)}
- ${c.scans} Scan${c.scans === 1 ? '' : 's'}
- ${used
- ? `● verbraucht `
- : scanned
- ? `◐ gescannt `
- : `○ frei `}
-
`;
- }).join('');
- } catch (err) { UI.toast.error(err.message); }
- });
- });
-
// Einreichen
el.querySelector('#pp-submit-btn')?.addEventListener('click', async () => {
const btn = el.querySelector('#pp-submit-btn');
diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js
index 80d3ee5..66d9af0 100644
--- a/backend/static/js/pages/settings.js
+++ b/backend/static/js/pages/settings.js
@@ -672,11 +672,16 @@ window.Page_settings = (() => {
Als Partner hast du vollen Pro-Zugang und eine öffentliche Karte auf der
- Partner-Seite. Richte dein Profil ein — nach der Freigabe ist es für alle sichtbar.
+ Partner-Seite. Deine Zahlen und QR-Codes findest du im Partner-Bereich.
-
- ${UI.icon('pencil-simple')} Mein Partner-Profil
-
+
+
+ ${UI.icon('handshake')} Partner-Bereich
+
+
+ ${UI.icon('pencil-simple')} Öffentliches Profil
+
+
` : ''}
@@ -1676,6 +1681,8 @@ window.Page_settings = (() => {
_loadReferral();
_loadBreederCard();
+ document.getElementById('settings-partner-dashboard-btn')
+ ?.addEventListener('click', () => App.navigate('partner-dashboard'));
document.getElementById('settings-partner-profile-btn')
?.addEventListener('click', () => App.navigate('partner-profil'));
}
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js
index 91fe314..e54f867 100644
--- a/backend/static/js/worlds.js
+++ b/backend/static/js/worlds.js
@@ -578,6 +578,7 @@ window.Worlds = (() => {
{ icon:'sparkle', label:'Social', page:'social', role:'social',
fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] },
{ icon:'shield-check', label:'Moderation', page:'moderation', role:'mod' },
+ { icon:'handshake', label:'Partner', page:'partner-dashboard', role:'partner' },
{ icon:'gear', label:'Admin', page:'admin', role:'admin' },
// ── NEUE FEATURES ────────────────────────────────────────────
{ icon:'fork-knife', label:'Ernährung', page:'ernaehrung', pro: true,
@@ -587,7 +588,7 @@ window.Worlds = (() => {
];
const _DEFAULT_CONFIG = {
- jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','admin'],
+ jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','partner-dashboard','admin'],
hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse',
'litters','zuchthunde','laeufi','ernaehrung','personality'],
welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events',
@@ -681,6 +682,7 @@ window.Worlds = (() => {
}
if (chip.role === 'social') return u?.is_social_media || u?.rolle === 'admin';
if (chip.role === 'mod') return u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator;
+ if (chip.role === 'partner') return !!u?.is_partner || u?.rolle === 'admin';
if (chip.role === 'admin') return u?.rolle === 'admin';
return true;
}
diff --git a/backend/static/landing.html b/backend/static/landing.html
index c57180e..66378b4 100644
--- a/backend/static/landing.html
+++ b/backend/static/landing.html
@@ -4,7 +4,7 @@
-
+
Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 6057213..29d104a 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
-const VER = '1260';
+const VER = '1261';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
diff --git a/tests/test_partner_qr.py b/tests/test_partner_qr.py
index 84fa51e..fe3eed2 100644
--- a/tests/test_partner_qr.py
+++ b/tests/test_partner_qr.py
@@ -215,3 +215,12 @@ def test_partner_self_service_qr(client, admin, user):
assert first["first_registration_at"]
r = client.get("/api/partner/my-qr", headers=user["headers"])
assert r.json()[0]["codes_used"] == 1
+
+ # Dashboard-Stats: eigener Code mit Zahlen + Profil-Status
+ r = client.get("/api/partner/my-stats", headers=user["headers"])
+ assert r.status_code == 200
+ d = r.json()
+ mycode = [c for c in d["codes"] if c["id"] == code["id"]][0]
+ assert mycode["registrations"] == 1
+ assert mycode["registrations_month"] == 1
+ assert "approved" in d["profile"]