banyaro/backend/static/js/pages/zuchthunde.js
rene 91340be5a3 Feature: Vollständige Züchter-Rolle — Antrag, Würfe, Stammbaum, Genetik
Basis-Features (Schritte 1–11):
- Züchter-Antrag mit Dokument-Upload, Admin-Prüfung, E-Mail-Benachrichtigungen
- Öffentliches Züchter-Profil + Karten-Marker (lila, certificate-Icon)
- Wurfverwaltung: Würfe, Welpen, Gewichtsverlauf, Foto-System
- Wurfbörse (öffentlich) mit Filtersuche nach Rasse/Status
- Läufigkeits-Tracker: Deckdatum + Wurftermin (+63 Tage, nur für Züchter)
- Interessenten-Chat: Kontakt-Button in Wurfbörse und Züchter-Profil
- Sidebar-Einträge: Zuchtkartei + Wurfverwaltung für Züchter/Admin

Stammbaum & Genetik (Schritte 1–8):
- Zuchtkartei: Hunde-Stammdaten mit Vater/Mutter-Verknüpfung
- Stammbaum-Visualisierung: 4 Generationen, horizontales CSS-Grid
- Gesundheitstests (HD, ED, OCD, Augen…) mit farbigen Ergebnis-Badges
- Genetische Tests (MDR1, PRA, DM…): clear/carrier/affected
- Titel & Auszeichnungen (CAC, CACIB, IPO…)
- Probeverpaarung: IK-Berechnung nach Wright + Ampel-Bewertung
- Teilen-Link für öffentliche Hunde-Profile
- Kaufvertrag: druckbares HTML-Dokument pro Welpe

Technisch: 4 neue Route-Dateien, 5 neue Page-Module, 11 neue DB-Tabellen,
icons shield-check + certificate + tree-structure im Sprite — SW by-v465, APP_VER 444
2026-04-28 18:25:21 +02:00

