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

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

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

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

646 lines
30 KiB
JavaScript

/* BAN YARO — Läufigkeit & Trächtigkeit (Züchter) */
window.Page_laeufi = (() => {
let _container, _appState;
let _hunde = [];
let _openHundId = null;
let _breederInfo = null;
// ----------------------------------------------------------
// Init / Refresh
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
if (!appState.user || !['breeder','admin'].includes(appState.user.rolle)) {
_container.innerHTML = `<div style="text-align:center;padding:var(--space-10)">
<p class="text-secondary">Nur für verifizierte Züchter.</p></div>`;
return;
}
API.breeder.status().then(s => {
_breederInfo = s?.profile ? { zwingername: s.profile.zwingername, logo_url: s.profile.logo_url } : null;
const headerEl = _container.querySelector('#breeder-private-header');
if (headerEl) headerEl.outerHTML = _privateHeader();
}).catch(() => {});
_render();
await _loadHunde();
}
function refresh() { _loadHunde(); }
function onDogChange() {}
// ----------------------------------------------------------
// Grundstruktur
// ----------------------------------------------------------
function _privateHeader() {
const zwinger = _breederInfo?.zwingername || 'Mein Zwinger';
const logoUrl = _breederInfo?.logo_url || null;
const logoHtml = logoUrl
? `<img src="${UI.escape(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">${UI.escape(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 style="width:100%;max-width:860px;margin:0 auto;box-sizing:border-box">
${_privateHeader()}
<div class="by-toolbar" style="margin-bottom:var(--space-4);padding:0 var(--space-4)">
<h2 style="margin:0;font-size:var(--text-lg);font-weight:var(--weight-semibold)">
${UI.icon('thermometer')} Läufigkeit & Trächtigkeit
</h2>
</div>
<div id="laeufi-list" style="padding:0 var(--space-4) var(--space-6)">
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt…</p>
</div>
</div>`;
}
async function _loadHunde() {
try {
const alle = await API.zuchthunde.list();
_hunde = alle.filter(h => h.geschlecht === 'weiblich');
_renderHundeList();
} catch (err) {
document.getElementById('laeufi-list').innerHTML =
`<p class="text-danger">${UI.escape(err.message || 'Fehler')}</p>`;
}
}
function _renderHundeList() {
const el = document.getElementById('laeufi-list');
if (!_hunde.length) {
el.innerHTML = `
<div style="text-align:center;padding:var(--space-10) var(--space-4);
border:1px dashed var(--c-border);border-radius:var(--radius-lg)">
<div style="font-size:2.5rem;margin-bottom:var(--space-3)">${UI.icon('gender-female')}</div>
<p style="font-weight:600;color:var(--c-text)">Keine Hündinnen in der Zuchtkartei</p>
<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-2)">
Lege zuerst weibliche Hunde in der Zuchtkartei an.
</p>
</div>`;
return;
}
el.innerHTML = _hunde.map(h => _hundCardHTML(h)).join('');
_hunde.forEach(h => {
document.getElementById(`laeufi-toggle-${h.id}`)
?.addEventListener('click', () => _toggleHund(h.id));
});
if (_openHundId) _toggleHund(_openHundId, true);
}
// ----------------------------------------------------------
// Hund-Karte
// ----------------------------------------------------------
function _hundCardHTML(h) {
const alter = h.geburtsdatum
? Math.floor((Date.now() - new Date(h.geburtsdatum)) / 31557600000) + ' J'
: '';
return `
<div style="background:var(--c-bg-secondary);border:1px solid var(--c-border);
border-radius:var(--radius-lg);margin-bottom:var(--space-3);overflow:hidden"
id="laeufi-card-${h.id}">
<div id="laeufi-toggle-${h.id}"
style="padding:var(--space-4);display:flex;align-items:center;gap:var(--space-3);
cursor:pointer;user-select:none">
<div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<span style="font-size:var(--text-base);font-weight:700">${UI.escape(h.name)}</span>
${h.rufname ? `<span class="text-sm-muted">"${UI.escape(h.rufname)}"</span>` : ''}
${alter ? `<span class="text-xs-muted">${alter}</span>` : ''}
</div>
${h.rasse_text || h.farbe ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
${[h.rasse_text, h.farbe].filter(Boolean).map(s => UI.escape(s)).join(' · ')}
</div>` : ''}
</div>
<span class="text-muted">${UI.icon('caret-down')}</span>
</div>
<div id="laeufi-detail-${h.id}" style="display:none;border-top:1px solid var(--c-border)">
<div id="laeufi-content-${h.id}"
class="p-4">
<p class="text-sm-muted">Lädt…</p>
</div>
</div>
</div>`;
}
async function _toggleHund(hundId, forceOpen = false) {
const detail = document.getElementById(`laeufi-detail-${hundId}`);
if (!detail) return;
const isOpen = detail.style.display !== 'none';
if (isOpen && !forceOpen) {
detail.style.display = 'none';
_openHundId = null;
return;
}
detail.style.display = '';
_openHundId = hundId;
await _loadHundContent(hundId);
}
// ----------------------------------------------------------
// Inhalt pro Hündin laden
// ----------------------------------------------------------
async function _loadHundContent(hundId) {
const el = document.getElementById(`laeufi-content-${hundId}`);
if (!el) return;
try {
const [laeufiList, deckList] = await Promise.all([
API.laeufi.list(hundId),
API.laeufi.listDeck(hundId),
]);
_renderHundContent(el, hundId, laeufiList, deckList);
} catch (err) {
el.innerHTML = `<p class="text-danger">${UI.escape(err.message || 'Fehler')}</p>`;
}
}
function _renderHundContent(container, hundId, laeufiList, deckList) {
container.innerHTML = `
<div style="display:flex;flex-direction:column;gap:var(--space-5)">
<!-- LÄUFIGKEITEN -->
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-3)">
<h3 style="margin:0;font-size:var(--text-sm);font-weight:700;color:var(--c-text-secondary);
text-transform:uppercase;letter-spacing:.06em">
${UI.icon('drop')} Läufigkeiten
</h3>
<button class="btn btn-secondary btn-xs" id="laeufi-add-btn-${hundId}">
${UI.icon('plus')} Eintragen
</button>
</div>
<div id="laeufi-entries-${hundId}">
${_renderLaeufiEntries(hundId, laeufiList)}
</div>
</div>
<!-- DECKDATEN & TRÄCHTIGKEIT -->
<div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-3)">
<h3 style="margin:0;font-size:var(--text-sm);font-weight:700;color:var(--c-text-secondary);
text-transform:uppercase;letter-spacing:.06em">
${UI.icon('heart')} Deckdaten & Trächtigkeit
</h3>
<button class="btn btn-secondary btn-xs" id="deck-add-btn-${hundId}">
${UI.icon('plus')} Deckung eintragen
</button>
</div>
<div id="deck-entries-${hundId}">
${_renderDeckEntries(hundId, deckList)}
</div>
</div>
</div>`;
document.getElementById(`laeufi-add-btn-${hundId}`)
?.addEventListener('click', () => _showLaeufiForm(hundId, null, laeufiList));
document.getElementById(`deck-add-btn-${hundId}`)
?.addEventListener('click', () => _showDeckForm(hundId, null, laeufiList));
// Edit/Delete Events für Läufigkeiten
container.querySelectorAll('.laeufi-edit-btn').forEach(btn => {
const id = parseInt(btn.dataset.id);
const entry = laeufiList.find(l => l.id === id);
if (entry) btn.addEventListener('click', () => _showLaeufiForm(hundId, entry, laeufiList));
});
container.querySelectorAll('.laeufi-delete-btn').forEach(btn => {
const id = parseInt(btn.dataset.id);
btn.addEventListener('click', async () => {
if (!window.confirm('Läufigkeit und alle Progesterontests löschen?')) return;
try { await API.laeufi.remove(id); await _loadHundContent(hundId); }
catch (err) { UI.toast.error(err.message); }
});
});
container.querySelectorAll('.laeufi-prog-btn').forEach(btn => {
const id = parseInt(btn.dataset.id);
const entry = laeufiList.find(l => l.id === id);
if (entry) btn.addEventListener('click', () => _showProgModal(hundId, entry));
});
// Edit/Delete Events für Deckdaten
container.querySelectorAll('.deck-edit-btn').forEach(btn => {
const id = parseInt(btn.dataset.id);
const entry = deckList.find(d => d.id === id);
if (entry) btn.addEventListener('click', () => _showDeckForm(hundId, entry, laeufiList));
});
container.querySelectorAll('.deck-delete-btn').forEach(btn => {
const id = parseInt(btn.dataset.id);
btn.addEventListener('click', async () => {
if (!window.confirm('Deckdaten löschen?')) return;
try { await API.laeufi.removeDeck(id); await _loadHundContent(hundId); }
catch (err) { UI.toast.error(err.message); }
});
});
}
// ----------------------------------------------------------
// Läufigkeits-Einträge rendern
// ----------------------------------------------------------
function _renderLaeufiEntries(hundId, list) {
if (!list.length) return `
<div style="padding:var(--space-4);text-align:center;border:1px dashed var(--c-border);
border-radius:var(--radius-md);color:var(--c-text-muted);font-size:var(--text-sm)">
Noch keine Läufigkeit eingetragen.
</div>`;
return list.map(l => `
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-md);
padding:var(--space-3);margin-bottom:var(--space-2);display:flex;gap:var(--space-3);align-items:flex-start">
<div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<span style="font-weight:600;font-size:var(--text-sm)">${_fmtDate(l.beginn)}</span>
${l.ende ? `<span class="text-xs-muted">→ ${_fmtDate(l.ende)}</span>
<span class="text-xs-muted">${_daysDiff(l.beginn, l.ende)} Tage</span>` : ''}
</div>
${l.notiz ? `<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:var(--space-1) 0 0;font-style:italic">${UI.escape(l.notiz)}</p>` : ''}
</div>
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
<button class="btn btn-ghost btn-xs laeufi-prog-btn" data-id="${l.id}" title="Progesterontests">
${UI.icon('test-tube')}
</button>
<button class="btn btn-ghost btn-xs laeufi-edit-btn" data-id="${l.id}" title="Bearbeiten">
${UI.icon('pencil-simple')}
</button>
<button class="btn btn-ghost btn-xs laeufi-delete-btn" data-id="${l.id}"
title="Löschen" class="text-danger">
${UI.icon('trash')}
</button>
</div>
</div>`).join('');
}
// ----------------------------------------------------------
// Deckdaten + Meilensteine rendern
// ----------------------------------------------------------
const _TRAECHTIG = { 0: { label: 'Unbekannt', color: '#6b7280' }, 1: { label: 'Trächtig ✓', color: '#16a34a' }, [-1]: { label: 'Nicht trächtig', color: '#dc2626' } };
const _DECKART = { natuerlich: 'Natürlich', ki_frisch: 'KI frisch', ki_tiefgekuehlt: 'KI tiefgekühlt', ki_gefroren: 'KI gefroren' };
function _renderDeckEntries(hundId, list) {
if (!list.length) return `
<div style="padding:var(--space-4);text-align:center;border:1px dashed var(--c-border);
border-radius:var(--radius-md);color:var(--c-text-muted);font-size:var(--text-sm)">
Noch keine Deckung eingetragen.
</div>`;
return list.map(d => {
const tc = _TRAECHTIG[d.traechtig] || _TRAECHTIG[0];
const heute = d.meilensteine?.find(m => !m.vorbei);
const naechster = heute || d.meilensteine?.[d.meilensteine.length - 1];
return `
<div style="background:var(--c-surface);border:1px solid var(--c-border);border-radius:var(--radius-md);
margin-bottom:var(--space-3);overflow:hidden">
<!-- Deck-Header -->
<div style="padding:var(--space-3);display:flex;gap:var(--space-3);align-items:flex-start">
<div class="flex-1-min">
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
<span style="font-weight:700;font-size:var(--text-sm)">${UI.icon('heart')} Deckung ${_fmtDate(d.deckdatum)}</span>
<span style="background:${tc.color}1a;color:${tc.color};border:1px solid ${tc.color}30;
border-radius:999px;padding:1px 8px;font-size:var(--text-xs);font-weight:600">${tc.label}</span>
</div>
<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)">
${d.ruede_name ? `<span>${UI.icon('dog')} Rüde: ${UI.escape(d.ruede_name)}</span>` : ''}
<span>${UI.icon('arrows-clockwise')} ${_DECKART[d.deckart] || d.deckart}</span>
${d.ultraschall_datum ? `<span>${UI.icon('heartbeat')} Ultraschall: ${_fmtDate(d.ultraschall_datum)}</span>` : ''}
</div>
${naechster && d.traechtig === 1 ? `
<div style="margin-top:var(--space-2);font-size:var(--text-xs);
background:var(--c-primary-light,#f5e6d3);color:var(--c-primary-dark,#a86e2e);
border-radius:var(--radius-sm);padding:var(--space-1) var(--space-2);display:inline-flex;gap:4px;align-items:center">
${UI.icon('calendar-dots')} ${naechster.vorbei ? 'Letzter' : 'Nächster'} Meilenstein:
<strong>${naechster.label}</strong> · Tag ${naechster.tag} · ${_fmtDate(naechster.datum)}
</div>` : ''}
</div>
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
<button class="btn btn-ghost btn-xs deck-edit-btn" data-id="${d.id}" title="Bearbeiten">${UI.icon('pencil-simple')}</button>
<button class="btn btn-ghost btn-xs deck-delete-btn" data-id="${d.id}" title="Löschen" class="text-danger">${UI.icon('trash')}</button>
</div>
</div>
<!-- Meilensteine -->
${d.traechtig === 1 && d.meilensteine?.length ? _renderMeilensteine(d.meilensteine) : ''}
</div>`;
}).join('');
}
function _renderMeilensteine(meilensteine) {
return `
<div style="border-top:1px solid var(--c-border);padding:var(--space-3)">
<p style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-muted);text-transform:uppercase;
letter-spacing:.06em;margin-bottom:var(--space-2)">${UI.icon('calendar-check')} Trächtigkeits-Meilensteine</p>
<div style="display:flex;flex-direction:column;gap:var(--space-1)">
${meilensteine.map(m => `
<div style="display:flex;align-items:center;gap:var(--space-2);font-size:var(--text-xs);
opacity:${m.vorbei ? '.45' : '1'}">
<span style="width:18px;height:18px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;
background:${m.vorbei ? 'var(--c-success)' : 'var(--c-border)'};
color:${m.vorbei ? 'white' : 'var(--c-text-muted)'};font-size:9px">
${m.vorbei ? '✓' : m.tag}
</span>
<span class="text-secondary">${_fmtDate(m.datum)}</span>
<span style="color:${m.vorbei ? 'var(--c-text-muted)' : 'var(--c-text)'};font-weight:${m.vorbei ? '400' : '600'}">
${UI.escape(m.label)}
</span>
</div>`).join('')}
</div>
</div>`;
}
// ----------------------------------------------------------
// Formulare
// ----------------------------------------------------------
function _showLaeufiForm(hundId, entry, _allLaeufi) {
const isEdit = !!entry;
const v = entry || {};
const today = new Date().toISOString().slice(0, 10);
UI.modal.open({
title: isEdit ? 'Läufigkeit bearbeiten' : 'Läufigkeit eintragen',
body: `
<form id="laeufi-form" class="flex-col-gap-3">
<div class="grid-2">
<div class="form-group">
<label class="form-label">Beginn *</label>
<input class="form-control" type="date" name="beginn" required value="${v.beginn || today}">
</div>
<div class="form-group">
<label class="form-label">Ende</label>
<input class="form-control" type="date" name="ende" value="${v.ende || ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Notiz</label>
<textarea class="form-control" name="notiz" rows="2">${UI.escape(v.notiz || '')}</textarea>
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" form="laeufi-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
});
document.getElementById('laeufi-form').addEventListener('submit', async e => {
e.preventDefault();
const fd = new FormData(e.target);
const data = { beginn: fd.get('beginn'), ende: fd.get('ende') || null, notiz: fd.get('notiz') || null };
try {
if (isEdit) await API.laeufi.update(entry.id, data);
else await API.laeufi.add(hundId, data);
UI.modal.close();
await _loadHundContent(hundId);
UI.toast.success(isEdit ? 'Gespeichert.' : 'Läufigkeit eingetragen.');
} catch (err) { UI.toast.error(err.message); }
});
}
function _showDeckForm(hundId, entry, laeufiList) {
const isEdit = !!entry;
const v = entry || {};
const today = new Date().toISOString().slice(0, 10);
const laeufiOpts = laeufiList.map(l =>
`<option value="${l.id}" ${v.laeufi_id === l.id ? 'selected' : ''}>${_fmtDate(l.beginn)}</option>`
).join('');
UI.modal.open({
title: isEdit ? 'Deckung bearbeiten' : 'Deckung eintragen',
body: `
<form id="deck-form" class="flex-col-gap-3">
<div class="grid-2">
<div class="form-group">
<label class="form-label">Deckdatum *</label>
<input class="form-control" type="date" name="deckdatum" required value="${v.deckdatum || today}">
</div>
<div class="form-group">
<label class="form-label">Zugehörige Läufigkeit</label>
<select class="form-control" name="laeufi_id">
<option value="">— keine —</option>
${laeufiOpts}
</select>
</div>
</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">Rüde</label>
<input class="form-control" name="ruede_name" placeholder="Name des Deckrüden"
value="${UI.escape(v.ruede_name || '')}">
</div>
<div class="form-group">
<label class="form-label">Deckart</label>
<select class="form-control" name="deckart">
<option value="natuerlich" ${(v.deckart||'natuerlich') === 'natuerlich' ? 'selected':''}>Natürlich</option>
<option value="ki_frisch" ${v.deckart === 'ki_frisch' ? 'selected':''}>KI frisch</option>
<option value="ki_tiefgekuehlt"${v.deckart === 'ki_tiefgekuehlt'? 'selected':''}>KI tiefgekühlt</option>
<option value="ki_gefroren" ${v.deckart === 'ki_gefroren' ? 'selected':''}>KI gefroren</option>
</select>
</div>
</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">Trächtigkeitsstatus</label>
<select class="form-control" name="traechtig">
<option value="0" ${(v.traechtig === 0 || v.traechtig == null) ? 'selected':''}>Unbekannt</option>
<option value="1" ${v.traechtig === 1 ? 'selected':''}>Trächtig ✓</option>
<option value="-1" ${v.traechtig === -1 ? 'selected':''}>Nicht trächtig</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Ultraschall-Datum</label>
<input class="form-control" type="date" name="ultraschall_datum"
value="${v.ultraschall_datum || ''}">
</div>
</div>
<div class="form-group">
<label class="form-label">Notiz</label>
<textarea class="form-control" name="notiz" rows="2">${UI.escape(v.notiz || '')}</textarea>
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" form="deck-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
});
document.getElementById('deck-form').addEventListener('submit', async e => {
e.preventDefault();
const fd = new FormData(e.target);
const data = {
deckdatum: fd.get('deckdatum'),
laeufi_id: fd.get('laeufi_id') ? parseInt(fd.get('laeufi_id')) : null,
ruede_name: fd.get('ruede_name') || null,
deckart: fd.get('deckart'),
traechtig: parseInt(fd.get('traechtig')),
ultraschall_datum: fd.get('ultraschall_datum') || null,
notiz: fd.get('notiz') || null,
};
try {
if (isEdit) await API.laeufi.updateDeck(entry.id, data);
else await API.laeufi.addDeck(hundId, data);
UI.modal.close();
await _loadHundContent(hundId);
UI.toast.success(isEdit ? 'Gespeichert.' : 'Deckung eingetragen.');
} catch (err) { UI.toast.error(err.message); }
});
}
// ----------------------------------------------------------
// Progesterontests-Modal
// ----------------------------------------------------------
async function _showProgModal(hundId, laeufi) {
UI.modal.open({
title: `Progesterontests — ${_fmtDate(laeufi.beginn)}`,
body: `<div id="prog-modal-content"><p class="text-muted">Lädt…</p></div>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>
<button class="btn btn-primary" id="prog-add-btn">${UI.icon('plus')} Test eintragen</button>`,
});
await _loadProgContent(laeufi.id);
document.getElementById('prog-add-btn')
?.addEventListener('click', () => _showProgForm(hundId, laeufi.id, null));
}
async function _loadProgContent(laeufiId) {
const el = document.getElementById('prog-modal-content');
if (!el) return;
const tests = await API.laeufi.listProg(laeufiId);
if (!tests.length) {
el.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm);text-align:center;padding:var(--space-4)">
Noch keine Tests eingetragen.</p>`;
return;
}
el.innerHTML = `
<table style="width:100%;border-collapse:collapse;font-size:var(--text-sm)">
<thead>
<tr style="font-size:var(--text-xs);color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.05em">
<th style="text-align:left;padding:var(--space-1) var(--space-2)">Datum</th>
<th style="text-align:right;padding:var(--space-1) var(--space-2)">Wert</th>
<th style="text-align:left;padding:var(--space-1) var(--space-2)">Labor</th>
<th style="padding:var(--space-1) var(--space-2)"></th>
</tr>
</thead>
<tbody>
${tests.map(t => `
<tr style="border-top:1px solid var(--c-border)">
<td class="p-2">${_fmtDate(t.datum)}</td>
<td style="text-align:right;padding:var(--space-2);font-weight:600">
${t.wert != null ? `${t.wert} ${UI.escape(t.einheit)}` : '—'}
${t.wert != null ? `<span style="font-size:10px;margin-left:4px;color:var(--c-text-muted)">${_progEinschaetzung(t.wert, t.einheit)}</span>` : ''}
</td>
<td style="padding:var(--space-2);color:var(--c-text-secondary)">${t.labor ? UI.escape(t.labor) : '—'}</td>
<td style="padding:var(--space-2);text-align:right">
<button class="btn btn-ghost btn-xs prog-delete-btn" data-id="${t.id}"
class="text-danger">${UI.icon('trash')}</button>
</td>
</tr>`).join('')}
</tbody>
</table>`;
el.querySelectorAll('.prog-delete-btn').forEach(btn => {
btn.addEventListener('click', async () => {
try {
await API.laeufi.removeProg(parseInt(btn.dataset.id));
await _loadProgContent(laeufiId);
} catch (err) { UI.toast.error(err.message); }
});
});
}
function _progEinschaetzung(wert, einheit) {
if (einheit !== 'ng/ml') return '';
if (wert < 2) return '(Basiswert)';
if (wert < 5) return '(Anstieg)';
if (wert < 10) return '(LH-Peak Nähe)';
if (wert < 15) return '(Ovulation)';
return '(Post-Ovulation)';
}
function _showProgForm(hundId, laeufiId, _entry) {
const today = new Date().toISOString().slice(0, 10);
UI.modal.open({
title: 'Progesterontest eintragen',
body: `
<form id="prog-form" class="flex-col-gap-3">
<div class="grid-2">
<div class="form-group">
<label class="form-label">Datum *</label>
<input class="form-control" type="date" name="datum" required value="${today}">
</div>
<div class="form-group">
<label class="form-label">Einheit</label>
<select class="form-control" name="einheit">
<option value="ng/ml">ng/ml</option>
<option value="nmol/l">nmol/l</option>
</select>
</div>
</div>
<div class="grid-2">
<div class="form-group">
<label class="form-label">Wert</label>
<input class="form-control" type="number" step="0.01" name="wert" placeholder="z.B. 8.5">
</div>
<div class="form-group">
<label class="form-label">Labor / Tierarzt</label>
<input class="form-control" name="labor" placeholder="z.B. Tierarzt Müller">
</div>
</div>
<div class="form-group">
<label class="form-label">Notiz</label>
<input class="form-control" name="notiz">
</div>
</form>`,
footer: `
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
<button class="btn btn-primary" form="prog-form" type="submit">Eintragen</button>`,
});
document.getElementById('prog-form').addEventListener('submit', async e => {
e.preventDefault();
const fd = new FormData(e.target);
const wertRaw = fd.get('wert');
const data = {
datum: fd.get('datum'),
wert: wertRaw ? parseFloat(wertRaw) : null,
einheit: fd.get('einheit'),
labor: fd.get('labor') || null,
notiz: fd.get('notiz') || null,
};
try {
await API.laeufi.addProg(laeufiId, data);
UI.modal.close();
// Prog-Modal neu öffnen
const laeufi = { id: laeufiId, beginn: '' };
await _showProgModal(hundId, laeufi);
await _loadHundContent(hundId);
UI.toast.success('Test eingetragen.');
} catch (err) { UI.toast.error(err.message); }
});
}
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _fmtDate(iso) {
if (!iso) return '—';
const [y, m, d] = iso.slice(0, 10).split('-');
return `${d}.${m}.${y}`;
}
function _daysDiff(a, b) {
if (!a || !b) return '';
return Math.round((new Date(b) - new Date(a)) / 86400000);
}
return { init, refresh, onDogChange };
})();