/* ============================================================ 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 = {}; // { : '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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 `${_esc(ergebnis || '—')}`; } 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 `${_esc(ergebnis || '—')}`; } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; const u = _appState.user; if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) { _container.innerHTML = `
${UI.icon('lock')}

Kein Zugriff

Diese Seite ist nur für verifizierte Züchter.

`; 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 ? `Logo` : `
`; return `
${logoHtml}

${_esc(zwinger)}

Privater Bereich · Nur du siehst das
`; } function _render() { _container.innerHTML = `
${_privateHeader()}

${UI.icon('dog')} Zuchtkartei

${UI.icon('download-simple')} Export ${_appState?.user?.ki_zucht_jahresbericht !== 0 ? ` ${UI.icon('chart-bar')} Jahresbericht ${UI.icon('archive')} ` : ''}

Lädt…

`; 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; }).catch(() => {}), ]); _renderList(); } catch (err) { const el = document.getElementById('zh-list'); if (el) el.innerHTML = `

${_esc(err.message || 'Fehler beim Laden.')}

`; } } // ---------------------------------------------------------- // 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 ? `

Keine Treffer für „${_esc(_query)}".

` : `
${UI.icon('dog')}

Noch keine Hunde angelegt.

`; 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) : 'Unbenannt'; 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('  ·  '); const pubLabel = h.is_public ? `${UI.icon('eye')} Öffentlich` : ''; return `
${_genderIcon(h.geschlecht)} ${nameLabel}${_esc(rufname)} ${pubLabel}
${h.rasse ? `${UI.icon('paw-print')} ${_esc(h.rasse)}  ` : ''} ${geburtstag ? `${UI.icon('calendar-dots')} ${geburtstag}  ` : ''} ${h.chip_nr ? `${UI.icon('barcode')} ${_esc(h.chip_nr)}  ` : ''} ${h.zuchtbuchnummer ? `${UI.icon('book-open')} ${_esc(h.zuchtbuchnummer)}  ` : ''}
${eltern ? `
${eltern}
` : ''}
${_appState.user?.ki_zucht_beschreibung !== 0 ? ` ` : ''}
`; } // ---------------------------------------------------------- // 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 = `

Lädt…

`; 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 = `

${_esc(err.message || 'Fehler.')}

`; } } function _renderHealthSection(hundId, wrap, tests) { const rows = tests.length ? tests.map(t => `
${_esc(t.test_typ || 'Sonstiges')} ${t.test_name ? `${_esc(t.test_name)}` : ''} ${_healthBadge(t.test_typ || '', t.ergebnis)} ${t.untersuch_am ? `${_fmtDate(t.untersuch_am)}` : ''} ${t.labor ? `${_esc(t.labor)}` : ''}
`).join('') : `

Noch keine Gesundheitstests eingetragen.

`; wrap.innerHTML = `
${UI.icon('heart')} Gesundheitstests
${rows}
`; 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 = `

${_esc(err.message || 'Fehler.')}

`; } } function _renderGeneticSection(hundId, wrap, tests) { const rows = tests.length ? tests.map(t => `
${_esc(t.marker_name || '—')} ${_geneticBadge(t.ergebnis_klasse)} ${t.getestet_am ? `${_fmtDate(t.getestet_am)}` : ''} ${t.labor ? `${_esc(t.labor)}` : ''}
`).join('') : `

Noch keine Gentests eingetragen.

`; wrap.innerHTML = `
${UI.icon('dna')} Gentests
${rows}
`; 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 = `

${_esc(err.message || 'Fehler.')}

`; } } function _renderTitlesSection(hundId, wrap, titles) { const rows = titles.length ? titles.map(t => `
${_esc(t.titel_name || '—')} ${t.titel_typ ? `${_esc(t.titel_typ)}` : ''} ${t.verliehen_am ? `${_fmtDate(t.verliehen_am)}` : ''} ${t.ort ? `${_esc(t.ort)}` : ''} ${t.richter ? `${_esc(t.richter)}` : ''} ${t.formwert ? `${_esc(t.formwert)}` : ''}
`).join('') : `

Noch keine Titel eingetragen.

`; wrap.innerHTML = `
${UI.icon('trophy')} Titel
${rows}
`; 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 = [ ``, ...maennliche.map(h => ``), ].join(''); const mutterOptions = [ ``, ...weibliche.map(h => ``), ].join(''); const body = `
`; const footer = ` `; 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 = `
HD: A1/A2 = frei, B1/B2 = noch zugelassen, C = Grenzfall, D/E = nicht zugelassen
`; const footer = ` `; 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 1–3', 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 = `
`; const footer = ` `; 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 = `
`; const footer = ` `; 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 = [ ``, ..._hunde .filter(h => h.geschlecht !== 'weiblich') .map(h => ``), ].join(''); const muetterOptions = [ ``, ..._hunde .filter(h => h.geschlecht !== 'maennlich') .map(h => ``), ].join(''); const body = `

Wähle Vater und Mutter aus deinen eigenen Hunden, um den Inzuchtkoeffizienten der möglichen Nachkommen zu berechnen.

`; const footer = ` `; 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 ? ` (${genInfo.join(' / ')})` : ''; return `
  • ${_esc(v.name || '—')}${genStr}
  • `; }).join('') : `
  • Keine gemeinsamen Vorfahren gefunden.
  • `; 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 => `
    ${UI.icon('warning')} ${_esc(i.text)}
    `).join(''); const wOkHTML = (welfare.ok_points || []).map(p => `
    ${UI.icon('check')} ${_esc(p)}
    `).join(''); welfareHTML = `
    ${UI.icon(wIcon)} Tierschutz-Check: ${wTitle}
    ${wIssueHTML || ''} ${wOkHTML}
    `; } const body = `
    ${ik.toFixed(2)} %
    ${_esc(result.ik_rating || ampelLabel)}
    ${welfareHTML}
    Gemeinsame Vorfahren
      ${vorfahrenRows}
    `; const kiPaarungBtn = _appState?.user?.ki_zucht_paarung !== 0 ? `` : ''; const footer = `
    ${kiPaarungBtn}
    `; 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: `

    KI erstellt Beschreibung…

    `, 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: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); return; } UI.modal.open({ title: `${UI.icon('sparkle')} KI-Hunde-Beschreibung`, body: `
    ${_esc(text)}
    `, footer: ` `, }); 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: `
    ${UI.icon('warning')}

    Du hast bereits einen Bericht vom ${datumStr}.

    Ein neuer Bericht kostet ein KI-Guthaben. Was möchtest du tun?

    `, footer: ` `, }); 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: `

    KI analysiert deine Zuchtkartei…

    `, 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: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); return; } _renderBerichtModal(text, jahr, savedId); } function _renderBerichtModal(text, jahr, savedId) { UI.modal.open({ title: `${UI.icon('chart-bar')} KI-Jahresbericht ${jahr}`, body: ` ${savedId ? `

    ${UI.icon('check-circle')} Automatisch gespeichert

    ` : ''}
    ${_esc(text)}
    `, footer: ` `, }); 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: `

    Lädt…

    `, footer: ``, }); 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: `

    Noch keine Berichte gespeichert.

    `, footer: ``, }); return; } const listHtml = berichte.map(b => `
    Jahresbericht ${b.jahr}
    ${new Date(b.created_at).toLocaleDateString('de', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})}
    `).join(''); UI.modal.open({ title: `${UI.icon('archive')} Gespeicherte Jahresberichte`, body: `
    ${listHtml}
    `, footer: ``, }); 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: `

    KI analysiert die Verpaarung…

    `, 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: `

    ${_esc(err.message || 'Fehler beim Generieren.')}

    `, footer: ``, }); 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: `
    ${empfehlung ? `
    ${UI.icon('check-circle')} ${_esc(empfehlungLabel)}
    ` : ''}
    ${_esc(text)}
    `, footer: ` `, }); 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: `

    Diese Fotos erscheinen im öffentlichen Züchterprofil. Das primäre Foto wird als Logo im Hero angezeigt.

    Lädt…


    `, footer: ``, }); 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 = `

    Noch keine Fotos — lade das erste hoch.

    `; return; } el.innerHTML = `
    ${photos.map(ph => { const thumb = ph.thumbnail_url || ph.url || ''; const vis = visLabels[ph.visibility] || visLabels.private; const isPrimary = ph.is_primary; return `
    ${_esc(ph.caption || '')} ${isPrimary ? `Logo` : ''} ${!isPrimary ? `` : ''}
    `; }).join('')}
    `; 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 = `

    ${_esc(err.message || 'Fehler')}

    `; } } _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 }; })();