banyaro/backend/static/js/pages/zuchthunde.js
rene 459cd425f2 Design-System Sprint A: utilities.css + 948 Inline-Styles → Utility-Klassen, SW by-v1102
PHASE 1 — Sofort-Cleanup ohne Risiko:
- Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen:
  * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary
  * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3
  * flex-between, flex-1-min, mb-1/3, mt-1/3
  * icon-xs/sm/md/lg, label-block, caption
- index.html bindet utilities.css ein
- mb-3/mt-3 ergänzt (waren in design-system.css unvollständig)

PHASE 2 — .by-tab Modifier für Vereinheitlichung:
- .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.)
- .by-tabs.sticky (Desktop vertikale Tabs für Admin)
- .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll)
- .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border)

PHASE 3 — Inline-Style → Klassen-Migration (Python-Script):
- 948 Inline-Styles entfernt (5101 → 4153, -18%)
- 962 Migrationen über 47 Page-Dateien
- Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67),
  litters.js (62), settings.js (61), zuchthunde.js (51)
- Patterns: text-muted, text-secondary, text-danger, text-xs-muted,
  text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3,
  p-3/4, mb-2/3/4, hidden, w-full, flex-1, ...
- Bewahrt bestehende class-Attribute (mergt korrekt)

Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
2026-05-27 07:11:27 +02:00