1299 lines
52 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Zuchtkartei
Züchter verwalten ihre Hunde inkl. Gesundheitstests,
Gentests und Titel.
============================================================ */
window.Page_zuchthunde = (() => {
let _container = null;
let _appState = null;
let _hunde = []; // geladene Hunde
let _query = ''; // Suchtext
let _openSections = {}; // { <hundId>: 'health'|'genetic'|'titles'|null }
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _esc(s) {
return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _fmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso + 'T12:00:00');
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function _genderIcon(g) {
if (g === 'maennlich') return UI.icon('gender-male');
if (g === 'weiblich') return UI.icon('gender-female');
return UI.icon('dog');
}
// ----------------------------------------------------------
// Badge-Farben
// ----------------------------------------------------------
function _healthBadge(testTyp, ergebnis) {
const e = (ergebnis || '').trim().toUpperCase();
let color = '#6B7280'; // neutral grau
if (testTyp === 'HD') {
if (['A1','A2','A'].includes(e)) color = '#22C55E'; // grün
else if (['B1','B2','B'].includes(e)) color = '#86EFAC'; // hellgrün
else if (e === 'C') color = '#EAB308'; // gelb
else if (e === 'D') color = '#F97316'; // orange
else if (e === 'E') color = '#EF4444'; // rot
} else if (testTyp === 'ED') {
if (e === '0' || e === 'ED 0') color = '#22C55E';
else if (e === '1' || e === 'ED 1') color = '#EAB308';
else if (e === '2' || e === 'ED 2') color = '#F97316';
else if (e === '3' || e === 'ED 3') color = '#EF4444';
}
return `<span class="zh-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`;
}
function _geneticBadge(ergebnis) {
const e = (ergebnis || '').toLowerCase().trim();
let color = '#6B7280';
if (e === 'clear') color = '#22C55E';
if (e === 'carrier') color = '#EAB308';
if (e === 'affected') color = '#EF4444';
return `<span class="zh-badge" style="background:${color}">${_esc(ergebnis || '—')}</span>`;
}
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
const u = _appState.user;
if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) {
_container.innerHTML = `
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('lock')}</div>
<h3 style="margin:0 0 var(--space-2)">Kein Zugriff</h3>
<p style="color:var(--c-text-secondary);margin:0">Diese Seite ist nur für verifizierte Züchter.</p>
</div>`;
return;
}
_render();
await _load();
}
function refresh() {
const u = _appState?.user;
if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) return;
_load();
}
function onDogChange() {}
// ----------------------------------------------------------
// Grundstruktur
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="zh-layout">
<div class="by-toolbar">
<h2 style="margin:0;font-size:var(--text-lg);font-weight:var(--weight-semibold)">
${UI.icon('dog')} Zuchtkartei
</h2>
<button class="btn btn-primary btn-sm" id="zh-new-btn">
${UI.icon('plus')} Hund anlegen
</button>
<button class="btn btn-secondary btn-sm" id="zh-trial-btn">
${UI.icon('dna')} Probeverpaarung
</button>
</div>
<div style="padding:0 0 var(--space-3)">
<input class="form-control" id="zh-search" type="search"
placeholder="Suche nach Name oder Rufname…"
style="max-width:320px">
</div>
<div id="zh-list">
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt…</p>
</div>
</div>`;
document.getElementById('zh-new-btn')?.addEventListener('click', () => _showHundForm(null));
document.getElementById('zh-trial-btn')?.addEventListener('click', () => _showTrialMatingModal());
document.getElementById('zh-search')?.addEventListener('input', e => {
_query = e.target.value.toLowerCase().trim();
_renderList();
});
}
// ----------------------------------------------------------
// Hunde laden
// ----------------------------------------------------------
async function _load() {
try {
_hunde = await API.zuchthunde.list();
_renderList();
} catch (err) {
const el = document.getElementById('zh-list');
if (el) el.innerHTML = `
<p style="color:var(--c-danger);text-align:center;padding:var(--space-8)">
${_esc(err.message || 'Fehler beim Laden.')}
</p>`;
}
}
// ----------------------------------------------------------
// Liste rendern (inkl. Suche)
// ----------------------------------------------------------
function _renderList() {
const el = document.getElementById('zh-list');
if (!el) return;
const filtered = _query
? _hunde.filter(h =>
(h.name || '').toLowerCase().includes(_query) ||
(h.rufname || '').toLowerCase().includes(_query))
: _hunde;
if (!filtered.length) {
el.innerHTML = _query
? `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">Keine Treffer für „${_esc(_query)}".</p>`
: `
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
<p style="color:var(--c-text-secondary)">Noch keine Hunde angelegt.</p>
<button class="btn btn-primary" style="margin-top:var(--space-4)" id="zh-first-btn">
${UI.icon('plus')} Ersten Hund anlegen
</button>
</div>`;
document.getElementById('zh-first-btn')?.addEventListener('click', () => _showHundForm(null));
return;
}
el.innerHTML = filtered.map(h => _hundCardHTML(h)).join('');
// Events verdrahten
el.querySelectorAll('.zh-edit-btn').forEach(btn => {
btn.addEventListener('click', () => {
const h = _hunde.find(x => x.id === parseInt(btn.dataset.id));
if (h) _showHundForm(h);
});
});
el.querySelectorAll('.zh-delete-btn').forEach(btn => {
btn.addEventListener('click', () => _deleteHund(parseInt(btn.dataset.id)));
});
el.querySelectorAll('.zh-pedigree-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id);
if (App?.navigate) App.navigate('zucht-profil', true, { id });
});
});
el.querySelectorAll('.zh-link-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id);
const url = window.location.origin + '#zucht-profil&id=' + id;
navigator.clipboard.writeText(url).then(() => {
UI.toast.success('Link kopiert!');
}).catch(() => {
UI.toast.error('Kopieren nicht möglich.');
});
});
});
el.querySelectorAll('.zh-section-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id);
const section = btn.dataset.section;
_toggleSection(id, section);
});
});
// Offene Sektionen wiederherstellen
Object.entries(_openSections).forEach(([id, sec]) => {
if (sec) _openSection(parseInt(id), sec);
});
}
// ----------------------------------------------------------
// Hund-Card HTML
// ----------------------------------------------------------
function _hundCardHTML(h) {
const nameLabel = h.name ? _esc(h.name) : '<em style="color:var(--c-text-muted)">Unbenannt</em>';
const rufname = h.rufname ? ` (${_esc(h.rufname)})` : '';
const geburtstag = h.geburtsdatum ? _fmtDate(h.geburtsdatum) : null;
const vaterLabel = h.vater_name ? `Vater: ${_esc(h.vater_name)}` : null;
const mutterLabel = h.mutter_name ? `Mutter: ${_esc(h.mutter_name)}` : null;
const eltern = [vaterLabel, mutterLabel].filter(Boolean).join(' &nbsp;·&nbsp; ');
const pubLabel = h.is_public
? `<span style="color:var(--c-success);font-size:var(--text-xs)">${UI.icon('eye')} Öffentlich</span>`
: '';
return `
<div class="zh-card" id="zh-card-${h.id}">
<div class="zh-card-header">
<div style="flex:1;min-width:0">
<div class="zh-card-title">
${_genderIcon(h.geschlecht)}
${nameLabel}${_esc(rufname)}
${pubLabel}
</div>
<div class="zh-card-meta">
${h.rasse ? `${UI.icon('paw-print')} ${_esc(h.rasse)}&nbsp;&nbsp;` : ''}
${geburtstag ? `${UI.icon('calendar-dots')} ${geburtstag}&nbsp;&nbsp;` : ''}
${h.chip_nr ? `${UI.icon('barcode')} ${_esc(h.chip_nr)}&nbsp;&nbsp;` : ''}
${h.zuchtbuchnummer ? `${UI.icon('book-open')} ${_esc(h.zuchtbuchnummer)}&nbsp;&nbsp;` : ''}
</div>
${eltern ? `<div class="zh-card-meta" style="font-size:var(--text-xs);color:var(--c-text-secondary)">${eltern}</div>` : ''}
</div>
<div class="zh-card-actions">
<button class="btn btn-ghost btn-sm zh-pedigree-btn" data-id="${h.id}"
title="Stammbaum">
${UI.icon('tree-structure')} Stammbaum
</button>
<button class="btn btn-ghost btn-sm zh-link-btn" data-id="${h.id}"
title="Profil-Link kopieren">
${UI.icon('link-simple')}
</button>
<button class="btn btn-ghost btn-sm zh-edit-btn" data-id="${h.id}"
title="Bearbeiten">
${UI.icon('pencil-simple')}
</button>
<button class="btn btn-ghost btn-sm zh-delete-btn" data-id="${h.id}"
title="Löschen" style="color:var(--c-danger)">
${UI.icon('trash')}
</button>
</div>
</div>
<div class="zh-section-buttons">
<button class="btn btn-ghost btn-sm zh-section-btn" data-id="${h.id}" data-section="health">
${UI.icon('heart')} Gesundheit
</button>
<button class="btn btn-ghost btn-sm zh-section-btn" data-id="${h.id}" data-section="genetic">
${UI.icon('dna')} Genetik
</button>
<button class="btn btn-ghost btn-sm zh-section-btn" data-id="${h.id}" data-section="titles">
${UI.icon('trophy')} Titel
</button>
</div>
<div class="zh-section-wrap" id="zh-section-${h.id}" style="display:none"></div>
</div>`;
}
// ----------------------------------------------------------
// Sektion aufklappen / zuklappen
// ----------------------------------------------------------
function _toggleSection(hundId, section) {
const current = _openSections[hundId];
if (current === section) {
// zuklappen
_closeSection(hundId);
} else {
_openSection(hundId, section);
}
}
function _closeSection(hundId) {
const wrap = document.getElementById(`zh-section-${hundId}`);
if (wrap) wrap.style.display = 'none';
_openSections[hundId] = null;
_updateSectionButtons(hundId, null);
}
function _openSection(hundId, section) {
_openSections[hundId] = section;
_updateSectionButtons(hundId, section);
const wrap = document.getElementById(`zh-section-${hundId}`);
if (!wrap) return;
wrap.style.display = '';
wrap.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm);padding:var(--space-2)">Lädt…</p>`;
if (section === 'health') _loadHealthSection(hundId, wrap);
if (section === 'genetic') _loadGeneticSection(hundId, wrap);
if (section === 'titles') _loadTitlesSection(hundId, wrap);
}
function _updateSectionButtons(hundId, activeSection) {
const card = document.getElementById(`zh-card-${hundId}`);
if (!card) return;
card.querySelectorAll('.zh-section-btn').forEach(btn => {
const isActive = btn.dataset.section === activeSection;
btn.style.fontWeight = isActive ? 'var(--weight-semibold)' : '';
btn.style.color = isActive ? 'var(--c-primary)' : '';
});
}
// ----------------------------------------------------------
// Gesundheits-Sektion
// ----------------------------------------------------------
async function _loadHealthSection(hundId, wrap) {
try {
const tests = await API.zuchthunde.healthTests(hundId);
_renderHealthSection(hundId, wrap, tests);
} catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`;
}
}
function _renderHealthSection(hundId, wrap, tests) {
const rows = tests.length
? tests.map(t => `
<div class="zh-detail-row">
<div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.test_typ || 'Sonstiges')}</span>
${t.test_name ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(t.test_name)}</span>` : ''}
${_healthBadge(t.test_typ || '', t.ergebnis)}
${t.untersuch_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.untersuch_am)}</span>` : ''}
${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.labor)}</span>` : ''}
</div>
<button class="btn btn-ghost btn-xs zh-health-del-btn" data-tid="${t.id}" title="Löschen"
style="color:var(--c-danger)">${UI.icon('trash')}</button>
</div>`).join('')
: `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Gesundheitstests eingetragen.</p>`;
wrap.innerHTML = `
<div class="zh-section-inner">
<div class="zh-section-header">
<span style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
${UI.icon('heart')} Gesundheitstests
</span>
<button class="btn btn-secondary btn-xs zh-health-add-btn" data-id="${hundId}">
${UI.icon('plus')} Test
</button>
</div>
<div class="zh-detail-list">${rows}</div>
</div>`;
wrap.querySelector('.zh-health-add-btn')?.addEventListener('click', () => _showHealthTestForm(hundId));
wrap.querySelectorAll('.zh-health-del-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const tid = parseInt(btn.dataset.tid);
if (!window.confirm('Gesundheitstest wirklich löschen?')) return;
try {
await API.zuchthunde.deleteHealthTest(tid);
UI.toast.success('Test gelöscht.');
await _reloadHealthSection(hundId);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
});
});
}
async function _reloadHealthSection(hundId) {
const wrap = document.getElementById(`zh-section-${hundId}`);
if (!wrap || _openSections[hundId] !== 'health') return;
await _loadHealthSection(hundId, wrap);
}
// ----------------------------------------------------------
// Genetik-Sektion
// ----------------------------------------------------------
async function _loadGeneticSection(hundId, wrap) {
try {
const tests = await API.zuchthunde.geneticTests(hundId);
_renderGeneticSection(hundId, wrap, tests);
} catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`;
}
}
function _renderGeneticSection(hundId, wrap, tests) {
const rows = tests.length
? tests.map(t => `
<div class="zh-detail-row">
<div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.marker_name || '—')}</span>
${_geneticBadge(t.ergebnis_klasse)}
${t.getestet_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.getestet_am)}</span>` : ''}
${t.labor ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.labor)}</span>` : ''}
</div>
<button class="btn btn-ghost btn-xs zh-genetic-del-btn" data-tid="${t.id}" title="Löschen"
style="color:var(--c-danger)">${UI.icon('trash')}</button>
</div>`).join('')
: `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Gentests eingetragen.</p>`;
wrap.innerHTML = `
<div class="zh-section-inner">
<div class="zh-section-header">
<span style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
${UI.icon('dna')} Gentests
</span>
<button class="btn btn-secondary btn-xs zh-genetic-add-btn" data-id="${hundId}">
${UI.icon('plus')} Gentest
</button>
</div>
<div class="zh-detail-list">${rows}</div>
</div>`;
wrap.querySelector('.zh-genetic-add-btn')?.addEventListener('click', () => _showGeneticTestForm(hundId));
wrap.querySelectorAll('.zh-genetic-del-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const tid = parseInt(btn.dataset.tid);
if (!window.confirm('Gentest wirklich löschen?')) return;
try {
await API.zuchthunde.deleteGeneticTest(tid);
UI.toast.success('Gentest gelöscht.');
await _reloadGeneticSection(hundId);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
});
});
}
async function _reloadGeneticSection(hundId) {
const wrap = document.getElementById(`zh-section-${hundId}`);
if (!wrap || _openSections[hundId] !== 'genetic') return;
await _loadGeneticSection(hundId, wrap);
}
// ----------------------------------------------------------
// Titel-Sektion
// ----------------------------------------------------------
async function _loadTitlesSection(hundId, wrap) {
try {
const titles = await API.zuchthunde.titles(hundId);
_renderTitlesSection(hundId, wrap, titles);
} catch (err) {
wrap.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm);padding:var(--space-2)">${_esc(err.message || 'Fehler.')}</p>`;
}
}
function _renderTitlesSection(hundId, wrap, titles) {
const rows = titles.length
? titles.map(t => `
<div class="zh-detail-row">
<div class="zh-detail-info">
<span class="zh-detail-label">${_esc(t.titel_name || '—')}</span>
${t.titel_typ ? `<span class="zh-badge" style="background:#6B7280">${_esc(t.titel_typ)}</span>` : ''}
${t.verliehen_am ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_fmtDate(t.verliehen_am)}</span>` : ''}
${t.ort ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.ort)}</span>` : ''}
${t.richter ? `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${_esc(t.richter)}</span>` : ''}
${t.formwert ? `<span class="zh-badge" style="background:#3B82F6">${_esc(t.formwert)}</span>` : ''}
</div>
<button class="btn btn-ghost btn-xs zh-title-del-btn" data-tid="${t.id}" title="Löschen"
style="color:var(--c-danger)">${UI.icon('trash')}</button>
</div>`).join('')
: `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Titel eingetragen.</p>`;
wrap.innerHTML = `
<div class="zh-section-inner">
<div class="zh-section-header">
<span style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
${UI.icon('trophy')} Titel
</span>
<button class="btn btn-secondary btn-xs zh-title-add-btn" data-id="${hundId}">
${UI.icon('plus')} Titel
</button>
</div>
<div class="zh-detail-list">${rows}</div>
</div>`;
wrap.querySelector('.zh-title-add-btn')?.addEventListener('click', () => _showTitleForm(hundId));
wrap.querySelectorAll('.zh-title-del-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const tid = parseInt(btn.dataset.tid);
if (!window.confirm('Titel wirklich löschen?')) return;
try {
await API.zuchthunde.deleteTitle(tid);
UI.toast.success('Titel gelöscht.');
await _reloadTitlesSection(hundId);
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
});
});
}
async function _reloadTitlesSection(hundId) {
const wrap = document.getElementById(`zh-section-${hundId}`);
if (!wrap || _openSections[hundId] !== 'titles') return;
await _loadTitlesSection(hundId, wrap);
}
// ----------------------------------------------------------
// Hund anlegen / bearbeiten
// ----------------------------------------------------------
function _showHundForm(hund) {
const isEdit = !!hund;
const v = hund || {};
// Für Vater/Mutter-Dropdown: alle männlichen / weiblichen Hunde (außer sich selbst)
const maennliche = _hunde.filter(h => h.geschlecht === 'maennlich' && h.id !== (hund?.id));
const weibliche = _hunde.filter(h => h.geschlecht === 'weiblich' && h.id !== (hund?.id));
const vaterOptions = [
`<option value="">— kein Vater —</option>`,
...maennliche.map(h =>
`<option value="${h.id}" ${v.vater_id === h.id ? 'selected' : ''}>${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`),
].join('');
const mutterOptions = [
`<option value="">— keine Mutter —</option>`,
...weibliche.map(h =>
`<option value="${h.id}" ${v.mutter_id === h.id ? 'selected' : ''}>${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`),
].join('');
const body = `
<form id="zh-hund-form" autocomplete="off">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Vollständiger Name <span style="color:var(--c-danger)">*</span></label>
<input class="form-control" type="text" name="name" required
value="${_esc(v.name || '')}" placeholder="z. B. Banyaro's Black Diamond">
</div>
<div class="form-group">
<label class="form-label">Rufname</label>
<input class="form-control" type="text" name="rufname"
value="${_esc(v.rufname || '')}" placeholder="z. B. Diamond">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Geschlecht</label>
<select class="form-control" name="geschlecht">
<option value="" ${!v.geschlecht ? 'selected' : ''}>—</option>
<option value="maennlich" ${v.geschlecht === 'maennlich' ? 'selected' : ''}>Männlich</option>
<option value="weiblich" ${v.geschlecht === 'weiblich' ? 'selected' : ''}>Weiblich</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Geburtsdatum</label>
<input class="form-control" type="date" name="geburtsdatum"
value="${_esc(v.geburtsdatum || '')}">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Sterbedatum</label>
<input class="form-control" type="date" name="sterbedatum"
value="${_esc(v.sterbedatum || '')}">
</div>
<div class="form-group">
<label class="form-label">Farbe / Fell</label>
<input class="form-control" type="text" name="farbe"
value="${_esc(v.farbe || '')}" placeholder="z. B. schwarz-braun">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Chip-Nr.</label>
<input class="form-control" type="text" name="chip_nr"
value="${_esc(v.chip_nr || '')}" placeholder="15-stellig">
</div>
<div class="form-group">
<label class="form-label">Tätowiernummer</label>
<input class="form-control" type="text" name="taetowier_nr"
value="${_esc(v.taetowier_nr || '')}">
</div>
</div>
<div class="form-group">
<label class="form-label">Zuchtbuchnummer</label>
<input class="form-control" type="text" name="zuchtbuchnummer"
value="${_esc(v.zuchtbuchnummer || '')}" placeholder="z. B. SZ 123456">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Vater</label>
<select class="form-control" name="vater_id">${vaterOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Mutter</label>
<select class="form-control" name="mutter_id">${mutterOptions}</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Züchter-Name</label>
<input class="form-control" type="text" name="zuechter_name"
value="${_esc(v.zuechter_name || '')}" placeholder="Bei Fremdzüchter">
</div>
<div class="form-group">
<label class="form-label">Eigentümer-Name</label>
<input class="form-control" type="text" name="eigentuemer_name"
value="${_esc(v.eigentuemer_name || '')}">
</div>
</div>
<div class="form-group">
<label class="form-label">Notiz <span style="color:var(--c-text-secondary)">(intern)</span></label>
<textarea class="form-control" name="notiz" rows="2"
placeholder="Interne Anmerkungen…">${_esc(v.notiz || '')}</textarea>
</div>
<div class="form-group">
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="is_public" value="1" ${v.is_public ? 'checked' : ''}>
Öffentlich sichtbar (im Züchterprofil)
</label>
</div>
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhf-cancel">Abbrechen</button>
<button type="submit" form="zh-hund-form" class="btn btn-primary flex-1" id="zhf-submit">
${isEdit ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Anlegen`}
</button>`;
UI.modal.open({
title: isEdit ? `${UI.icon('pencil-simple')} Hund bearbeiten` : `${UI.icon('dog')} Hund anlegen`,
body,
footer,
});
document.getElementById('zhf-cancel')?.addEventListener('click', UI.modal.close);
document.getElementById('zh-hund-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('zhf-submit');
const fd = new FormData(e.target);
const payload = {
name: fd.get('name')?.trim() || '',
rufname: fd.get('rufname')?.trim() || null,
geschlecht: fd.get('geschlecht') || null,
geburtsdatum: fd.get('geburtsdatum') || null,
sterbedatum: fd.get('sterbedatum') || null,
chip_nr: fd.get('chip_nr')?.trim() || null,
taetowier_nr: fd.get('taetowier_nr')?.trim() || null,
zuchtbuchnummer: fd.get('zuchtbuchnummer')?.trim() || null,
farbe: fd.get('farbe')?.trim() || null,
vater_id: fd.get('vater_id') ? parseInt(fd.get('vater_id')) : null,
mutter_id: fd.get('mutter_id') ? parseInt(fd.get('mutter_id')) : null,
zuechter_name: fd.get('zuechter_name')?.trim() || null,
eigentuemer_name: fd.get('eigentuemer_name')?.trim() || null,
notiz: fd.get('notiz')?.trim() || null,
is_public: fd.get('is_public') === '1' ? 1 : 0,
};
await UI.asyncButton(btn, async () => {
if (isEdit) {
const updated = await API.zuchthunde.update(hund.id, payload);
const idx = _hunde.findIndex(x => x.id === hund.id);
if (idx !== -1) _hunde[idx] = { ..._hunde[idx], ...updated };
UI.toast.success('Hund aktualisiert.');
} else {
const created = await API.zuchthunde.create(payload);
_hunde.unshift(created);
UI.toast.success('Hund angelegt.');
}
UI.modal.close();
_renderList();
});
});
}
// ----------------------------------------------------------
// Hund löschen
// ----------------------------------------------------------
async function _deleteHund(id) {
const h = _hunde.find(x => x.id === id);
const label = h?.name || `Hund #${id}`;
if (!window.confirm(`${label}" wirklich löschen? Alle Tests und Titel werden ebenfalls gelöscht.`)) return;
try {
await API.zuchthunde.remove(id);
_hunde = _hunde.filter(x => x.id !== id);
delete _openSections[id];
_renderList();
UI.toast.success('Hund gelöscht.');
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Löschen.');
}
}
// ----------------------------------------------------------
// Gesundheitstest hinzufügen
// ----------------------------------------------------------
function _showHealthTestForm(hundId) {
const today = new Date().toISOString().slice(0, 10);
const body = `
<form id="zh-health-form" autocomplete="off">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Test-Typ <span style="color:var(--c-danger)">*</span></label>
<select class="form-control" name="test_typ" id="zh-health-typ" required>
<option value="HD">HD (Hüftgelenksdysplasie)</option>
<option value="ED">ED (Ellbogendysplasie)</option>
<option value="OCD">OCD</option>
<option value="Augen">Augen</option>
<option value="Herz">Herz</option>
<option value="Patella">Patella</option>
<option value="ZTP">ZTP (Wesenstest)</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div class="form-group" id="zh-health-name-wrap">
<label class="form-label">Test-Name <span style="color:var(--c-text-secondary)">(bei Sonstiges)</span></label>
<input class="form-control" type="text" name="test_name" placeholder="Bezeichnung des Tests">
</div>
</div>
<div class="form-group">
<label class="form-label">Ergebnis <span style="color:var(--c-danger)">*</span></label>
<input class="form-control" type="text" name="ergebnis" required
id="zh-health-ergebnis" placeholder="z. B. A1, A2, B1 …">
<small class="form-hint" id="zh-health-hint" style="color:var(--c-text-secondary);font-size:var(--text-xs)">
HD: A1/A2 = frei, B1/B2 = noch zugelassen, C = Grenzfall, D/E = nicht zugelassen
</small>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Untersuchungsdatum</label>
<input class="form-control" type="date" name="untersuch_am" value="${today}">
</div>
<div class="form-group">
<label class="form-label">Gültig bis</label>
<input class="form-control" type="date" name="gueltig_bis">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Untersucher / Tierarzt</label>
<input class="form-control" type="text" name="untersucher" placeholder="Dr. Müller">
</div>
<div class="form-group">
<label class="form-label">Labor / Institut</label>
<input class="form-control" type="text" name="labor">
</div>
</div>
<div class="form-group">
<label class="form-label">Zertifikat-Nr.</label>
<input class="form-control" type="text" name="zertifikat_nr">
</div>
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhh-cancel">Abbrechen</button>
<button type="submit" form="zh-health-form" class="btn btn-primary flex-1" id="zhh-submit">
${UI.icon('floppy-disk')} Speichern
</button>`;
UI.modal.open({
title: `${UI.icon('heart')} Gesundheitstest hinzufügen`,
body,
footer,
});
document.getElementById('zhh-cancel')?.addEventListener('click', UI.modal.close);
// Hinweis je nach Test-Typ aktualisieren
const typSelect = document.getElementById('zh-health-typ');
const hintEl = document.getElementById('zh-health-hint');
const hints = {
HD: 'HD: A1/A2 = frei, B1/B2 = noch zugelassen, C = Grenzfall, D/E = nicht zugelassen',
ED: 'ED: 0 = frei, 1 = leicht, 2 = mittel, 3 = schwer',
OCD: 'OCD: positiv / negativ',
Augen: 'z. B. CAER-Ergebnis, frei / betroffen / Träger',
Herz: 'z. B. frei (Auskultation), SAS-Grad 1/2/3',
Patella: 'Patella: 0 = frei, 1/2/3 = Luxation Grad 13',
ZTP: 'z. B. bestanden / nicht bestanden',
Sonstiges: 'Ergebnis frei eingeben',
};
typSelect?.addEventListener('change', () => {
if (hintEl) hintEl.textContent = hints[typSelect.value] || '';
});
document.getElementById('zh-health-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('zhh-submit');
const fd = new FormData(e.target);
const payload = {
test_typ: fd.get('test_typ') || 'Sonstiges',
test_name: fd.get('test_name')?.trim() || null,
ergebnis: fd.get('ergebnis')?.trim() || '',
untersuch_am: fd.get('untersuch_am') || null,
gueltig_bis: fd.get('gueltig_bis') || null,
untersucher: fd.get('untersucher')?.trim() || null,
labor: fd.get('labor')?.trim() || null,
zertifikat_nr: fd.get('zertifikat_nr')?.trim() || null,
};
await UI.asyncButton(btn, async () => {
await API.zuchthunde.addHealthTest(hundId, payload);
UI.toast.success('Gesundheitstest gespeichert.');
UI.modal.close();
_openSections[hundId] = 'health';
await _reloadHealthSection(hundId);
});
});
}
// ----------------------------------------------------------
// Gentest hinzufügen
// ----------------------------------------------------------
function _showGeneticTestForm(hundId) {
const today = new Date().toISOString().slice(0, 10);
const body = `
<form id="zh-genetic-form" autocomplete="off">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Marker / Gen <span style="color:var(--c-danger)">*</span></label>
<select class="form-control" name="marker_name" required>
<option value="MDR1">MDR1 (Multi-Drug Resistance)</option>
<option value="PRA-prcd">PRA-prcd (Progressive Retinaatrophie)</option>
<option value="DM">DM (Degenerative Myelopathie)</option>
<option value="vWD">vWD (von-Willebrand-Krankheit)</option>
<option value="HUU">HUU (Harnsäure-Urolithiasis)</option>
<option value="Fell Locus A">Fell Locus A</option>
<option value="Fell Locus B">Fell Locus B</option>
<option value="Fell Locus E">Fell Locus E</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Ergebnis <span style="color:var(--c-danger)">*</span></label>
<select class="form-control" name="ergebnis_klasse" required>
<option value="clear">clear (frei)</option>
<option value="carrier">carrier (Träger)</option>
<option value="affected">affected (betroffen)</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Testdatum</label>
<input class="form-control" type="date" name="getestet_am" value="${today}">
</div>
<div class="form-group">
<label class="form-label">Labor</label>
<input class="form-control" type="text" name="labor" placeholder="z. B. Laboklin, Embark">
</div>
</div>
<div class="form-group">
<label class="form-label">Zertifikat-Nr.</label>
<input class="form-control" type="text" name="zertifikat_nr">
</div>
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhg-cancel">Abbrechen</button>
<button type="submit" form="zh-genetic-form" class="btn btn-primary flex-1" id="zhg-submit">
${UI.icon('floppy-disk')} Speichern
</button>`;
UI.modal.open({
title: `${UI.icon('dna')} Gentest hinzufügen`,
body,
footer,
});
document.getElementById('zhg-cancel')?.addEventListener('click', UI.modal.close);
document.getElementById('zh-genetic-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('zhg-submit');
const fd = new FormData(e.target);
const payload = {
marker_name: fd.get('marker_name') || 'Sonstiges',
ergebnis_klasse: fd.get('ergebnis_klasse') || 'clear',
getestet_am: fd.get('getestet_am') || null,
labor: fd.get('labor')?.trim() || null,
zertifikat_nr: fd.get('zertifikat_nr')?.trim() || null,
};
await UI.asyncButton(btn, async () => {
await API.zuchthunde.addGeneticTest(hundId, payload);
UI.toast.success('Gentest gespeichert.');
UI.modal.close();
_openSections[hundId] = 'genetic';
await _reloadGeneticSection(hundId);
});
});
}
// ----------------------------------------------------------
// Titel hinzufügen
// ----------------------------------------------------------
function _showTitleForm(hundId) {
const today = new Date().toISOString().slice(0, 10);
const body = `
<form id="zh-title-form" autocomplete="off">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Titel-Typ <span style="color:var(--c-danger)">*</span></label>
<select class="form-control" name="titel_typ" required>
<option value="Ausstellung">Ausstellung</option>
<option value="Arbeit">Arbeit</option>
<option value="Sport">Sport</option>
<option value="Zucht">Zucht</option>
<option value="Champion">Champion</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Titel-Name <span style="color:var(--c-danger)">*</span></label>
<input class="form-control" type="text" name="titel_name" required
placeholder="z. B. CAC, CACIB, BOB, IPO 1, BH">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Datum</label>
<input class="form-control" type="date" name="verliehen_am" value="${today}">
</div>
<div class="form-group">
<label class="form-label">Formwert</label>
<select class="form-control" name="formwert">
<option value="">—</option>
<option value="V">V (Vorzüglich)</option>
<option value="SG">SG (Sehr gut)</option>
<option value="G">G (Gut)</option>
<option value="A">A (Ausreichend)</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Ort</label>
<input class="form-control" type="text" name="ort" placeholder="Stadt / Veranstaltungsort">
</div>
<div class="form-group">
<label class="form-label">Richter</label>
<input class="form-control" type="text" name="richter">
</div>
</div>
<div class="form-group">
<label class="form-label">Ausstellung / Veranstaltung</label>
<input class="form-control" type="text" name="ausstellung" placeholder="z. B. Bundesiegerausstellung 2025">
</div>
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zht-cancel">Abbrechen</button>
<button type="submit" form="zh-title-form" class="btn btn-primary flex-1" id="zht-submit">
${UI.icon('floppy-disk')} Speichern
</button>`;
UI.modal.open({
title: `${UI.icon('trophy')} Titel hinzufügen`,
body,
footer,
});
document.getElementById('zht-cancel')?.addEventListener('click', UI.modal.close);
document.getElementById('zh-title-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('zht-submit');
const fd = new FormData(e.target);
const payload = {
titel_typ: fd.get('titel_typ') || 'Sonstiges',
titel_name: fd.get('titel_name')?.trim() || '',
verliehen_am: fd.get('verliehen_am') || null,
ort: fd.get('ort')?.trim() || null,
richter: fd.get('richter')?.trim() || null,
ausstellung: fd.get('ausstellung')?.trim() || null,
formwert: fd.get('formwert') || null,
};
await UI.asyncButton(btn, async () => {
await API.zuchthunde.addTitle(hundId, payload);
UI.toast.success('Titel gespeichert.');
UI.modal.close();
_openSections[hundId] = 'titles';
await _reloadTitlesSection(hundId);
});
});
}
// ----------------------------------------------------------
// Probeverpaarung
// ----------------------------------------------------------
function _showTrialMatingModal() {
const vaeterOptions = [
`<option value="">-- Aus eigenen Hunden --</option>`,
..._hunde
.filter(h => h.geschlecht !== 'weiblich')
.map(h => `<option value="${h.id}">${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`),
].join('');
const muetterOptions = [
`<option value="">-- Aus eigenen Hunden --</option>`,
..._hunde
.filter(h => h.geschlecht !== 'maennlich')
.map(h => `<option value="${h.id}">${_esc(h.name)}${h.rufname ? ` (${_esc(h.rufname)})` : ''}</option>`),
].join('');
const body = `
<form id="trial-form" style="display:flex;flex-direction:column;gap:var(--space-4)">
<div class="form-group">
<label class="form-label">Vater</label>
<select class="form-control" name="vater_id">${vaeterOptions}</select>
</div>
<div class="form-group">
<label class="form-label">Mutter</label>
<select class="form-control" name="mutter_id">${muetterOptions}</select>
</div>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:0">
Wähle Vater und Mutter aus deinen eigenen Hunden, um den Inzuchtkoeffizienten der möglichen Nachkommen zu berechnen.
</p>
</form>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhtrial-cancel">Abbrechen</button>
<button type="button" class="btn btn-primary flex-1" id="zhtrial-calc">
${UI.icon('calculator')} Berechnen
</button>`;
UI.modal.open({
title: `${UI.icon('dna')} Probeverpaarung`,
body,
footer,
});
document.getElementById('zhtrial-cancel')?.addEventListener('click', UI.modal.close);
document.getElementById('zhtrial-calc')?.addEventListener('click', async () => {
const form = document.getElementById('trial-form');
const vaterId = form?.querySelector('[name="vater_id"]')?.value || null;
const mutterId = form?.querySelector('[name="mutter_id"]')?.value || null;
if (!vaterId || !mutterId) {
UI.toast.error('Bitte Vater und Mutter auswählen.');
return;
}
const btn = document.getElementById('zhtrial-calc');
await UI.asyncButton(btn, async () => {
await _runTrialMating(vaterId, mutterId);
});
});
}
async function _runTrialMating(vaterId, mutterId) {
const result = await API.zuchthunde.trialMating(vaterId, mutterId);
_showTrialResult(result);
}
function _showTrialResult(result) {
const ik = parseFloat(result.ik_prozent || 0);
let ampelColor, ampelLabel;
if (ik < 2.5) {
ampelColor = '#22C55E';
ampelLabel = 'Optimal';
} else if (ik < 6.25) {
ampelColor = '#86EFAC';
ampelLabel = 'Akzeptabel';
} else if (ik < 12.5) {
ampelColor = '#F97316';
ampelLabel = 'Erhöht';
} else {
ampelColor = '#EF4444';
ampelLabel = 'Kritisch';
}
const vorfahrenRows = (result.gemeinsame_vorfahren || []).length
? (result.gemeinsame_vorfahren || []).map(v => {
const genInfo = [];
if (v.gen_vater != null) genInfo.push(`Gen. ${v.gen_vater} Vater`);
if (v.gen_mutter != null) genInfo.push(`Gen. ${v.gen_mutter} Mutter`);
const genStr = genInfo.length ? ` <span style="color:var(--c-text-secondary);font-size:var(--text-xs)">(${genInfo.join(' / ')})</span>` : '';
return `<li style="padding:var(--space-1) 0">${_esc(v.name || '—')}${genStr}</li>`;
}).join('')
: `<li style="color:var(--c-text-muted)">Keine gemeinsamen Vorfahren gefunden.</li>`;
const body = `
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3);
background:var(--c-surface-subtle,var(--c-bg));border-radius:var(--radius-md);
border:1px solid var(--c-border)">
<div style="width:14px;height:14px;border-radius:50%;background:${ampelColor};flex-shrink:0"></div>
<div>
<div style="font-size:var(--text-lg);font-weight:var(--weight-bold)">
${ik.toFixed(2)} %
</div>
<div style="font-size:var(--text-sm);color:${ampelColor};font-weight:var(--weight-semibold)">
${_esc(result.ik_rating || ampelLabel)}
</div>
</div>
</div>
<div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);margin-bottom:var(--space-2)">
Gemeinsame Vorfahren
</div>
<ul style="margin:0;padding-left:var(--space-5);font-size:var(--text-sm)">
${vorfahrenRows}
</ul>
</div>
</div>`;
const footer = `
<button type="button" class="btn btn-secondary flex-1" id="zhresult-back">
${UI.icon('arrow-left')} Zurück
</button>
<button type="button" class="btn btn-primary flex-1" id="zhresult-close">Schließen</button>`;
UI.modal.open({
title: `${UI.icon('dna')} Ergebnis Probeverpaarung`,
body,
footer,
});
document.getElementById('zhresult-close')?.addEventListener('click', UI.modal.close);
document.getElementById('zhresult-back')?.addEventListener('click', () => _showTrialMatingModal());
}
// ----------------------------------------------------------
// CSS (einmalig injizieren)
// ----------------------------------------------------------
(function _injectStyles() {
if (document.getElementById('zh-styles')) return;
const s = document.createElement('style');
s.id = 'zh-styles';
s.textContent = `
.zh-layout { padding: var(--space-4) 0; }
.zh-card {
background: var(--c-surface);
border: 1px solid var(--c-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-3);
overflow: hidden;
}
.zh-card-header {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
}
.zh-card-title {
font-size: var(--text-base);
font-weight: var(--weight-semibold);
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
margin-bottom: var(--space-1);
}
.zh-card-meta {
font-size: var(--text-sm);
color: var(--c-text-secondary);
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-1);
margin-top: 2px;
}
.zh-card-actions {
display: flex;
align-items: center;
gap: var(--space-1);
flex-shrink: 0;
}
.zh-section-buttons {
display: flex;
gap: var(--space-1);
padding: 0 var(--space-4) var(--space-2);
flex-wrap: wrap;
border-top: 1px solid var(--c-border);
padding-top: var(--space-2);
}
.zh-section-wrap {
border-top: 1px solid var(--c-border);
background: var(--c-surface-subtle, var(--c-bg));
}
.zh-section-inner {
padding: var(--space-3) var(--space-4);
}
.zh-section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-3);
}
.zh-detail-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.zh-detail-row {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
border-radius: var(--radius-md);
background: var(--c-surface);
border: 1px solid var(--c-border);
}
.zh-detail-info {
flex: 1;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--space-2);
}
.zh-detail-label {
font-size: var(--text-sm);
font-weight: var(--weight-medium);
}
.zh-badge {
display: inline-block;
padding: 1px 7px;
border-radius: 99px;
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: #fff;
white-space: nowrap;
}
`;
document.head.appendChild(s);
})();
return { init, refresh, onDogChange };
})();