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:
rene 2026-05-14 10:06:48 +02:00
parent f6b37717b4
commit 52160e4dc0
6 changed files with 113 additions and 10 deletions

View file

@ -691,6 +691,7 @@ const API = (() => {
updateProfile(data) { return put('/breeder/profile', data); },
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
pendingList() { return get('/admin/breeders/pending'); },
allList() { return get('/admin/breeders'); },
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },

View file

@ -3,7 +3,7 @@
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 IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -1893,12 +1893,17 @@ window.Page_admin = (() => {
${UI.icon('arrows-clockwise')} Aktualisieren
</button>
</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', () =>
_loadZuechterAntraege(el.querySelector('#adm-zuchter-list'))
);
await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list'));
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => {
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege'));
_loadZuechterListe(el.querySelector('#adm-zuchter-liste'));
});
await Promise.all([
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')),
_loadZuechterListe(el.querySelector('#adm-zuchter-liste')),
]);
}
async function _loadZuechterAntraege(el) {
@ -1912,12 +1917,20 @@ window.Page_admin = (() => {
}
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;
}
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 => `
<div class="card" style="padding:var(--space-4)">
<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) {
el.innerHTML = `

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v920';
const CACHE_VERSION = 'by-v921';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache