/* ============================================================ BAN YARO — Wurfverwaltung Züchter verwalten ihre Würfe und Welpen ============================================================ */ window.Page_litters = (() => { let _container = null; let _appState = null; let _litters = []; // geladene Würfe let _openId = null; // aufgeklappter Wurf let _filterStatus = null; // aktiver Status-Filter // ---------------------------------------------------------- // Hilfsfunktionen // ---------------------------------------------------------- function _emptyState(icon, title, text) { return `
${UI.icon(icon)}

${_esc(title)}

${_esc(text)}

`; } function _esc(s) { return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } function _statusBadge(status) { const map = { geplant: { label: 'Geplant', color: '#6B7280' }, geboren: { label: 'Geboren', color: '#3B82F6' }, verfuegbar: { label: 'Verfügbar', color: '#22C55E' }, abgeschlossen: { label: 'Abgeschlossen', color: '#374151' }, }; const s = map[status] || { label: status, color: '#6B7280' }; return `${_esc(s.label)}`; } 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 ''; } function _puppyStatusBadge(status) { const map = { verfuegbar: { label: 'Verfügbar', color: '#22C55E' }, reserviert: { label: 'Reserviert', color: '#F59E0B' }, abgegeben: { label: 'Abgegeben', color: '#6B7280' }, }; const s = map[status] || { label: status, color: '#9CA3AF' }; return `${_esc(s.label)}`; } // ---------------------------------------------------------- // INIT // ---------------------------------------------------------- async function init(container, appState) { _container = container; _appState = appState; // Auth-Guard const u = _appState.user; if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) { _container.innerHTML = _emptyState('lock', 'Kein Zugriff', 'Diese Seite ist nur für verifizierte Züchter.'); return; } _render(); await _loadLitters(); } function refresh() { const u = _appState?.user; if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) return; _loadLitters(); } function onDogChange() {} // ---------------------------------------------------------- // Grundstruktur rendern // ---------------------------------------------------------- function _render() { _container.innerHTML = `

${UI.icon('dog')} Meine Würfe

Lädt…

`; document.getElementById('litters-new-btn')?.addEventListener('click', () => { _showLitterForm(null); }); } // ---------------------------------------------------------- // Würfe laden // ---------------------------------------------------------- async function _loadLitters() { try { _litters = await API.litters.myList(); _renderList(); } catch (err) { if (err.status === 404) { const el = document.getElementById('litters-list'); if (el) el.innerHTML = _emptyState('certificate', 'Kein Züchter-Profil', 'Stelle zuerst einen Züchter-Antrag in den Einstellungen, um Würfe verwalten zu können.'); } else { UI.toast.error(err.message || 'Fehler beim Laden der Würfe.'); } } } // ---------------------------------------------------------- // Würfe-Liste rendern // ---------------------------------------------------------- function _renderStats() { const bar = document.getElementById('litters-stats'); if (!bar || !_litters.length) return; const total = _litters.length; const aktiv = _litters.filter(l => l.status === 'verfuegbar' || l.status === 'geboren').length; const geplant = _litters.filter(l => l.status === 'geplant').length; const welpen = _litters.reduce((s, l) => s + (l.welpen_gesamt || 0), 0); const verfuegb = _litters.reduce((s, l) => s + (l.welpen_verfuegbar || 0), 0); const statItems = [ { icon: 'list-bullets', label: 'Alle Würfe', val: total, filter: null }, { icon: 'baby', label: 'Aktiv', val: aktiv, filter: ['verfuegbar','geboren'], color: 'var(--c-success)' }, { icon: 'calendar-dots',label: 'Geplant', val: geplant, filter: ['geplant'] }, { icon: 'dog', label: 'Welpen ges.', val: welpen, filter: null }, { icon: 'tag', label: 'Verfügbar', val: verfuegb,filter: ['verfuegbar'], color: verfuegb > 0 ? 'var(--c-primary)' : undefined }, ]; bar.style.display = 'flex'; bar.innerHTML = statItems.map((s, i) => { const isActive = JSON.stringify(_filterStatus) === JSON.stringify(s.filter); const clickable = s.filter !== undefined && !(s.label === 'Welpen ges.'); return `
${UI.icon(s.icon)}
${s.val}
${s.label}
`; }).join(''); bar.querySelectorAll('[data-stat-idx]').forEach(chip => { const s = statItems[parseInt(chip.dataset.statIdx)]; if (!s.filter && s.label !== 'Alle Würfe') return; chip.addEventListener('click', () => { _filterStatus = JSON.stringify(_filterStatus) === JSON.stringify(s.filter) ? null : s.filter; _renderStats(); _renderFilteredList(); }); }); } function _renderFilteredList() { const el = document.getElementById('litters-list'); if (!el) return; const visible = _filterStatus ? _litters.filter(l => _filterStatus.includes(l.status)) : _litters; if (!visible.length) { el.innerHTML = `

Keine Würfe für diesen Filter.

`; return; } el.innerHTML = visible.map(l => _litterCardHTML(l)).join(''); _bindCardEvents(el, visible); } function _renderList() { const el = document.getElementById('litters-list'); if (!el) return; if (!_litters.length) { el.innerHTML = `
${UI.icon('dog')}

Noch keine Würfe angelegt.

`; document.getElementById('litters-first-btn')?.addEventListener('click', () => _showLitterForm(null)); return; } _renderStats(); _filterStatus = null; el.innerHTML = _litters.map(l => _litterCardHTML(l)).join(''); _bindCardEvents(el, _litters); if (_openId) _togglePuppies(_openId, true); } function _bindCardEvents(el, litters) { el.querySelectorAll('.litters-card-toggle').forEach(btn => { btn.addEventListener('click', () => _togglePuppies(parseInt(btn.dataset.id))); }); el.querySelectorAll('.litters-photos-btn').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.id); const l = litters.find(x => x.id === id); if (l) _showPhotosModal('litter', l.id, l.zwingername || `Wurf #${l.id}`); }); }); el.querySelectorAll('.litters-parent-photos-btn').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.id); const l = litters.find(x => x.id === id); if (!l) return; _showPhotosModal('parent', l.id, [l.vater_name, l.mutter_name].filter(Boolean).join(' × ') || `Eltern #${id}`); }); }); el.querySelectorAll('.litters-edit-btn').forEach(btn => { btn.addEventListener('click', () => { const l = litters.find(x => x.id === parseInt(btn.dataset.id)); if (l) _showLitterForm(l); }); }); el.querySelectorAll('.litters-delete-btn').forEach(btn => { btn.addEventListener('click', () => _deleteLitter(parseInt(btn.dataset.id))); }); el.querySelectorAll('.litters-ki-announce-btn').forEach(btn => { btn.addEventListener('click', () => _showKiAnnouncement(parseInt(btn.dataset.id))); }); el.querySelectorAll('.litters-add-puppy-btn').forEach(btn => { btn.addEventListener('click', () => _showPuppyForm(parseInt(btn.dataset.id), null)); }); el.querySelectorAll('.litters-waitlist-btn').forEach(btn => { btn.addEventListener('click', () => _toggleWaitlist(parseInt(btn.dataset.id))); }); el.querySelectorAll('.litters-add-waitlist-btn').forEach(btn => { btn.addEventListener('click', () => _showWaitlistForm(parseInt(btn.dataset.id), null)); }); } function _daysUntil(dateStr) { if (!dateStr) return null; const diff = Math.ceil((new Date(dateStr) - new Date()) / 86400000); return diff; } function _litterCardHTML(l) { const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?'; const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?'; const elternLabel = [l.vater_name, l.mutter_name].filter(Boolean).map(n => _esc(n)).join(' × ') || '—'; // Datum + Countdown let datumChip = ''; const refDate = l.geburt_datum || l.erwartetes_datum; if (refDate) { const days = _daysUntil(refDate); const label = l.geburt_datum ? `Geburt ${_fmtDate(l.geburt_datum)}` : `Erwartet ${_fmtDate(l.erwartetes_datum)}`; let countdownHtml = ''; if (days !== null && !l.geburt_datum) { const c = days < 0 ? `überfällig` : days === 0 ? `heute!` : days <= 7 ? `${days}d` : `${days}d`; countdownHtml = ` · ${c}`; } datumChip = `${UI.icon('calendar-dots')} ${label}${countdownHtml}`; } const sichtbarChip = l.sichtbar ? `${UI.icon('eye')} Öffentlich` : `${UI.icon('eye-slash')} Nicht öffentlich`; const welpenChip = `${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar`; const preisChip = l.preis_spanne ? `${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}` : ''; return `
${elternLabel} ${_statusBadge(l.status)} ${sichtbarChip}
${datumChip} ${welpenChip} ${preisChip}
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? ` ` : ''}
${l.beschreibung ? `

${_esc(l.beschreibung)}

` : ''}
`; } // ---------------------------------------------------------- // Welpen aufklappen / zuklappen // ---------------------------------------------------------- async function _togglePuppies(litterId, forceOpen = false) { const wrap = document.getElementById(`puppies-wrap-${litterId}`); if (!wrap) return; const isOpen = wrap.style.display !== 'none'; if (isOpen && !forceOpen) { wrap.style.display = 'none'; _openId = null; // Caret zurücksetzen const btn = document.querySelector(`.litters-card-toggle[data-id="${litterId}"]`); if (btn) btn.innerHTML = `${UI.icon('caret-down')} Welpen`; return; } wrap.style.display = ''; _openId = litterId; const btn = document.querySelector(`.litters-card-toggle[data-id="${litterId}"]`); if (btn) btn.innerHTML = `${UI.icon('caret-up')} Welpen`; await _loadPuppies(litterId); } async function _loadPuppies(litterId) { const inner = document.getElementById(`puppies-inner-${litterId}`); if (!inner) return; try { const puppies = await API.litters.puppies(litterId); _renderPuppies(inner, litterId, puppies); } catch (err) { inner.innerHTML = `

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

`; } } function _renderPuppies(container, litterId, puppies) { if (!puppies.length) { container.innerHTML = `

Noch keine Welpen eingetragen.

`; return; } container.innerHTML = puppies.map(p => `
${_genderIcon(p.geschlecht)} ${p.name ? _esc(p.name) : 'Unbenannt'} ${p.farbe ? `${_esc(p.farbe)}` : ''} ${_puppyStatusBadge(p.status)}
`).join(''); container.querySelectorAll('.litters-puppy-photo-btn').forEach(btn => { btn.addEventListener('click', () => { const pid = parseInt(btn.dataset.puppyId); const puppy = puppies.find(p => p.id === pid); if (puppy) _showPhotosModal('puppy', puppy.id, puppy.name || 'Welpe'); }); }); container.querySelectorAll('.litters-puppy-edit-btn').forEach(btn => { btn.addEventListener('click', () => { const pid = parseInt(btn.dataset.puppyId); const lid = parseInt(btn.dataset.litterId); const puppy = puppies.find(p => p.id === pid); if (puppy) _showPuppyForm(lid, puppy); }); }); container.querySelectorAll('.litters-puppy-weight-btn').forEach(btn => { btn.addEventListener('click', () => { const pid = parseInt(btn.dataset.puppyId); const puppy = puppies.find(p => p.id === pid); if (puppy) _showWeightModal(puppy); }); }); container.querySelectorAll('.litters-puppy-contract-btn').forEach(btn => { btn.addEventListener('click', () => { const pid = parseInt(btn.dataset.puppyId); const puppy = puppies.find(p => p.id === pid); if (puppy) _showContractModal(puppy); }); }); // Letztes Gewicht für jeden Welpen laden puppies.forEach(p => _loadLastWeight(p.id)); } async function _loadLastWeight(puppyId) { try { const weights = await API.get(`/litters/puppies/${puppyId}/weights`); const el = document.getElementById(`puppy-last-weight-${puppyId}`); if (!el) return; if (weights && weights.length) { const w = weights[0]; el.innerHTML = `${UI.icon('scales')} ${w.gewicht_g} g (${_fmtDate(w.gemessen_am)})`; } } catch (_) { // Gewichte nicht kritisch — still ignorieren } } function _showWeightModal(puppy) { const today = new Date().toISOString().slice(0, 10); const puppyLabel = puppy.name || 'Welpe'; const body = `

Lädt…


`; const footer = ` `; UI.modal.open({ title: `${UI.icon('scales')} Gewichtsverlauf — ${_esc(puppyLabel)}`, body, footer, }); // Gewichte laden und rendern _loadWeightHistory(puppy.id); document.getElementById('weight-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('weight-submit'); const fd = new FormData(e.target); const payload = { gewicht_g: parseFloat(fd.get('gewicht_g')), gemessen_am: fd.get('gemessen_am'), }; await UI.asyncButton(btn, async () => { await API.litters.addWeight(puppy.id, payload); UI.toast.success('Gewicht gespeichert.'); e.target.reset(); document.querySelector('[name="gemessen_am"]').value = today; _loadWeightHistory(puppy.id); // Letztes Gewicht im Welpen-Eintrag aktualisieren _loadLastWeight(puppy.id); }); }); } async function _loadWeightHistory(puppyId) { const el = document.getElementById('weight-history'); if (!el) return; try { const weights = await API.get(`/litters/puppies/${puppyId}/weights`); if (!weights || !weights.length) { el.innerHTML = `

Noch keine Messungen eingetragen.

`; return; } // Chronologisch für Chart (API liefert DESC) const asc = [...weights].reverse(); const first = asc[0].gewicht_g; const last = asc[asc.length - 1].gewicht_g; const gain = last - first; const days = asc.length > 1 ? Math.max(1, (new Date(asc[asc.length-1].gemessen_am) - new Date(asc[0].gemessen_am)) / 86400000) : 1; const dailyGain = asc.length > 1 ? (gain / days).toFixed(1) : '—'; // SVG Sparkline const W = 400, H = 80; const minW = Math.min(...asc.map(w => w.gewicht_g)); const maxW = Math.max(...asc.map(w => w.gewicht_g)); const range = maxW - minW || 1; const pts = asc.map((w, i) => { const x = asc.length === 1 ? W/2 : (i / (asc.length - 1)) * W; const y = H - 8 - ((w.gewicht_g - minW) / range) * (H - 16); return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const firstDate = asc[0].gemessen_am?.slice(5) || ''; const lastDate = asc[asc.length-1].gemessen_am?.slice(5) || ''; el.innerHTML = `
Aktuell
${last} g
Zunahme
${gain >= 0 ? '+' : ''}${gain} g
Ø tägl.
${dailyGain} g
Messungen
${weights.length}
${asc.length > 1 ? `
${asc.map((w, i) => { const x = (i / (asc.length - 1)) * W; const y = H - 8 - ((w.gewicht_g - minW) / range) * (H - 16); return ``; }).join('')}
${firstDate}${lastDate}
` : ''} ${weights.map((w, i) => { const prev = weights[i + 1]; const diff = prev ? w.gewicht_g - prev.gewicht_g : null; const diffStr = diff === null ? '—' : `${diff >= 0 ? '+' : ''}${diff} g`; return ` `; }).join('')}
Datum Gewicht Veränderung
${_fmtDate(w.gemessen_am)} ${w.gewicht_g} g ${diffStr}
`; } catch (err) { el.innerHTML = `

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

`; } } // ---------------------------------------------------------- // Warteliste // ---------------------------------------------------------- const _WL_STATUS = { anfrage: { label: 'Anfrage', color: '#6b7280' }, vorgemerkt: { label: 'Vorgemerkt', color: '#f59e0b' }, bestaetigt: { label: 'Bestätigt', color: '#3b82f6' }, abgegeben: { label: 'Abgegeben', color: '#16a34a' }, abgesagt: { label: 'Abgesagt', color: '#dc2626' }, }; function _wlStatusBadge(status) { const s = _WL_STATUS[status] || _WL_STATUS.anfrage; return `${s.label}`; } async function _toggleWaitlist(litterId) { const wrap = document.getElementById(`waitlist-wrap-${litterId}`); if (!wrap) return; const isOpen = wrap.style.display !== 'none'; if (isOpen) { wrap.style.display = 'none'; return; } wrap.style.display = ''; await _loadWaitlist(litterId); } async function _loadWaitlist(litterId) { const inner = document.getElementById(`waitlist-inner-${litterId}`); if (!inner) return; try { const entries = await API.litters.waitlist(litterId); _renderWaitlist(inner, litterId, entries); // Badge am Button aktualisieren const btn = document.querySelector(`.litters-waitlist-btn[data-id="${litterId}"]`); if (btn) { const active = entries.filter(e => e.status !== 'abgesagt').length; btn.innerHTML = `${UI.icon('list-bullets')} Warteliste${active ? ` ${active}` : ''}`; } } catch (err) { inner.innerHTML = `

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

`; } } function _renderWaitlist(container, litterId, entries) { const active = entries.filter(e => e.status !== 'abgesagt'); const statusCounts = {}; entries.forEach(e => { statusCounts[e.status] = (statusCounts[e.status] || 0) + 1; }); const summaryPills = Object.entries(statusCounts).map(([s, n]) => { const cfg = _WL_STATUS[s] || _WL_STATUS.anfrage; return `${cfg.label}: ${n}`; }).join(''); const header = `
${entries.length} Interessent${entries.length !== 1 ? 'en' : ''} ${summaryPills}
`; if (!entries.length) { container.innerHTML = `
${UI.icon('users')}

Noch keine Interessenten

Trage Anfragen ein — mit Wunsch-Geschlecht, Kontaktdaten und Status.

`; return; } container.innerHTML = header + `
${entries.map((e, i) => `
${i + 1}
${_esc(e.name)} ${_wlStatusBadge(e.status)} ${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}` : ''} ${e.wunsch_farbe ? `${_esc(e.wunsch_farbe)}` : ''}
${e.email ? `${UI.icon('envelope')} ${_esc(e.email)}` : ''} ${e.telefon ? `${UI.icon('phone')} ${_esc(e.telefon)}` : ''} ${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'}
${e.nachricht ? `
"${_esc(e.nachricht)}"
` : ''} ${e.notiz ? `
${UI.icon('note-pencil')} ${_esc(e.notiz)}
` : ''}
`).join('')}
`; container.querySelectorAll('.wl-edit-btn').forEach(btn => { btn.addEventListener('click', () => { const entry = entries.find(e => e.id === parseInt(btn.dataset.entryId)); if (entry) _showWaitlistForm(litterId, entry); }); }); container.querySelectorAll('.wl-delete-btn').forEach(btn => { btn.addEventListener('click', async () => { if (!window.confirm('Interessenten aus der Warteliste entfernen?')) return; try { await API.litters.removeWaitlist(parseInt(btn.dataset.entryId)); await _loadWaitlist(litterId); } catch (err) { UI.toast.error(err.message || 'Fehler.'); } }); }); } function _showWaitlistForm(litterId, entry) { const isEdit = !!entry; const v = entry || {}; UI.modal.open({ title: isEdit ? 'Interessent bearbeiten' : 'Interessent eintragen', body: `
`, footer: ` `, }); document.getElementById('wl-form').addEventListener('submit', async e => { e.preventDefault(); const fd = new FormData(e.target); const data = { name: fd.get('name')?.trim(), email: fd.get('email')?.trim() || null, telefon: fd.get('telefon')?.trim() || null, nachricht: fd.get('nachricht')?.trim() || null, wunsch_geschlecht: fd.get('wunsch_geschlecht'), wunsch_farbe: fd.get('wunsch_farbe')?.trim() || null, prioritaet: parseInt(fd.get('prioritaet')) || 0, status: fd.get('status'), notiz: fd.get('notiz')?.trim() || null, }; try { if (isEdit) { await API.litters.updateWaitlist(entry.id, data); } else { await API.litters.addWaitlist(litterId, data); } UI.modal.close(); await _loadWaitlist(litterId); UI.toast.success(isEdit ? 'Gespeichert.' : 'Interessent eingetragen.'); } catch (err) { UI.toast.error(err.message || 'Fehler.'); } }); } // ---------------------------------------------------------- // Wurf-Formular (neu / bearbeiten) // ---------------------------------------------------------- async function _showLitterForm(litter) { const isEdit = !!litter; const v = litter || {}; const today = new Date().toISOString().slice(0, 10); // Zuchtkartei laden für Elterntier-Auswahl let zuchthunde = []; try { zuchthunde = await API.zuchthunde.list(); } catch {} const maennlich = zuchthunde.filter(h => h.geschlecht !== 'weiblich'); const weiblich = zuchthunde.filter(h => h.geschlecht !== 'maennlich'); const buildSelect = (name, idName, list, currentId, currentName, placeholder) => { const opts = list.map(h => { const label = h.name + (h.rufname ? ` (${h.rufname})` : '') + (h.zuchtbuchnummer ? ` · ${h.zuchtbuchnummer}` : ''); return ``; }).join(''); return ` `; }; const body = `
${buildSelect('vater_name', 'vater_id', maennlich, v.vater_id, v.vater_name, 'Aus Zuchtkartei')}
${buildSelect('mutter_name', 'mutter_id', weiblich, v.mutter_id, v.mutter_name, 'Aus Zuchtkartei')}

Für geplante Würfe / laufende Trächtigkeit

Wenn die Welpen bereits geboren sind

`; const footer = ` `; UI.modal.open({ title: isEdit ? `${UI.icon('pencil-simple')} Wurf bearbeiten` : `${UI.icon('dog')} Neuer Wurf`, body, footer, }); document.getElementById('lf-cancel')?.addEventListener('click', UI.modal.close); // Auto-Fill: Dropdown → Namenfeld befüllen ['vater', 'mutter'].forEach(role => { document.getElementById(`${role}_id-sel`)?.addEventListener('change', e => { const sel = e.target; const opt = sel.options[sel.selectedIndex]; const txt = document.getElementById(`${role}_name-txt`); if (txt) txt.value = opt.value ? (opt.dataset.name || '') : ''; }); }); document.getElementById('litter-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('lf-submit'); const fd = new FormData(e.target); const payload = { vater_name: fd.get('vater_name')?.trim() || null, mutter_name: fd.get('mutter_name')?.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, geburt_datum: fd.get('geburt_datum') || null, erwartetes_datum: fd.get('erwartetes_datum') || null, welpen_gesamt: fd.get('welpen_gesamt') ? parseInt(fd.get('welpen_gesamt')) : null, welpen_verfuegbar: fd.get('welpen_verfuegbar') ? parseInt(fd.get('welpen_verfuegbar')) : null, beschreibung: fd.get('beschreibung')?.trim() || null, gesundheitstests: fd.get('gesundheitstests')?.trim() || null, preis_spanne: fd.get('preis_spanne')?.trim() || null, status: fd.get('status') || 'geplant', sichtbar: fd.get('sichtbar') === '1' ? 1 : 0, sichtbar_bis: fd.get('sichtbar_bis') || null, }; await UI.asyncButton(btn, async () => { if (isEdit) { const updated = await API.litters.update(litter.id, payload); const idx = _litters.findIndex(l => l.id === litter.id); if (idx !== -1) _litters[idx] = updated; UI.modal.close(); UI.toast.success('Wurf aktualisiert.'); _renderList(); } else { const created = await API.litters.create(payload); _litters.unshift(created); UI.modal.close(); _renderList(); _showWelfareModal(created.welfare, created.id); } }); }); } // ---------------------------------------------------------- // Wurf löschen // ---------------------------------------------------------- async function _deleteLitter(litterId) { const litter = _litters.find(l => l.id === litterId); const label = [litter?.vater_name, litter?.mutter_name].filter(Boolean).join(' × ') || `Wurf #${litterId}`; if (!window.confirm(`Wurf "${label}" wirklich löschen? Alle Welpen werden ebenfalls gelöscht.`)) return; try { await API.litters.remove(litterId); _litters = _litters.filter(l => l.id !== litterId); if (_openId === litterId) _openId = null; _renderList(); UI.toast.success('Wurf gelöscht.'); } catch (err) { UI.toast.error(err.message || 'Fehler beim Löschen.'); } } // ---------------------------------------------------------- // Welpen-Formular (neu / bearbeiten) // ---------------------------------------------------------- function _showPuppyForm(litterId, puppy) { const isEdit = !!puppy; const v = puppy || {}; const body = `
`; const footer = ` `; UI.modal.open({ title: isEdit ? `${UI.icon('dog')} Welpe bearbeiten` : `${UI.icon('dog')} Welpe hinzufügen`, body, footer, }); document.getElementById('pf-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('puppy-form')?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('pf-submit'); const fd = new FormData(e.target); const payload = { name: fd.get('name')?.trim() || null, geschlecht: fd.get('geschlecht') || null, farbe: fd.get('farbe')?.trim() || null, chip_nr: fd.get('chip_nr')?.trim() || null, geburtsgewicht: fd.get('geburtsgewicht') ? parseFloat(fd.get('geburtsgewicht')) : null, status: fd.get('status') || 'verfuegbar', status_sichtbar: fd.get('status_sichtbar') === '1' ? 1 : 0, notiz: fd.get('notiz')?.trim() || null, }; await UI.asyncButton(btn, async () => { if (isEdit) { await API.litters.updatePuppy(puppy.id, payload); UI.toast.success('Welpe aktualisiert.'); } else { await API.litters.addPuppy(litterId, payload); UI.toast.success('Welpe hinzugefügt.'); } UI.modal.close(); // Welpen-Liste für diesen Wurf neu laden await _loadPuppies(litterId); }); }); } // ---------------------------------------------------------- // Kaufvertrag Modal // ---------------------------------------------------------- function _showContractModal(puppy) { const puppyLabel = puppy.name || `Welpe #${puppy.id}`; const body = `
`; const footer = ` `; UI.modal.open({ title: `${UI.icon('file-text')} Kaufvertrag — ${_esc(puppyLabel)}`, body, footer, }); document.getElementById('contract-cancel')?.addEventListener('click', UI.modal.close); document.getElementById('contract-form')?.addEventListener('submit', e => { e.preventDefault(); const fd = new FormData(e.target); const params = new URLSearchParams({ kaeufer_name: fd.get('kaeufer_name')?.trim() || '', kaeufer_adresse: fd.get('kaeufer_adresse')?.trim() || '', kaeufer_email: fd.get('kaeufer_email')?.trim() || '', preis: fd.get('preis')?.trim() || '', }); const url = `/api/litters/puppies/${puppy.id}/contract?` + params.toString(); window.open(url, '_blank'); UI.modal.close(); }); } // ---------------------------------------------------------- // Foto-Verwaltung Modal // ---------------------------------------------------------- async function _showPhotosModal(entityType, entityId, label) { const modalId = 'photos-modal'; const galleryId = 'photos-gallery'; const uploadFormId = 'photos-upload-form'; const visLabels = { public: { text: 'Öffentlich', color: 'var(--c-success,#22C55E)' }, inquiry: { text: 'Anfrage', color: '#F59E0B' }, private: { text: 'Privat', color: 'var(--c-text-muted,#9CA3AF)' }, }; const visOrder = ['public', 'inquiry', 'private']; const body = `

Lädt…


`; const footer = ` `; UI.modal.open({ title: `${UI.icon('images')} Fotos — ${_esc(label)}`, body, footer, }); // Galerie laden async function _loadGallery() { const el = document.getElementById(galleryId); if (!el) return; try { const photos = await API.breederPhotos.list(entityType, entityId); if (!photos.length) { el.innerHTML = `

Noch keine Fotos vorhanden.

`; return; } el.innerHTML = `
${photos.map(ph => { const thumb = ph.thumbnail_url || ph.url || ''; const vis = visLabels[ph.visibility] || visLabels.private; return `
${_esc(ph.caption || '')}
`; }).join('')}
`; // Sichtbarkeit rotieren el.querySelectorAll('.photos-vis-btn').forEach(btn => { btn.addEventListener('click', async () => { const photoId = parseInt(btn.dataset.photoId); const cur = btn.dataset.vis; const next = visOrder[(visOrder.indexOf(cur) + 1) % visOrder.length]; try { await API.breederPhotos.updateVisibility(photoId, next); _loadGallery(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Ändern der Sichtbarkeit.'); } }); }); // Löschen el.querySelectorAll('.photos-del-btn').forEach(btn => { btn.addEventListener('click', async () => { const photoId = parseInt(btn.dataset.photoId); if (!window.confirm('Foto wirklich löschen?')) return; try { await API.breederPhotos.remove(photoId); _loadGallery(); } catch (err) { UI.toast.error(err.message || 'Fehler beim Löschen.'); } }); }); } catch (err) { const el = document.getElementById(galleryId); if (el) el.innerHTML = `

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

`; } } _loadGallery(); // Upload document.getElementById(uploadFormId)?.addEventListener('submit', async e => { e.preventDefault(); const btn = document.getElementById('photos-upload-btn'); const fd = new FormData(e.target); const fileInput = e.target.querySelector('[name="file"]'); if (!fileInput?.files?.length) { UI.toast.error('Bitte eine Datei auswählen.'); return; } const uploadFd = new FormData(); uploadFd.append('entity_type', entityType); uploadFd.append('entity_id', String(entityId)); uploadFd.append('visibility', 'public'); uploadFd.append('file', fileInput.files[0]); await UI.asyncButton(btn, async () => { await API.breederPhotos.upload(uploadFd); UI.toast.success('Foto hochgeladen.'); e.target.reset(); await _loadGallery(); }); }); } // ---------------------------------------------------------- // Tierschutz-Check Modal // ---------------------------------------------------------- function _showWelfareModal(welfare, litterId) { if (!welfare) return; const color = { ok: '#16a34a', info: '#3b82f6', warning: '#f59e0b', critical: '#dc2626' }[welfare.level] || '#6b7280'; const title = { ok: 'Alles prima', info: 'Hinweis', warning: 'Bitte beachten', critical: 'Kritischer Hinweis' }[welfare.level]; const icon = { ok: 'check-circle', info: 'info', warning: 'warning', critical: 'warning-circle' }[welfare.level]; const issueHTML = (welfare.issues || []).map(i => `
${UI.icon('warning')} ${_esc(i.text)}
`).join(''); const okHTML = (welfare.ok_points || []).map(p => `
${UI.icon('check')} ${_esc(p)}
`).join(''); const isProblematic = welfare.level === 'warning' || welfare.level === 'critical'; UI.modal.open({ title: `${UI.icon(icon)} Tierschutz-Check: ${title}`, body: `
${issueHTML || ''} ${okHTML}
${welfare.level === 'critical' ? `
${UI.icon('info')} Wenn du fortfährst, wird der Administrator informiert.
` : ''} `, footer: isProblematic ? `
` : ` `, }); document.getElementById('welfare-back-btn')?.addEventListener('click', () => { UI.modal.close?.(); const litter = _litters.find(l => l.id === litterId); API.litters.remove(litterId).catch(() => {}); _litters = _litters.filter(l => l.id !== litterId); _renderList(); setTimeout(() => _showLitterForm(null), 150); }); document.getElementById('welfare-confirm-btn')?.addEventListener('click', async () => { await API.litters.welfareConfirm(litterId).catch(() => {}); UI.modal.close?.(); UI.toast.info('Wurf gespeichert.'); }); } // ---------------------------------------------------------- // KI: Wurfankündigung // ---------------------------------------------------------- async function _showKiAnnouncement(litterId) { UI.modal.open({ title: `${UI.icon('sparkle')} KI-Wurfankündigung`, body: `

KI schreibt Wurfankündigung…

`, footer: '', }); let text = ''; try { const result = await API.zuchtKi.wurfankuendigung(litterId); text = result.text || result.content || result.ankuendigung || JSON.stringify(result); } catch (err) { UI.modal.open({ title: `${UI.icon('sparkle')} KI-Wurfankündigung`, body: `

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

`, footer: ``, }); return; } UI.modal.open({ title: `${UI.icon('sparkle')} KI-Wurfankündigung`, body: `
${_esc(text)}
`, footer: ` `, }); document.getElementById('ki-announce-copy')?.addEventListener('click', async () => { try { await navigator.clipboard.writeText(text); UI.toast.success('Text kopiert.'); } catch { UI.toast.error('Kopieren nicht möglich.'); } }); document.getElementById('ki-announce-use')?.addEventListener('click', async () => { const btn = document.getElementById('ki-announce-use'); await UI.asyncButton(btn, async () => { await API.litters.update(litterId, { beschreibung: text }); UI.modal.close(); UI.toast.success('Beschreibung aktualisiert.'); await _loadLitters(); }); }); } return { init, refresh, onDogChange }; })();