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:
rene 2026-06-07 19:55:51 +02:00
parent 6a6a09d879
commit ed7c469c6a
11 changed files with 241 additions and 44 deletions

View file

@ -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 () => {