diff --git a/MARKETING.md b/MARKETING.md index 72078eb..d232fe2 100644 --- a/MARKETING.md +++ b/MARKETING.md @@ -14,8 +14,7 @@ _Stand: 2026-06-03_ | Lokal (Ebersberg) | ⬜ offen | Tierärzte, Hundeschulen, Futterläden, Tierheim | | Online-Communities | ⬜ offen | FB-Gruppen Landkreis EBE + nebenan.de | | Empfehlung / Referral | 🟡 Infra da (`referral_code`) | Empfehlungs-QR + Tracking sichtbar machen | -| Partner-Programm | 🟢 Infra komplett (v1265, 07.06.) | Partner einladen! Showcase `#partner`, Pro gratis, Partner-Dashboard, QR-Kontingente (Druck-PDF) mit Einzel-Code-Tracking, Dank-Mails mit Statistik, Pause-Notbremse für geleakte Codes. Onboarding: Admin → Code anlegen → Partner-Badge → Besitzer zuordnen | -| Influencer | 🟡 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern — jetzt mit Partner-Paket als konkretem Angebot | +| Influencer | 🟡 2 Runden (Mai), kaum Resonanz | Runde 3 erst ab ~50 aktiven Usern | | Presse / Blogs | 🟡 1 Runde, kaum Resonanz | keine Massenwelle; Nische zuerst | | Verzeichnisse / Listings | ⬜ offen | Product Hunt, PWA-Dirs, Google Business EBE | | SEO / KI-Auffindbarkeit | 🟡 technisch optimiert | Backlinks (Blog-Testberichte) | diff --git a/VERSION b/VERSION index 4c8735e..3420149 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1266 \ No newline at end of file +1265 \ No newline at end of file diff --git a/backend/routes/partner.py b/backend/routes/partner.py index 7690352..115517b 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -48,28 +48,6 @@ def list_partner_codes(user=Depends(require_admin)): return [dict(r) for r in rows] -@router.get("/admin/partner/codes/{code_id}/registrations") -def code_registrations(code_id: int, user=Depends(require_admin)): - """ALLE Einlösungen eines Partner-Codes — mit Kanal (QR-Sticker vs. Link/manuell). - Admin-only (personenbezogene Daten).""" - with db() as conn: - if not conn.execute( - "SELECT id FROM partner_codes WHERE id=?", (code_id,) - ).fetchone(): - raise HTTPException(404, "Partner-Code nicht gefunden.") - rows = conn.execute( - """SELECT u.id, u.name, u.email, u.email_verified, u.created_at, - q.seq AS qr_seq, b.label AS qr_batch_label - FROM users u - LEFT JOIN partner_qr_codes q ON q.token = u.referred_qr - LEFT JOIN partner_qr_batches b ON b.id = q.batch_id - WHERE u.referred_by = ? - ORDER BY u.created_at DESC""", - (-code_id,) - ).fetchall() - return [dict(r) for r in rows] - - @router.post("/admin/partner/codes/{code_id}/toggle") def toggle_partner_code(code_id: int, user=Depends(require_admin)): """Notbremse: Code pausieren/reaktivieren (z. B. wenn er im Internet kursiert). diff --git a/backend/static/index.html b/backend/static/index.html index 80ab2df..4d30aec 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -511,10 +511,6 @@
-
-
-
-
@@ -620,11 +616,11 @@ - - - - - + + + + + @@ -634,7 +630,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 9b433b3..1903e4a 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 = '1266'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1265'; // ← 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; @@ -73,7 +73,6 @@ const App = (() => { notifications: { title: 'Aktuelles', module: null, requiresAuth: true }, breeder: { title: 'Züchter-Profil', module: null }, 'breeder-editor': { title: 'Profil bearbeiten', module: null, requiresAuth: true }, - 'breeder-dashboard': { title: 'Züchter-Bereich', module: null, requiresAuth: true }, litters: { title: 'Wurfverwaltung', module: null, requiresAuth: true }, wurfboerse: { title: 'Wurfbörse', module: null }, zuchthunde: { title: 'Zuchtkartei', module: null, requiresAuth: true }, diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js index 21120e7..b07b51f 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -2381,10 +2381,6 @@ window.Page_admin = (() => { ${c.grants_founder ? '✓' : '—'} - ${c.uses > 0 ? ` - ` : ''} - - -
Lädt…
- - `).join('')} ` @@ -2580,38 +2571,6 @@ window.Page_admin = (() => { `; - // Alle Einlösungen eines Codes (lazy, .hidden via classList) — mit Kanal-Spalte - el.querySelectorAll('.adm-code-regs').forEach(btn => { - btn.addEventListener('click', async () => { - const row = el.querySelector(`#adm-code-regs-${btn.dataset.id}`); - if (!row) return; - row.classList.toggle('hidden'); - if (row.classList.contains('hidden') || row.dataset.loaded === '1') return; - try { - const regs = await API.get(`/admin/partner/codes/${btn.dataset.id}/registrations`); - row.dataset.loaded = '1'; - const cell = row.querySelector('td'); - cell.innerHTML = !regs.length - ? `
Keine Accounts.
` - : regs.map(u => ` -
-
- ${UI.escape(u.name)} - · ${UI.escape(u.email)} -
- - ${u.qr_seq ? `QR #${u.qr_seq}` : 'Link/manuell'} - - ${(u.created_at || '').slice(0, 16).replace(' ', ' · ')} - ${u.email_verified - ? `✓ bestätigt` - : `⏳ unbestätigt`} -
`).join(''); - } catch (err) { UI.toast.error(err.message); } - }); - }); - // Code pausieren/aktivieren (Notbremse bei geleakten Codes) el.querySelectorAll('.adm-toggle-code').forEach(btn => { btn.addEventListener('click', async () => { diff --git a/backend/static/js/pages/breeder-dashboard.js b/backend/static/js/pages/breeder-dashboard.js deleted file mode 100644 index 4fde179..0000000 --- a/backend/static/js/pages/breeder-dashboard.js +++ /dev/null @@ -1,129 +0,0 @@ -/* ============================================================ - BAN YARO — Züchter-Bereich - Hub für Züchter: Profil-Status, Wurfverwaltung, Zuchtkartei. - (Läufigkeit bleibt bewusst als eigener Chip in der HUND-Welt.) - ============================================================ */ - -window.Page_breeder_dashboard = (() => { - - let _container = null; - - async function init(container) { - _container = container; - _render(); - await _load(); - } - - function refresh() { _load(); } - function onDogChange() {} - - function _render() { - _container.innerHTML = ` -
-
-

- ${UI.icon('certificate')} Züchter-Bereich -

-

- Dein Zwinger, deine Würfe, deine Zuchthunde. -

-
-
-
Lade…
-
-
- `; - } - - async function _load() { - const el = _container.querySelector('#bd-content'); - try { - const [status, litters, hunde] = await Promise.all([ - API.breeder.status().catch(() => null), - API.litters.myList().catch(() => []), - API.zuchthunde.list().catch(() => []), - ]); - el.innerHTML = _renderHub(status, litters || [], hunde || []); - _bindEvents(el); - } catch (e) { - el.innerHTML = `

${UI.escape(e.message || 'Fehler beim Laden.')}

`; - } - } - - function _renderHub(status, litters, hunde) { - const profile = status?.profile; - const isBreeder = status?.rolle === 'breeder' || status?.rolle === 'admin'; - if (!isBreeder) { - return ` -
-

- Der Züchter-Bereich ist für verifizierte Züchter. - Den Antrag findest du in den Einstellungen. -

-
`; - } - - return ` - -
-
-
-
Mein Zwinger
-
${UI.escape(profile?.zwingername || 'Noch kein Profil angelegt')}
- ${profile?.rasse_text ? `
${UI.escape(profile.rasse_text)}
` : ''} - - ${UI.icon('check-circle')} Verifizierter Züchter - -
- -
-
- - -
-
-
- -
-
-
Wurfverwaltung
-
${litters.length} ${litters.length === 1 ? 'Wurf' : 'Würfe'} · Welpen, Gewichte, Kaufverträge
-
- -
-
- - -
-
-
- -
-
-
Zuchtkartei
-
${hunde.length} ${hunde.length === 1 ? 'Zuchthund' : 'Zuchthunde'} · Stammbaum, Genetik, Titel
-
- -
-
- -
- ${UI.icon('info')} Läufigkeit & Trächtigkeit findest du wie gewohnt in der HUND-Welt. -
- `; - } - - function _bindEvents(el) { - el.querySelectorAll('[data-bd-nav]').forEach(btn => { - btn.addEventListener('click', () => App.navigate(btn.dataset.bdNav)); - }); - } - - return { init, refresh, onDogChange }; - -})(); diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index f4bc01d..66d9af0 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -665,6 +665,25 @@ window.Page_settings = (() => {
+ ${u.is_partner ? ` + +
+
${UI.icon('handshake')} Partner
+
+

+ Als Partner hast du vollen Pro-Zugang und eine öffentliche Karte auf der + Partner-Seite. Deine Zahlen und QR-Codes findest du im Partner-Bereich. +

+
+ + +
+
+
` : ''}
Trophäen
@@ -1662,6 +1681,10 @@ 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 936b348..e54f867 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -570,9 +570,10 @@ window.Worlds = (() => { { icon:'sparkle', label:'Jobs', page:'jobs' }, { icon:'book-open', label:'Knigge', page:'knigge' }, { icon:'film-slate', label:'Filme', page:'movies' }, - { icon:'certificate', label:'Züchter', page:'breeder-dashboard', role:'breeder', - fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }, - { icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] }, + { icon:'tree-structure', label:'Zuchtkartei', page:'zuchthunde', role:'breeder', + fab:[{ icon:'tree-structure', color:'#8B5CF6', label:'Zuchthund eintragen', sub:'Neuen Hund anlegen', page:'zuchthunde', action:'openNew' }] }, + { icon:'notebook', label:'Wurfverw.', page:'litters', role:'breeder', + fab:[{ icon:'notebook', color:'#10B981', label:'Wurf anlegen', sub:'Neuen Wurf eintragen', page:'litters', action:'openNew' }] }, { icon:'thermometer', label:'Läufigkeit', page:'laeufi', role:'breeder' }, { icon:'sparkle', label:'Social', page:'social', role:'social', fab:[{ icon:'sparkle', color:'#EC4899', label:'Social-Post', sub:'Beitrag erstellen', page:'social', action:'openNew' }] }, @@ -589,7 +590,7 @@ window.Worlds = (() => { const _DEFAULT_CONFIG = { jetzt: ['notes','expenses','erste-hilfe','playdate','chat','wetter','social','moderation','partner-dashboard','admin'], hund: ['diary','health','uebungen','trainingsplaene','adoption','sitting','wiki','wurfboerse', - 'breeder-dashboard','laeufi','ernaehrung','personality'], + 'litters','zuchthunde','laeufi','ernaehrung','personality'], welt: ['map','forum','friends','walks','poison','recalls','lost','routes','events', 'jobs','knigge','movies','reise'], }; diff --git a/backend/static/landing.html b/backend/static/landing.html index 9ead496..88ec792 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 46913ce..3059fa7 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 = '1266'; +const VER = '1265'; 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 6dfbac7..73c2994 100644 --- a/tests/test_partner_qr.py +++ b/tests/test_partner_qr.py @@ -125,30 +125,6 @@ def test_registration_with_qr_only(client, admin): assert row["referred_qr"] == token -def test_code_registrations_with_channel(client, admin): - """Admin-Liste aller Code-Einloesungen unterscheidet QR-Sticker und Link/manuell.""" - code = _create_code(client, admin) - batch = _create_batch(client, admin, code["id"], quantity=1) - token = _batch_tokens(batch["id"])[0] - - # 1x via QR, 1x via Code direkt - client.post("/api/auth/register", json={ - "email": f"ch1-{secrets.token_hex(4)}@example.com", "password": "QrTest1234!", - "name": f"ch1{secrets.token_hex(3)}", "ref_code": code["code"], "qr_token": token, - }) - client.post("/api/auth/register", json={ - "email": f"ch2-{secrets.token_hex(4)}@example.com", "password": "QrTest1234!", - "name": f"ch2{secrets.token_hex(3)}", "ref_code": code["code"], - }) - - r = client.get(f"/api/admin/partner/codes/{code['id']}/registrations", headers=admin["headers"]) - assert r.status_code == 200 - regs = r.json() - assert len(regs) == 2 - channels = {(x["qr_seq"] or 0) for x in regs} - assert channels == {0, 1} # einer ohne QR (None), einer über Sticker #1 - - def test_paused_code_not_redeemable(client, admin): """Pausierter Code (Notbremse) -> keine Einloesung, Info-Endpoint 404; reaktivierbar.""" code = _create_code(client, admin)