From ed7c469c6ac4e52c37747d232586cd80df7f6028 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 7 Jun 2026 19:55:51 +0200 Subject: [PATCH] =?UTF-8?q?Z=C3=BCchter-Bereich=20(Hub)=20+=20Settings-Par?= =?UTF-8?q?tner-Karte=20raus=20+=20Admin:=20alle=20Code-Einl=C3=B6sungen?= =?UTF-8?q?=20mit=20Kanal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Seite #breeder-dashboard (Welten-Chip 'Züchter' role:breeder in HUND, ersetzt die Einzel-Chips Zuchtkartei + Wurfverw.; beide FABs wandern an den neuen Chip; Läufigkeit bleibt eigener Chip in HUND, Rene-Vorgabe): Zwinger-Karte (Name, verifiziert-Badge, Profil-Editor), Wurfverwaltung mit Wurf-Anzahl, Zuchtkartei mit Hunde-Anzahl. Einzelseiten bleiben erreichbar. - Settings: Partner-Karte entfernt — der 🤝-Welten-Chip ist der Einstieg. - Admin 'Aktive Codes': 👥 zeigt jetzt ALLE Einlösungen eines Codes mit Kanal-Badge (QR #seq aus Kontingent vs. Link/manuell), Datum und Bestätigt-Status — Endpoint /admin/partner/codes/{id}/registrations. Suite: 55 passed. --- VERSION | 2 +- backend/routes/partner.py | 22 ++++ backend/static/index.html | 28 ++-- backend/static/js/app.js | 3 +- backend/static/js/pages/admin.js | 41 ++++++ backend/static/js/pages/breeder-dashboard.js | 129 +++++++++++++++++++ backend/static/js/pages/settings.js | 23 ---- backend/static/js/worlds.js | 9 +- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- tests/test_partner_qr.py | 24 ++++ 11 files changed, 241 insertions(+), 44 deletions(-) create mode 100644 backend/static/js/pages/breeder-dashboard.js diff --git a/VERSION b/VERSION index 3420149..4c8735e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1265 \ No newline at end of file +1266 \ No newline at end of file diff --git a/backend/routes/partner.py b/backend/routes/partner.py index 115517b..7690352 100644 --- a/backend/routes/partner.py +++ b/backend/routes/partner.py @@ -48,6 +48,28 @@ 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 4d30aec..80ab2df 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -511,6 +511,10 @@
+
+
+
+
@@ -616,11 +620,11 @@ - - - - - + + + + + @@ -630,7 +634,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1903e4a..9b433b3 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 = '1265'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1266'; // ← 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,6 +73,7 @@ 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 b07b51f..21120e7 100644 --- a/backend/static/js/pages/admin.js +++ b/backend/static/js/pages/admin.js @@ -2381,6 +2381,10 @@ window.Page_admin = (() => { ${c.grants_founder ? '✓' : '—'} + ${c.uses > 0 ? ` + ` : ''} + + +
Lädt…
+ + `).join('')} ` @@ -2571,6 +2580,38 @@ 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 new file mode 100644 index 0000000..4fde179 --- /dev/null +++ b/backend/static/js/pages/breeder-dashboard.js @@ -0,0 +1,129 @@ +/* ============================================================ + 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 66d9af0..f4bc01d 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -665,25 +665,6 @@ 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
@@ -1681,10 +1662,6 @@ 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 e54f867..936b348 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -570,10 +570,9 @@ window.Worlds = (() => { { icon:'sparkle', label:'Jobs', page:'jobs' }, { icon:'book-open', label:'Knigge', page:'knigge' }, { icon:'film-slate', label:'Filme', page:'movies' }, - { 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:'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:'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' }] }, @@ -590,7 +589,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', - 'litters','zuchthunde','laeufi','ernaehrung','personality'], + 'breeder-dashboard','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 88ec792..9ead496 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 3059fa7..46913ce 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 = '1265'; +const VER = '1266'; 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 73c2994..6dfbac7 100644 --- a/tests/test_partner_qr.py +++ b/tests/test_partner_qr.py @@ -125,6 +125,30 @@ 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)