1837 lines
76 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 }
let _breederId = null; // ID des Züchter-Profils
let _breederInfo = null; // { zwingername, logo_url }
// ----------------------------------------------------------
// 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 _privateHeader() {
const zwinger = _breederInfo?.zwingername || 'Mein Zwinger';
const logoUrl = _breederInfo?.logo_url || null;
const logoHtml = logoUrl
? `<img src="${_esc(logoUrl)}" alt="Logo"
style="width:48px;height:48px;border-radius:50%;object-fit:cover;
border:2px solid rgba(196,132,58,.5);flex-shrink:0"
onerror="this.style.display='none'">`
: `<div style="width:48px;height:48px;border-radius:50%;background:rgba(196,132,58,.15);
border:2px solid rgba(196,132,58,.4);display:flex;align-items:center;
justify-content:center;flex-shrink:0">
<svg style="width:24px;height:24px;color:var(--c-primary)" viewBox="0 0 256 256">
<use href="/icons/phosphor.svg#paw-print"></use>
</svg>
</div>`;
return `
<div id="breeder-private-header" style="background:var(--c-bg-secondary);
border-bottom:1px solid var(--c-border);
padding:var(--space-3) var(--space-4);
display:flex;align-items:center;gap:var(--space-3)">
${logoHtml}
<div class="flex-1-min">
<h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700;
color:var(--c-text);white-space:nowrap;overflow:hidden;
text-overflow:ellipsis;line-height:1.2">${_esc(zwinger)}</h2>
<div style="display:flex;align-items:center;gap:var(--space-2)">
<svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256">
<use href="/icons/phosphor.svg#lock-key"></use>
</svg>
<span class="text-xs-secondary">Privater Bereich · Nur du siehst das</span>
</div>
</div>
</div>`;
}
function _render() {
_container.innerHTML = `
<div class="zh-layout">
${_privateHeader()}
<div class="by-toolbar" style="flex-wrap:wrap">
<h2 style="margin:0;font-size:var(--text-lg);font-weight:var(--weight-semibold);flex-shrink:0;white-space:nowrap">
${UI.icon('dog')} Zuchtkartei
</h2>
<button class="btn btn-primary btn-sm" id="zh-new-btn" style="flex-shrink:0">
${UI.icon('plus')} Hund anlegen
</button>
<button class="btn btn-secondary btn-sm" id="zh-trial-btn" style="flex-shrink:0;white-space:nowrap">
${UI.icon('heart-fill')} Probeverpaarung
</button>
<button class="btn btn-ghost btn-sm" id="zh-photos-btn" style="flex-shrink:0;white-space:nowrap"
title="Fotos & Logo für das Züchter-Profil verwalten">
${UI.icon('images')} Profilfotos
</button>
<a href="/api/breeder/export" download class="btn btn-ghost btn-sm" id="zh-export-btn"
style="flex-shrink:0" title="Alle Daten herunterladen (HTML + ODS)">
${UI.icon('download-simple')} Export
</a>
${_appState?.user?.ki_zucht_jahresbericht !== 0 ? `
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-btn" style="flex-shrink:0;white-space:nowrap">
${UI.icon('chart-bar')} Jahresbericht
</a>
<a class="btn btn-ghost btn-sm" id="zh-jahresbericht-archiv-btn" style="flex-shrink:0" title="Frühere Berichte">
${UI.icon('archive')}
</a>` : ''}
</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-jahresbericht-btn')?.addEventListener('click', () => _showJahresbericht());
document.getElementById('zh-jahresbericht-archiv-btn')?.addEventListener('click', () => _showJahresberichtArchiv());
document.getElementById('zh-photos-btn')?.addEventListener('click', () => {
if (!_breederId) { UI.toast.warning('Züchter-Profil noch nicht geladen.'); return; }
_showBreederPhotosModal(_breederId);
});
document.getElementById('zh-search')?.addEventListener('input', e => {
_query = e.target.value.toLowerCase().trim();
_renderList();
});
}
// ----------------------------------------------------------
// Hunde laden
// ----------------------------------------------------------
async function _load() {
try {
[_hunde] = await Promise.all([
API.zuchthunde.list(),
API.breeder.status().then(s => {
_breederId = s?.profile?.id || null;
_breederInfo = s?.profile ? { zwingername: s.profile.zwingername, logo_url: s.profile.logo_url } : null;
const h = _container?.querySelector('#breeder-private-header');
if (h) h.outerHTML = _privateHeader();
}).catch(() => {}),
]);
_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 class="text-secondary">Noch keine Hunde angelegt.</p>
<button class="btn btn-primary mt-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);
});
});
el.querySelectorAll('.zh-ki-desc-btn').forEach(btn => {
btn.addEventListener('click', () => {
const id = parseInt(btn.dataset.id);
_showKiDesc(id);
});
});
// 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 class="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 class="flex-1-min">
<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 text-xs-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>
${_appState.user?.ki_zucht_beschreibung !== 0 ? `
<button class="btn btn-ghost btn-sm zh-ki-desc-btn" data-id="${h.id}">
${UI.icon('sparkle')} Beschreibung
</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" class="text-danger">
${UI.icon('trash')}
</button>
</div>
</div>
<div class="by-tabs" style="padding:var(--space-2) var(--space-3) var(--space-3);flex-wrap:wrap;overflow-x:visible">
<button class="by-tab zh-section-btn" data-id="${h.id}" data-section="health">
${UI.icon('heart')} Gesundheit
</button>
<button class="by-tab zh-section-btn" data-id="${h.id}" data-section="genetic">
${UI.icon('dna')} Genetik
</button>
<button class="by-tab 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}" class="hidden"></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 => {
btn.classList.toggle('active', btn.dataset.section === activeSection);
});
}
// ----------------------------------------------------------
// 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"
class="text-danger">${UI.icon('trash')}</button>
</div>`).join('')
: `<p class="text-sm-muted">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"
class="text-danger">${UI.icon('trash')}</button>
</div>`).join('')
: `<p class="text-sm-muted">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"
class="text-danger">${UI.icon('trash')}</button>
</div>`).join('')
: `<p class="text-sm-muted">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 class="grid-2">
<div class="form-group">
<label class="form-label">Vollständiger Name <span class="text-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 class="grid-2">
<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 class="grid-2">
<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 class="grid-2">
<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 class="grid-2">
<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 class="grid-2">
<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 class="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 class="grid-2">
<div class="form-group">
<label class="form-label">Test-Typ <span class="text-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 class="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 class="text-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 class="grid-2">
<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 class="grid-2">
<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 class="grid-2">
<div class="form-group">
<label class="form-label">Marker / Gen <span class="text-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 class="text-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 class="grid-2">
<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 class="grid-2">
<div class="form-group">
<label class="form-label">Titel-Typ <span class="text-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 class="text-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 class="grid-2">
<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 class="grid-2">
<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 class="text-muted">Keine gemeinsamen Vorfahren gefunden.</li>`;
const welfare = result.welfare;
let welfareHTML = '';
if (welfare) {
const wColor = { ok: '#16a34a', info: '#3b82f6', warning: '#f59e0b', critical: '#dc2626' }[welfare.level] || '#6b7280';
const wTitle = { ok: 'Alles prima', info: 'Hinweis', warning: 'Bitte beachten', critical: 'Kritischer Hinweis' }[welfare.level];
const wIcon = { ok: 'check-circle', info: 'info', warning: 'warning', critical: 'warning-circle' }[welfare.level];
const wIssueHTML = (welfare.issues || []).map(i => `
<div style="display:flex;gap:8px;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.06)">
<span style="color:${wColor};flex-shrink:0">${UI.icon('warning')}</span>
<span class="text-sm">${_esc(i.text)}</span>
</div>`).join('');
const wOkHTML = (welfare.ok_points || []).map(p => `
<div style="display:flex;gap:8px;padding:4px 0">
<span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span>
<span class="text-sm-secondary">${_esc(p)}</span>
</div>`).join('');
welfareHTML = `
<div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm);margin-bottom:var(--space-2);
color:${wColor}">
${UI.icon(wIcon)} Tierschutz-Check: ${wTitle}
</div>
<div style="background:${wColor}18;border:1.5px solid ${wColor}40;border-radius:var(--radius-md);
padding:var(--space-3)">
${wIssueHTML || ''}
${wOkHTML}
</div>
</div>`;
}
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>
${welfareHTML}
<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 kiPaarungBtn = _appState?.user?.ki_zucht_paarung !== 0
? `<button type="button" class="btn btn-secondary btn-sm" id="trial-ki-btn">
${UI.icon('sparkle')} KI-Analyse anfordern
</button>`
: '';
const footer = `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
${kiPaarungBtn}
<div class="flex-gap-2">
<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>
</div>
</div>`;
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());
document.getElementById('trial-ki-btn')?.addEventListener('click', () => _showKiPaarung(result));
}
// ----------------------------------------------------------
// KI: Hund-Beschreibung
// ----------------------------------------------------------
async function _showKiDesc(hundId) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI erstellt Beschreibung…</p>`,
footer: '',
});
let text = '';
try {
const result = await API.zuchtKi.hundBeschreibung(hundId);
text = result.text || result.content || result.beschreibung || JSON.stringify(result);
} catch (err) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`,
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
footer: `
<button class="btn btn-secondary flex-1" id="ki-desc-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
});
document.getElementById('ki-desc-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(text);
UI.toast.success('Text kopiert.');
} catch {
UI.toast.error('Kopieren nicht möglich.');
}
});
}
// ----------------------------------------------------------
// KI: Jahresbericht generieren
// ----------------------------------------------------------
async function _showJahresbericht() {
// Zuerst prüfen ob bereits ein Bericht existiert
let vorhandeneBerichte = [];
try { vorhandeneBerichte = await API.zuchtKi.jahresberichtList(); } catch {}
if (vorhandeneBerichte.length) {
const letzter = vorhandeneBerichte[0];
const letzterTs = new Date(letzter.created_at);
const tageAlt = Math.floor((Date.now() - letzterTs) / 86400000);
const SCHWELLE = 30; // Tage
if (tageAlt < SCHWELLE) {
const datumStr = letzterTs.toLocaleDateString('de', {
day: '2-digit', month: '2-digit', year: 'numeric'
});
// Wahlmöglichkeit anbieten
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `
<div style="text-align:center;padding:var(--space-4) 0">
<div style="font-size:2rem;margin-bottom:var(--space-3)">${UI.icon('warning')}</div>
<p style="margin:0 0 var(--space-2);font-size:var(--text-sm)">
Du hast bereits einen Bericht vom <strong>${datumStr}</strong>.
</p>
<p style="margin:0;font-size:var(--text-sm);color:var(--c-text-secondary)">
Ein neuer Bericht kostet ein KI-Guthaben. Was möchtest du tun?
</p>
</div>`,
footer: `
<button class="btn btn-secondary btn-sm" id="ki-bericht-abbrechen">
Schließen
</button>
<button class="btn btn-ghost btn-sm" id="ki-bericht-letzten-anzeigen">
${UI.icon('eye')} Letzten anzeigen (${datumStr})
</button>
<button class="btn btn-primary btn-sm" id="ki-bericht-neu-erstellen">
${UI.icon('sparkle')} Neuen erstellen
</button>`,
});
document.getElementById('ki-bericht-abbrechen')?.addEventListener('click', () => UI.modal.close());
document.getElementById('ki-bericht-letzten-anzeigen')?.addEventListener('click', async () => {
try {
const r = await API.zuchtKi.jahresberichtGet(letzter.id);
_renderBerichtModal(r.text, r.jahr, r.id);
} catch { UI.toast.error('Bericht konnte nicht geladen werden.'); }
});
document.getElementById('ki-bericht-neu-erstellen')?.addEventListener('click', () => _generiereJahresbericht());
return;
}
}
_generiereJahresbericht();
}
async function _generiereJahresbericht() {
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI analysiert deine Zuchtkartei…</p>`,
footer: '',
});
let text = '', savedId = null, jahr = new Date().getFullYear();
try {
const result = await API.zuchtKi.jahresbericht();
text = result.text || result.content || result.bericht || JSON.stringify(result);
savedId = result.saved_id;
jahr = result.jahr || jahr;
} catch (err) {
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
_renderBerichtModal(text, jahr, savedId);
}
function _renderBerichtModal(text, jahr, savedId) {
UI.modal.open({
title: `${UI.icon('chart-bar')} KI-Jahresbericht ${jahr}`,
body: `
${savedId ? `<p style="font-size:var(--text-xs);color:var(--c-success);margin:0 0 var(--space-3);display:flex;align-items:center;gap:4px">
${UI.icon('check-circle')} Automatisch gespeichert</p>` : ''}
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
footer: `
<button class="btn btn-ghost btn-sm" id="ki-bericht-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-ghost btn-sm" id="ki-bericht-download">
${UI.icon('download-simple')} Herunterladen
</button>
<button class="btn btn-primary" id="ki-bericht-close">Schließen</button>`,
});
document.getElementById('ki-bericht-close')?.addEventListener('click', () => UI.modal.close());
document.getElementById('ki-bericht-copy')?.addEventListener('click', async () => {
try { await navigator.clipboard.writeText(text); UI.toast.success('Bericht kopiert.'); }
catch { UI.toast.error('Kopieren nicht möglich.'); }
});
document.getElementById('ki-bericht-download')?.addEventListener('click', () => {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `ban-yaro-jahresbericht-${jahr}.txt`;
a.click(); URL.revokeObjectURL(url);
});
}
// KI: Archiv früherer Berichte
// ----------------------------------------------------------
async function _showJahresberichtArchiv() {
UI.modal.open({
title: `${UI.icon('archive')} Gespeicherte Jahresberichte`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-4)">Lädt…</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
let berichte = [];
try { berichte = await API.zuchtKi.jahresberichtList(); }
catch { UI.toast.error('Berichte konnten nicht geladen werden.'); return; }
if (!berichte.length) {
UI.modal.open({
title: `${UI.icon('archive')} Gespeicherte Jahresberichte`,
body: `<p style="color:var(--c-text-muted);text-align:center;padding:var(--space-6)">Noch keine Berichte gespeichert.</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
const listHtml = berichte.map(b => `
<div style="display:flex;align-items:center;justify-content:space-between;
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border-light)">
<div>
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
Jahresbericht ${b.jahr}
</div>
<div class="text-xs-muted">
${new Date(b.created_at).toLocaleDateString('de', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}
</div>
</div>
<button class="btn btn-ghost btn-sm" data-bericht-id="${b.id}" data-bericht-jahr="${b.jahr}">
${UI.icon('eye')} Lesen
</button>
</div>`).join('');
UI.modal.open({
title: `${UI.icon('archive')} Gespeicherte Jahresberichte`,
body: `<div style="padding:0 var(--space-1)">${listHtml}</div>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
document.querySelectorAll('[data-bericht-id]').forEach(btn => {
btn.addEventListener('click', async () => {
const id = Number(btn.dataset.berichtId);
const jahr = Number(btn.dataset.berichtJahr);
try {
const r = await API.zuchtKi.jahresberichtGet(id);
_renderBerichtModal(r.text, r.jahr || jahr, r.id);
} catch { UI.toast.error('Bericht konnte nicht geladen werden.'); }
});
});
}
// ----------------------------------------------------------
// KI: Paarungsanalyse
// ----------------------------------------------------------
async function _showKiPaarung(trialResult) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">KI analysiert die Verpaarung…</p>`,
footer: '',
});
let result;
try {
result = await API.zuchtKi.paarungAnalyse(
trialResult.vater_id,
trialResult.mutter_id,
trialResult.ik_prozent,
trialResult.welfare?.level
);
} catch (err) {
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `<p class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
});
return;
}
const empfehlung = result.empfehlung || result.recommendation || '';
const text = result.text || result.content || result.analyse || JSON.stringify(result);
const empfehlungColor = {
empfohlen: '#16a34a',
bedingt: '#f59e0b',
nicht_empfohlen: '#dc2626',
}[empfehlung] || '#6b7280';
const empfehlungLabel = {
empfohlen: 'Empfohlen',
bedingt: 'Bedingt empfohlen',
nicht_empfohlen: 'Nicht empfohlen',
}[empfehlung] || empfehlung;
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Paarungsanalyse`,
body: `
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
${empfehlung ? `
<div style="padding:var(--space-3);border-radius:var(--radius-md);
background:${empfehlungColor}18;border:1.5px solid ${empfehlungColor}40;
font-weight:var(--weight-semibold);color:${empfehlungColor}">
${UI.icon('check-circle')} ${_esc(empfehlungLabel)}
</div>` : ''}
<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>
</div>`,
footer: `
<button class="btn btn-secondary flex-1" id="ki-paarung-copy">
${UI.icon('clipboard-text')} Kopieren
</button>
<button class="btn btn-primary flex-1" data-modal-close>Schließen</button>`,
});
document.getElementById('ki-paarung-copy')?.addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(text);
UI.toast.success('Analyse kopiert.');
} catch {
UI.toast.error('Kopieren nicht möglich.');
}
});
}
// ----------------------------------------------------------
// 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);
})();
// ----------------------------------------------------------
// Profilfotos & Logo verwalten
// ----------------------------------------------------------
async function _showBreederPhotosModal(breederId) {
const galleryId = 'bp-gallery';
const visLabels = {
public: { text: 'Öffentlich', color: '#16a34a' },
inquiry: { text: 'Anfrage', color: '#f59e0b' },
private: { text: 'Privat', color: '#6b7280' },
};
const visOrder = ['public', 'inquiry', 'private'];
UI.modal.open({
title: `${UI.icon('images')} Züchter-Profilfotos & Logo`,
body: `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)">
Diese Fotos erscheinen im öffentlichen Züchterprofil. Das primäre Foto wird als <strong>Logo</strong> im Hero angezeigt.
</p>
<div id="${galleryId}" class="mb-4">
<p class="text-sm-muted">Lädt…</p>
</div>
<hr style="margin:var(--space-3) 0;border:none;border-top:1px solid var(--c-border)">
<form id="bp-upload-form" class="flex-col-gap-2">
<label style="font-size:var(--text-sm);font-weight:600">${UI.icon('upload-simple')} Foto hochladen</label>
<input class="form-control" type="file" name="file" accept="image/*" required>
<input class="form-control" type="text" name="caption" placeholder="Bildunterschrift (optional)">
</form>`,
footer: `<button type="submit" form="bp-upload-form" class="btn btn-primary" id="bp-upload-btn">
${UI.icon('upload-simple')} Hochladen
</button>`,
});
async function _loadGallery() {
const el = document.getElementById(galleryId);
if (!el) return;
try {
const photos = await API.breederPhotos.list('breeder', breederId);
if (!photos.length) {
el.innerHTML = `<p class="text-sm-muted">Noch keine Fotos — lade das erste hoch.</p>`;
return;
}
el.innerHTML = `
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:var(--space-2)">
${photos.map(ph => {
const thumb = ph.thumbnail_url || ph.url || '';
const vis = visLabels[ph.visibility] || visLabels.private;
const isPrimary = ph.is_primary;
return `
<div style="position:relative;border-radius:var(--radius-md);overflow:hidden;
border:${isPrimary ? '2px solid var(--c-primary)' : '1px solid var(--c-border)'};aspect-ratio:1"
data-photo-id="${ph.id}">
<a href="${_esc(ph.url || '')}" target="_blank" rel="noopener noreferrer">
<img src="${_esc(thumb)}" alt="${_esc(ph.caption || '')}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.parentElement.style.opacity='.4'">
</a>
${isPrimary ? `<span style="position:absolute;top:3px;left:3px;background:var(--c-primary);color:white;
font-size:9px;font-weight:700;border-radius:999px;padding:1px 5px">Logo</span>` : ''}
<!-- Sichtbarkeit -->
<button class="bp-vis-btn" data-photo-id="${ph.id}" data-vis="${_esc(ph.visibility)}"
style="position:absolute;bottom:0;left:0;right:0;background:${vis.color};color:#fff;
border:none;cursor:pointer;font-size:9px;padding:2px 4px;font-weight:700">
${vis.text}
</button>
<!-- Als Logo setzen -->
${!isPrimary ? `<button class="bp-primary-btn" data-photo-id="${ph.id}" title="Als Logo setzen"
style="position:absolute;top:2px;right:24px;background:rgba(0,0,0,.5);color:#fff;
border:none;border-radius:50%;cursor:pointer;width:20px;height:20px;
display:flex;align-items:center;justify-content:center;font-size:10px">
${UI.icon('star')}
</button>` : ''}
<!-- Löschen -->
<button class="bp-del-btn" data-photo-id="${ph.id}" title="Löschen"
style="position:absolute;top:2px;right:2px;background:rgba(0,0,0,.5);color:#fff;
border:none;border-radius:50%;cursor:pointer;width:20px;height:20px;
display:flex;align-items:center;justify-content:center;font-size:10px">
${UI.icon('x')}
</button>
</div>`;
}).join('')}
</div>`;
el.querySelectorAll('.bp-vis-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const next = visOrder[(visOrder.indexOf(btn.dataset.vis) + 1) % visOrder.length];
try { await API.breederPhotos.updateVisibility(parseInt(btn.dataset.photoId), next); _loadGallery(); }
catch (err) { UI.toast.error(err.message); }
});
});
el.querySelectorAll('.bp-primary-btn').forEach(btn => {
btn.addEventListener('click', async () => {
try { await API.breederPhotos.setPrimary(parseInt(btn.dataset.photoId)); _loadGallery(); UI.toast.success('Als Logo gesetzt.'); }
catch (err) { UI.toast.error(err.message); }
});
});
el.querySelectorAll('.bp-del-btn').forEach(btn => {
btn.addEventListener('click', async () => {
if (!window.confirm('Foto löschen?')) return;
try { await API.breederPhotos.remove(parseInt(btn.dataset.photoId)); _loadGallery(); }
catch (err) { UI.toast.error(err.message); }
});
});
} catch (err) {
const el = document.getElementById(galleryId);
if (el) el.innerHTML = `<p class="text-danger">${_esc(err.message || 'Fehler')}</p>`;
}
}
_loadGallery();
document.getElementById('bp-upload-form')?.addEventListener('submit', async e => {
e.preventDefault();
const btn = document.getElementById('bp-upload-btn');
const fileInput = e.target.querySelector('[name="file"]');
const caption = e.target.querySelector('[name="caption"]')?.value?.trim() || '';
if (!fileInput?.files?.length) { UI.toast.error('Bitte Datei auswählen.'); return; }
const fd = new FormData();
fd.append('entity_type', 'breeder');
fd.append('entity_id', String(breederId));
fd.append('visibility', 'public');
fd.append('caption', caption);
fd.append('file', fileInput.files[0]);
await UI.asyncButton(btn, async () => {
await API.breederPhotos.upload(fd);
UI.toast.success('Foto hochgeladen.');
e.target.reset();
await _loadGallery();
});
});
}
return { init, refresh, onDogChange };
})();