Züchter-Bereich (Hub) + Settings-Partner-Karte raus + Admin: alle Code-Einlösungen mit Kanal
- 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.
This commit is contained in:
parent
6a6a09d879
commit
ed7c469c6a
11 changed files with 241 additions and 44 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1265"></script>
|
||||
<script src="/js/boot-early.js?v=1266"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1265">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1265">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1265">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1265">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1265">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1266">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1266">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1266">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1266">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1266">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -511,6 +511,10 @@
|
|||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="page" id="page-breeder-dashboard">
|
||||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
||||
<section class="page" id="page-jobs">
|
||||
<div class="page-body page-container"></div>
|
||||
</section>
|
||||
|
|
@ -616,11 +620,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1265"></script>
|
||||
<script src="/js/ui.js?v=1265"></script>
|
||||
<script src="/js/app.js?v=1265"></script>
|
||||
<script src="/js/worlds.js?v=1265"></script>
|
||||
<script src="/js/offline-indicator.js?v=1265"></script>
|
||||
<script src="/js/api.js?v=1266"></script>
|
||||
<script src="/js/ui.js?v=1266"></script>
|
||||
<script src="/js/app.js?v=1266"></script>
|
||||
<script src="/js/worlds.js?v=1266"></script>
|
||||
<script src="/js/offline-indicator.js?v=1266"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -630,7 +634,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1265"></script>
|
||||
<script src="/js/boot.js?v=1266"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -2381,6 +2381,10 @@ window.Page_admin = (() => {
|
|||
${c.grants_founder ? '✓' : '—'}
|
||||
</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);white-space:nowrap;text-align:right">
|
||||
${c.uses > 0 ? `
|
||||
<button class="btn btn-ghost btn-sm adm-code-regs" data-id="${c.id}" title="Alle Einlösungen anzeigen (inkl. Kanal)">
|
||||
${UI.icon('users')}
|
||||
</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm adm-toggle-code" data-id="${c.id}"
|
||||
title="${c.active ? 'Pausieren — Notbremse wenn der Code im Netz kursiert (Einlösung gesperrt, Historie bleibt)' : 'Wieder aktivieren'}"
|
||||
style="font-size:var(--text-xs)">
|
||||
|
|
@ -2392,6 +2396,11 @@ window.Page_admin = (() => {
|
|||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hidden" id="adm-code-regs-${c.id}">
|
||||
<td colspan="5" style="padding:0 var(--space-3) var(--space-3);background:var(--c-surface-2)">
|
||||
<div class="text-sm-muted" style="padding:var(--space-3) 0">Lädt…</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>`
|
||||
|
|
@ -2571,6 +2580,38 @@ window.Page_admin = (() => {
|
|||
</div>
|
||||
`;
|
||||
|
||||
// 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
|
||||
? `<div class="text-sm-muted" style="padding:var(--space-3) 0">Keine Accounts.</div>`
|
||||
: regs.map(u => `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) 0;border-bottom:1px solid var(--c-border);font-size:var(--text-sm)">
|
||||
<div class="flex-1-min">
|
||||
<span style="font-weight:600">${UI.escape(u.name)}</span>
|
||||
<span class="text-xs-muted">· ${UI.escape(u.email)}</span>
|
||||
</div>
|
||||
<span class="badge" style="background:${u.qr_seq ? 'rgba(139,92,246,.12)' : 'var(--c-surface)'};color:${u.qr_seq ? '#8B5CF6' : 'var(--c-text-muted)'}"
|
||||
title="${u.qr_seq ? `Gedruckter QR-Code aus Kontingent „${UI.escape(u.qr_batch_label || '')}"` : 'Code eingetippt oder ?ref-Link'}">
|
||||
${u.qr_seq ? `QR #${u.qr_seq}` : 'Link/manuell'}
|
||||
</span>
|
||||
<span class="text-xs-muted">${(u.created_at || '').slice(0, 16).replace(' ', ' · ')}</span>
|
||||
${u.email_verified
|
||||
? `<span class="badge" style="background:#dcfce7;color:#16a34a">✓ bestätigt</span>`
|
||||
: `<span class="badge" style="background:#fef9c3;color:#a16207">⏳ unbestätigt</span>`}
|
||||
</div>`).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 () => {
|
||||
|
|
|
|||
129
backend/static/js/pages/breeder-dashboard.js
Normal file
129
backend/static/js/pages/breeder-dashboard.js
Normal file
|
|
@ -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 = `
|
||||
<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('certificate')} Züchter-Bereich
|
||||
</h1>
|
||||
<p style="color:var(--c-text-secondary);font-size:var(--text-sm);margin:0">
|
||||
Dein Zwinger, deine Würfe, deine Zuchthunde.
|
||||
</p>
|
||||
</div>
|
||||
<div id="bd-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('#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 = `<p class="text-danger">${UI.escape(e.message || 'Fehler beim Laden.')}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function _renderHub(status, litters, hunde) {
|
||||
const profile = status?.profile;
|
||||
const isBreeder = status?.rolle === 'breeder' || status?.rolle === 'admin';
|
||||
if (!isBreeder) {
|
||||
return `
|
||||
<div class="card" style="padding:var(--space-5);text-align:center">
|
||||
<p class="text-sm-secondary" style="margin:0">
|
||||
Der Züchter-Bereich ist für verifizierte Züchter.
|
||||
Den Antrag findest du in den <a href="#settings" class="text-primary">Einstellungen</a>.
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<!-- Zwinger / Profil -->
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
|
||||
<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)">Mein Zwinger</div>
|
||||
<div style="font-weight:700">${UI.escape(profile?.zwingername || 'Noch kein Profil angelegt')}</div>
|
||||
${profile?.rasse_text ? `<div class="text-xs-muted">${UI.escape(profile.rasse_text)}</div>` : ''}
|
||||
<span class="badge" style="background:#dcfce7;color:#16a34a;margin-top:var(--space-1)">
|
||||
${UI.icon('check-circle')} Verifizierter Züchter
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" data-bd-nav="breeder-editor">
|
||||
${UI.icon('pencil-simple')} Profil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wurfverwaltung -->
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<div style="width:44px;height:44px;border-radius:var(--radius-md);background:rgba(16,185,129,.12);
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:22px;height:22px;color:#10B981"><use href="/icons/phosphor.svg#notebook"></use></svg>
|
||||
</div>
|
||||
<div class="flex-1-min">
|
||||
<div style="font-weight:700">Wurfverwaltung</div>
|
||||
<div class="text-xs-muted">${litters.length} ${litters.length === 1 ? 'Wurf' : 'Würfe'} · Welpen, Gewichte, Kaufverträge</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" data-bd-nav="litters">Öffnen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zuchtkartei -->
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<div style="width:44px;height:44px;border-radius:var(--radius-md);background:rgba(139,92,246,.12);
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:22px;height:22px;color:#8B5CF6"><use href="/icons/phosphor.svg#tree-structure"></use></svg>
|
||||
</div>
|
||||
<div class="flex-1-min">
|
||||
<div style="font-weight:700">Zuchtkartei</div>
|
||||
<div class="text-xs-muted">${hunde.length} ${hunde.length === 1 ? 'Zuchthund' : 'Zuchthunde'} · Stammbaum, Genetik, Titel</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" data-bd-nav="zuchthunde">Öffnen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs-muted" style="padding:0 var(--space-1)">
|
||||
${UI.icon('info')} Läufigkeit & Trächtigkeit findest du wie gewohnt in der HUND-Welt.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _bindEvents(el) {
|
||||
el.querySelectorAll('[data-bd-nav]').forEach(btn => {
|
||||
btn.addEventListener('click', () => App.navigate(btn.dataset.bdNav));
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
@ -665,25 +665,6 @@ window.Page_settings = (() => {
|
|||
<!-- Züchter-Profil Slot -->
|
||||
<div id="breeder-card-slot"></div>
|
||||
|
||||
${u.is_partner ? `
|
||||
<!-- Partner-Bereich -->
|
||||
<div class="card mb-4">
|
||||
<div class="by-card-section-header">${UI.icon('handshake')} Partner</div>
|
||||
<div class="p-4">
|
||||
<p class="text-sm-secondary" style="margin:0 0 var(--space-3)">
|
||||
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.
|
||||
</p>
|
||||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||
<button class="btn btn-primary btn-sm" id="settings-partner-dashboard-btn">
|
||||
${UI.icon('handshake')} Partner-Bereich
|
||||
</button>
|
||||
<button class="btn btn-secondary btn-sm" id="settings-partner-profile-btn">
|
||||
${UI.icon('pencil-simple')} Öffentliches Profil
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="by-card-section-header">Trophäen</div>
|
||||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1265"></script>
|
||||
<script src="/js/landing-init.js?v=1266"></script>
|
||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, direkt im Browser.">
|
||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue