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).
1837 lines
76 KiB
JavaScript
1837 lines
76 KiB
JavaScript
/* ============================================================
|
||
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 =>
|
||
({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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(' · ');
|
||
|
||
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)} ` : ''}
|
||
${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)} ` : ''}
|
||
</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 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 = `
|
||
<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 };
|
||
|
||
})();
|