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 ? `
+
+ ${UI.icon('users')}
+ ` : ''}
@@ -2392,6 +2396,11 @@ window.Page_admin = (() => {
+
+
+ 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.
+
+
+
+
+ `;
+ }
+
+ 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
+
+
+
+ ${UI.icon('pencil-simple')} Profil
+
+
+
+
+
+
+
+
+
+
+
+
Wurfverwaltung
+
${litters.length} ${litters.length === 1 ? 'Wurf' : 'Würfe'} · Welpen, Gewichte, Kaufverträge
+
+
Öffnen
+
+
+
+
+
+
+
+
+
+
+
Zuchtkartei
+
${hunde.length} ${hunde.length === 1 ? 'Zuchthund' : 'Zuchthunde'} · Stammbaum, Genetik, Titel
+
+
Öffnen
+
+
+
+
+ ${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 ? `
-
-
-
-
-
- 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.
-
-
-
- ${UI.icon('handshake')} Partner-Bereich
-
-
- ${UI.icon('pencil-simple')} Öffentliches Profil
-
-
-
-
` : ''}
@@ -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)