Fix: Admin Züchter-Tab — Alle Züchter Liste + Antraege-Section (SW by-v921)
- GET /api/admin/breeders: neuer Endpunkt listet alle aktiven Züchter mit Zwingername, Rasse, Stadt, Würfe/Zuchthunde-Zähler, subscription_tier - _renderZuechter: zwei Sektionen parallel geladen - "Offene Anträge" (wie vorher, aber mit Section-Header auch wenn leer) - "Alle Züchter": Tabelle analog Nutzer-Tab mit Abo-Button → _changeTier - api.js: API.breeder.allList() hinzugefügt - SW by-v921, APP_VER 921
This commit is contained in:
parent
f6b37717b4
commit
52160e4dc0
6 changed files with 113 additions and 10 deletions
|
|
@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request):
|
||||||
raise _HE(404, "Nicht gefunden.")
|
raise _HE(404, "Nicht gefunden.")
|
||||||
return _media_response(filepath)
|
return _media_response(filepath)
|
||||||
|
|
||||||
APP_VER = "920" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "921" # muss mit APP_VER in app.js übereinstimmen
|
||||||
|
|
||||||
@app.get("/.well-known/assetlinks.json")
|
@app.get("/.well-known/assetlinks.json")
|
||||||
async def assetlinks():
|
async def assetlinks():
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,27 @@ async def admin_pending_breeders(admin=Depends(require_admin)):
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/admin/breeders — alle aktiven Züchter
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/admin/breeders")
|
||||||
|
async def admin_all_breeders(admin=Depends(require_admin)):
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT u.id, u.name, u.email, u.created_at, u.subscription_tier,
|
||||||
|
u.breeder_status, u.last_login,
|
||||||
|
bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied,
|
||||||
|
bp.stadt, bp.website, bp.verified_at,
|
||||||
|
(SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count,
|
||||||
|
(SELECT COUNT(*) FROM dogs WHERE user_id=u.id AND is_zucht_hund=1) AS zuchthunde_count
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN breeder_profiles bp ON bp.user_id = u.id
|
||||||
|
WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved'
|
||||||
|
ORDER BY bp.verified_at DESC NULLS LAST, u.created_at DESC
|
||||||
|
""").fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags
|
# GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -691,6 +691,7 @@ const API = (() => {
|
||||||
updateProfile(data) { return put('/breeder/profile', data); },
|
updateProfile(data) { return put('/breeder/profile', data); },
|
||||||
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
|
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
|
||||||
pendingList() { return get('/admin/breeders/pending'); },
|
pendingList() { return get('/admin/breeders/pending'); },
|
||||||
|
allList() { return get('/admin/breeders'); },
|
||||||
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
|
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
|
||||||
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
|
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
|
||||||
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },
|
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '920'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '921'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||||
|
|
|
||||||
|
|
@ -1893,12 +1893,17 @@ window.Page_admin = (() => {
|
||||||
${UI.icon('arrows-clockwise')} Aktualisieren
|
${UI.icon('arrows-clockwise')} Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="adm-zuchter-list">Lade…</div>
|
<div id="adm-zuchter-antraege">Lade…</div>
|
||||||
|
<div id="adm-zuchter-liste" style="margin-top:var(--space-4)">Lade…</div>
|
||||||
`;
|
`;
|
||||||
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () =>
|
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => {
|
||||||
_loadZuechterAntraege(el.querySelector('#adm-zuchter-list'))
|
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege'));
|
||||||
);
|
_loadZuechterListe(el.querySelector('#adm-zuchter-liste'));
|
||||||
await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list'));
|
});
|
||||||
|
await Promise.all([
|
||||||
|
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')),
|
||||||
|
_loadZuechterListe(el.querySelector('#adm-zuchter-liste')),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadZuechterAntraege(el) {
|
async function _loadZuechterAntraege(el) {
|
||||||
|
|
@ -1912,12 +1917,20 @@ window.Page_admin = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!antraege.length) {
|
if (!antraege.length) {
|
||||||
el.innerHTML = _emptyState('certificate', 'Keine offenen Anträge', 'Aktuell liegen keine Züchter-Anträge zur Prüfung vor.');
|
el.innerHTML = `<div class="card" style="padding:var(--space-4)">
|
||||||
|
<div class="by-card-section-header">Offene Anträge</div>
|
||||||
|
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-sm);color:var(--c-text-muted)">
|
||||||
|
${UI.icon('check-circle')} Keine offenen Anträge
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
<div class="card" style="margin-bottom:0">
|
||||||
|
<div class="by-card-section-header">Offene Anträge (${antraege.length})</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-3)">
|
||||||
${antraege.map(a => `
|
${antraege.map(a => `
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
|
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
|
||||||
|
|
@ -2072,6 +2085,74 @@ window.Page_admin = (() => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function _loadZuechterListe(el) {
|
||||||
|
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">Lade…</div>`;
|
||||||
|
let breeders;
|
||||||
|
try {
|
||||||
|
breeders = await API.breeder.allList();
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = _emptyState('warning', 'Fehler', e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierBadge = t => {
|
||||||
|
if (t === 'breeder') return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#C4843A;color:#fff">Züchter-Abo</span>`;
|
||||||
|
if (t === 'breeder_test') return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#aaa;color:#fff">Test</span>`;
|
||||||
|
return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#eee;color:#666">Standard</span>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = breeders.map(b => `
|
||||||
|
<tr>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">
|
||||||
|
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(b.name)}</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">${_esc(b.email)}</div>
|
||||||
|
</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(b.zwingername || '—')}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.rasse_text || '—')}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.stadt || '—')}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-size:var(--text-xs)">
|
||||||
|
${b.wuerfe_count || 0} Würfe<br>
|
||||||
|
<span style="color:var(--c-text-muted)">${b.zuchthunde_count || 0} Zuchthunde</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(b.subscription_tier)}</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||||
|
${b.verified_at ? new Date(b.verified_at).toLocaleDateString('de-DE') : '—'}
|
||||||
|
</td>
|
||||||
|
<td style="padding:var(--space-2) var(--space-3)">
|
||||||
|
<button class="btn btn-sm btn-ghost adm-breeder-tier-btn"
|
||||||
|
data-uid="${b.id}" data-name="${_esc(b.name)}" data-tier="${_esc(b.subscription_tier || 'standard')}"
|
||||||
|
style="font-size:var(--text-xs)">
|
||||||
|
Abo
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>`).join('');
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="card adm-table-card">
|
||||||
|
<div class="by-card-section-header">Alle Züchter (${breeders.length})</div>
|
||||||
|
<div class="adm-table-scroll">
|
||||||
|
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||||
|
<thead><tr>
|
||||||
|
${['Nutzer','Zwingername','Rasse','Stadt','Aktivität','Abo','Seit',''].map(h =>
|
||||||
|
`<th style="padding:var(--space-2) var(--space-3);text-align:left;
|
||||||
|
font-size:var(--text-xs);color:var(--c-text-muted);font-weight:600;
|
||||||
|
border-bottom:1px solid var(--c-border);white-space:nowrap">${h}</th>`
|
||||||
|
).join('')}
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${rows || `<tr><td colspan="8" style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">Noch keine Züchter</td></tr>`}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
el.querySelectorAll('.adm-breeder-tier-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () =>
|
||||||
|
_changeTier(btn.dataset.uid, btn.dataset.name, btn.dataset.tier)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
async function _renderJobs(el) {
|
async function _renderJobs(el) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v920';
|
const CACHE_VERSION = 'by-v921';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue