Tierschutz-System (immer aktiv, nicht abschaltbar): - welfare_check.py: regelbasierte Prüfung IK, Alter, Deckpause, Wurfanzahl, Genetik - Grün/Gelb/Rot-Modal bei Wurf anlegen + Probeverpaarung - Bei kritischem Befund + "Trotzdem fortfahren" → automatische Admin-Mail - Tierschutz-Check nie durch Nutzer deaktivierbar KI-Züchter-Features (pro User an/abschaltbar außer Tierschutz): - routes/zucht_ki.py: 5 Endpunkte — Wurfankündigung, Genetik-Erklärung, Paarungsanalyse, Hund-Beschreibung, Jahresbericht - Toggles in Einstellungen (ki_zucht_* Felder) - KI-Buttons in litters.js + zuchthunde.js KI-Routing: Privilegierte Rollen (Admin, Züchter, Moderator, Manager) nutzen Claude Sonnet primär, lokales LLM als Fallback Datenexport: routes/breeder_export.py — ZIP mit HTML-Dossier + ODS (odfpy hinzugefügt in requirements.txt) Admin-Profil: POST /admin/breeder/create-profile für Schnellprofil ohne Antragsprozess; Admin-Rolle bleibt erhalten Wurfformular: Dropdown aus Zuchtkartei für Vater/Mutter mit Auto-Fill; litters.vater_id + mutter_id als FK auf zucht_hunde Probeverpaarung: heart-fill Icon + Welfare-Block im Ergebnis Landing Page: Züchter-Section + Feature-Gruppe, Meta-Tags, JSON-LD, keywords, softwareVersion 2.1 SEO: llms.txt vollständig überarbeitet, robots.txt Züchter-Pfade, sitemap.xml um Wurfbörse + Züchter-Profile erweitert SW by-v474, APP_VER 451
1143 lines
47 KiB
JavaScript
1143 lines
47 KiB
JavaScript
/* ============================================================
|
||
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
|
||
|
||
// ----------------------------------------------------------
|
||
// Hilfsfunktionen
|
||
// ----------------------------------------------------------
|
||
function _emptyState(icon, title, text) {
|
||
return `
|
||
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon(icon)}</div>
|
||
<h3 style="margin:0 0 var(--space-2)">${_esc(title)}</h3>
|
||
<p style="color:var(--c-text-secondary);margin:0">${_esc(text)}</p>
|
||
</div>`;
|
||
}
|
||
|
||
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 `<span class="litters-badge" style="background:${s.color}">${_esc(s.label)}</span>`;
|
||
}
|
||
|
||
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 `<span class="litters-badge litters-badge--sm" style="background:${s.color}">${_esc(s.label)}</span>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 = `
|
||
<div class="litters-layout">
|
||
<div class="by-toolbar">
|
||
<h2 style="margin:0;font-size:var(--text-lg);font-weight:var(--weight-semibold)">
|
||
${UI.icon('dog')} Meine Würfe
|
||
</h2>
|
||
<button class="btn btn-primary btn-sm" id="litters-new-btn">
|
||
${UI.icon('plus')} Neuer Wurf
|
||
</button>
|
||
</div>
|
||
<div id="litters-list">
|
||
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt…</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
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 _renderList() {
|
||
const el = document.getElementById('litters-list');
|
||
if (!el) return;
|
||
|
||
if (!_litters.length) {
|
||
el.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('dog')}</div>
|
||
<p style="color:var(--c-text-secondary)">Noch keine Würfe angelegt.</p>
|
||
<button class="btn btn-primary" style="margin-top:var(--space-4)" id="litters-first-btn">
|
||
${UI.icon('plus')} Ersten Wurf anlegen
|
||
</button>
|
||
</div>`;
|
||
document.getElementById('litters-first-btn')?.addEventListener('click', () => _showLitterForm(null));
|
||
return;
|
||
}
|
||
|
||
el.innerHTML = _litters.map(l => _litterCardHTML(l)).join('');
|
||
|
||
// Events
|
||
el.querySelectorAll('.litters-card-toggle').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
_togglePuppies(id);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-photos-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
const litter = _litters.find(l => l.id === id);
|
||
if (litter) _showPhotosModal('litter', litter.id, litter.zwingername || `Wurf #${litter.id}`);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-parent-photos-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
const litter = _litters.find(l => l.id === id);
|
||
if (!litter) return;
|
||
const label = [litter.vater_name, litter.mutter_name].filter(Boolean).join(' × ') || `Eltern Wurf #${id}`;
|
||
_showPhotosModal('parent', litter.id, label);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-edit-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
const litter = _litters.find(l => l.id === id);
|
||
if (litter) _showLitterForm(litter);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-delete-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
_deleteLitter(id);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-ki-announce-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
_showKiAnnouncement(id);
|
||
});
|
||
});
|
||
|
||
el.querySelectorAll('.litters-add-puppy-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const id = parseInt(btn.dataset.id);
|
||
_showPuppyForm(id, null);
|
||
});
|
||
});
|
||
|
||
// Aufgeklappten Wurf wiederherstellen
|
||
if (_openId) _togglePuppies(_openId, true);
|
||
}
|
||
|
||
function _litterCardHTML(l) {
|
||
const verfuegbar = l.welpen_verfuegbar != null ? l.welpen_verfuegbar : '?';
|
||
const gesamt = l.welpen_gesamt != null ? l.welpen_gesamt : '?';
|
||
const datumLabel = l.geburt_datum
|
||
? `Geburt: ${_fmtDate(l.geburt_datum)}`
|
||
: l.erwartetes_datum
|
||
? `Erwartet: ${_fmtDate(l.erwartetes_datum)}`
|
||
: '—';
|
||
|
||
const elternLabel = [l.vater_name, l.mutter_name]
|
||
.filter(Boolean)
|
||
.map(n => _esc(n))
|
||
.join(' × ') || '—';
|
||
|
||
const sichtbarLabel = l.sichtbar
|
||
? `<span style="color:var(--c-success);font-size:var(--text-xs)">${UI.icon('eye')} Öffentlich</span>`
|
||
: `<span style="color:var(--c-text-muted);font-size:var(--text-xs)">${UI.icon('eye-slash')} Nicht öffentlich</span>`;
|
||
|
||
return `
|
||
<div class="litters-card" id="litter-card-${l.id}">
|
||
<div class="litters-card-header">
|
||
<div style="flex:1;min-width:0">
|
||
<div class="litters-card-title">
|
||
${elternLabel}
|
||
${_statusBadge(l.status)}
|
||
</div>
|
||
<div class="litters-card-meta">
|
||
${UI.icon('calendar-dots')} ${_esc(datumLabel)} ·
|
||
${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar
|
||
· ${sichtbarLabel}
|
||
</div>
|
||
${l.preis_spanne ? `<div class="litters-card-meta">${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}</div>` : ''}
|
||
</div>
|
||
<div class="litters-card-actions">
|
||
<button class="btn btn-ghost btn-sm litters-card-toggle" data-id="${l.id}"
|
||
title="Welpen anzeigen">
|
||
${UI.icon('caret-down')} Welpen
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-photos-btn" data-id="${l.id}"
|
||
title="Wurf-Fotos verwalten">
|
||
${UI.icon('images')} Fotos
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-parent-photos-btn" data-id="${l.id}"
|
||
title="Elterntier-Fotos verwalten">
|
||
${UI.icon('users')} Eltern
|
||
</button>
|
||
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? `
|
||
<button class="btn btn-ghost btn-sm litters-ki-announce-btn" data-id="${l.id}"
|
||
title="KI: Wurfankündigung schreiben">
|
||
${UI.icon('sparkle')} Ankündigung
|
||
</button>` : ''}
|
||
<button class="btn btn-ghost btn-sm litters-edit-btn" data-id="${l.id}"
|
||
title="Bearbeiten">
|
||
${UI.icon('pencil-simple')}
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-delete-btn" data-id="${l.id}"
|
||
title="Löschen" style="color:var(--c-danger)">
|
||
${UI.icon('trash')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
${l.beschreibung ? `<div class="litters-card-desc">${_esc(l.beschreibung)}</div>` : ''}
|
||
<div class="litters-puppies-wrap" id="puppies-wrap-${l.id}" style="display:none">
|
||
<div class="litters-puppies-inner" id="puppies-inner-${l.id}">
|
||
<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt…</p>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm litters-add-puppy-btn" data-id="${l.id}"
|
||
style="margin-top:var(--space-3)">
|
||
${UI.icon('plus')} Welpen hinzufügen
|
||
</button>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`;
|
||
}
|
||
}
|
||
|
||
function _renderPuppies(container, litterId, puppies) {
|
||
if (!puppies.length) {
|
||
container.innerHTML = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Welpen eingetragen.</p>`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = puppies.map(p => `
|
||
<div class="litters-puppy-row" data-puppy-id="${p.id}">
|
||
<div class="litters-puppy-info">
|
||
${_genderIcon(p.geschlecht)}
|
||
<span class="litters-puppy-name">${p.name ? _esc(p.name) : '<em style="color:var(--c-text-muted)">Unbenannt</em>'}</span>
|
||
${p.farbe ? `<span style="color:var(--c-text-secondary);font-size:var(--text-xs)">${_esc(p.farbe)}</span>` : ''}
|
||
${_puppyStatusBadge(p.status)}
|
||
<span class="litters-puppy-last-weight" id="puppy-last-weight-${p.id}" style="font-size:var(--text-xs);color:var(--c-text-secondary)"></span>
|
||
</div>
|
||
<div class="litters-puppy-actions">
|
||
<button class="btn btn-ghost btn-xs litters-puppy-photo-btn" data-litter-id="${litterId}" data-puppy-id="${p.id}"
|
||
title="Fotos">${UI.icon('image')}</button>
|
||
<button class="btn btn-ghost btn-xs litters-puppy-weight-btn" data-litter-id="${litterId}" data-puppy-id="${p.id}"
|
||
title="Gewichtsverlauf">${UI.icon('scales')}</button>
|
||
<button class="btn btn-ghost btn-xs litters-puppy-contract-btn" data-puppy-id="${p.id}"
|
||
title="Kaufvertrag">${UI.icon('file-text')}</button>
|
||
<button class="btn btn-ghost btn-xs litters-puppy-edit-btn" data-litter-id="${litterId}" data-puppy-id="${p.id}"
|
||
title="Welpe bearbeiten">${UI.icon('pencil-simple')}</button>
|
||
</div>
|
||
</div>`).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 = `
|
||
<div id="weight-history" style="margin-bottom:var(--space-3)">
|
||
<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt…</p>
|
||
</div>
|
||
<hr style="margin:var(--space-3) 0;border:none;border-top:1px solid var(--c-border)">
|
||
<form id="weight-form" style="display:flex;gap:var(--space-2);align-items:flex-end">
|
||
<div style="flex:1">
|
||
<label style="display:block;font-size:var(--text-xs);color:var(--c-text-secondary);margin-bottom:var(--space-1)">Gewicht (g)</label>
|
||
<input class="form-control" name="gewicht_g" type="number" min="1" max="99999" step="1" required placeholder="z. B. 420">
|
||
</div>
|
||
<div style="flex:1">
|
||
<label style="display:block;font-size:var(--text-xs);color:var(--c-text-secondary);margin-bottom:var(--space-1)">Datum</label>
|
||
<input class="form-control" name="gemessen_am" type="date" required value="${today}">
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="submit" form="weight-form" class="btn btn-primary" id="weight-submit">
|
||
${UI.icon('floppy-disk')} Speichern
|
||
</button>
|
||
`;
|
||
|
||
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 = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Messungen eingetragen.</p>`;
|
||
return;
|
||
}
|
||
el.innerHTML = `
|
||
<table style="width:100%;font-size:var(--text-sm);border-collapse:collapse">
|
||
<thead>
|
||
<tr style="color:var(--c-text-secondary);font-size:var(--text-xs)">
|
||
<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)">Gewicht</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${weights.map((w, i) => `
|
||
<tr style="border-top:1px solid var(--c-border)${i === 0 ? ';font-weight:var(--weight-semibold)' : ''}">
|
||
<td style="padding:var(--space-1) var(--space-2)">${_fmtDate(w.gemessen_am)}</td>
|
||
<td style="padding:var(--space-1) var(--space-2);text-align:right">${w.gewicht_g} g</td>
|
||
</tr>`).join('')}
|
||
</tbody>
|
||
</table>`;
|
||
} catch (err) {
|
||
el.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`;
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 `<option value="${h.id}" data-name="${_esc(h.name)}" ${currentId == h.id ? 'selected' : ''}>${_esc(label)}</option>`;
|
||
}).join('');
|
||
return `
|
||
<select class="form-control" name="${idName}" id="${idName}-sel" style="margin-bottom:var(--space-2)">
|
||
<option value="">— ${placeholder} —</option>
|
||
${opts}
|
||
</select>
|
||
<input class="form-control" type="text" name="${name}" id="${name}-txt"
|
||
value="${_esc(currentName || '')}" placeholder="oder Namen frei eingeben">`;
|
||
};
|
||
|
||
const body = `
|
||
<form id="litter-form" autocomplete="off">
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Vater</label>
|
||
${buildSelect('vater_name', 'vater_id', maennlich, v.vater_id, v.vater_name, 'Aus Zuchtkartei')}
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Mutter</label>
|
||
${buildSelect('mutter_name', 'mutter_id', weiblich, v.mutter_id, v.mutter_name, 'Aus Zuchtkartei')}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Geburtsdatum</label>
|
||
<input class="form-control" type="date" name="geburt_datum"
|
||
value="${_esc(v.geburt_datum || '')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Erwartetes Datum</label>
|
||
<input class="form-control" type="date" name="erwartetes_datum"
|
||
value="${_esc(v.erwartetes_datum || '')}">
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Welpen gesamt</label>
|
||
<input class="form-control" type="number" name="welpen_gesamt" min="0"
|
||
value="${v.welpen_gesamt != null ? v.welpen_gesamt : ''}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Welpen verfügbar</label>
|
||
<input class="form-control" type="number" name="welpen_verfuegbar" min="0"
|
||
value="${v.welpen_verfuegbar != null ? v.welpen_verfuegbar : ''}">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Status</label>
|
||
<select class="form-control" name="status">
|
||
<option value="geplant" ${v.status === 'geplant' ? 'selected' : ''}>Geplant</option>
|
||
<option value="geboren" ${v.status === 'geboren' ? 'selected' : ''}>Geboren</option>
|
||
<option value="verfuegbar" ${v.status === 'verfuegbar' ? 'selected' : ''}>Verfügbar</option>
|
||
<option value="abgeschlossen" ${v.status === 'abgeschlossen' ? 'selected' : ''}>Abgeschlossen</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Preisspanne</label>
|
||
<input class="form-control" type="text" name="preis_spanne"
|
||
value="${_esc(v.preis_spanne || '')}" placeholder="z. B. 1.500 – 2.000 €">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Beschreibung <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<textarea class="form-control" name="beschreibung" rows="3"
|
||
placeholder="Elternlinie, Besonderheiten, Charakter…">${_esc(v.beschreibung || '')}</textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Gesundheitstests <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<textarea class="form-control" name="gesundheitstests" rows="2"
|
||
placeholder="HD, ED, Gentest, Augenkontrolle…">${_esc(v.gesundheitstests || '')}</textarea>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
||
<input type="checkbox" name="sichtbar" value="1" ${v.sichtbar ? 'checked' : ''}>
|
||
Öffentlich sichtbar
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Sichtbar bis <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<input class="form-control" type="date" name="sichtbar_bis"
|
||
value="${_esc(v.sichtbar_bis || '')}">
|
||
</div>
|
||
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="lf-cancel">Abbrechen</button>
|
||
<button type="submit" form="litter-form" class="btn btn-primary flex-1" id="lf-submit">
|
||
${isEdit ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Anlegen`}
|
||
</button>
|
||
`;
|
||
|
||
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 = `
|
||
<form id="puppy-form" autocomplete="off">
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Name <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<input class="form-control" type="text" name="name"
|
||
value="${_esc(v.name || '')}" placeholder="z. B. Max">
|
||
</div>
|
||
<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>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Farbe / Fellzeichnung</label>
|
||
<input class="form-control" type="text" name="farbe"
|
||
value="${_esc(v.farbe || '')}" placeholder="z. B. schwarz-braun">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Status</label>
|
||
<select class="form-control" name="status">
|
||
<option value="verfuegbar" ${v.status === 'verfuegbar' ? 'selected' : ''}>Verfügbar</option>
|
||
<option value="reserviert" ${v.status === 'reserviert' ? 'selected' : ''}>Reserviert</option>
|
||
<option value="abgegeben" ${v.status === 'abgegeben' ? 'selected' : ''}>Abgegeben</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||
<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">Geburtsgewicht (g)</label>
|
||
<input class="form-control" type="number" name="geburtsgewicht" min="0" step="1"
|
||
value="${v.geburtsgewicht != null ? v.geburtsgewicht : ''}">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
||
<input type="checkbox" name="status_sichtbar" value="1" ${v.status_sichtbar !== 0 ? 'checked' : ''}>
|
||
Status öffentlich anzeigen
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Notiz <span style="color:var(--c-text-secondary)">(intern)</span></label>
|
||
<textarea class="form-control" name="notiz" rows="2"
|
||
placeholder="Interne Notizen…">${_esc(v.notiz || '')}</textarea>
|
||
</div>
|
||
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="pf-cancel">Abbrechen</button>
|
||
<button type="submit" form="puppy-form" class="btn btn-primary flex-1" id="pf-submit">
|
||
${isEdit ? `${UI.icon('floppy-disk')} Speichern` : `${UI.icon('plus')} Hinzufügen`}
|
||
</button>
|
||
`;
|
||
|
||
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 = `
|
||
<form id="contract-form" autocomplete="off">
|
||
<div class="form-group">
|
||
<label class="form-label">Name des Käufers <span style="color:var(--c-danger)">*</span></label>
|
||
<input class="form-control" type="text" name="kaeufer_name" required
|
||
placeholder="Vor- und Nachname">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Adresse des Käufers <span style="color:var(--c-danger)">*</span></label>
|
||
<textarea class="form-control" name="kaeufer_adresse" rows="2" required
|
||
placeholder="Straße, PLZ, Ort"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">E-Mail des Käufers <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<input class="form-control" type="email" name="kaeufer_email"
|
||
placeholder="kaeufer@beispiel.de">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Kaufpreis <span style="color:var(--c-text-secondary)">(optional)</span></label>
|
||
<input class="form-control" type="text" name="preis"
|
||
placeholder="z. B. 1.500 €">
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="button" class="btn btn-secondary flex-1" id="contract-cancel">Abbrechen</button>
|
||
<button type="submit" form="contract-form" class="btn btn-primary flex-1" id="contract-submit">
|
||
${UI.icon('file-text')} Vertrag erstellen
|
||
</button>
|
||
`;
|
||
|
||
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 = `
|
||
<div id="${galleryId}" style="margin-bottom:var(--space-4)">
|
||
<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt…</p>
|
||
</div>
|
||
<hr style="margin:var(--space-3) 0;border:none;border-top:1px solid var(--c-border)">
|
||
<form id="${uploadFormId}" style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||
<label style="font-size:var(--text-sm);font-weight:var(--weight-semibold)">
|
||
${UI.icon('upload-simple')} Foto hochladen
|
||
</label>
|
||
<input class="form-control" type="file" name="file" accept="image/*,.pdf" required>
|
||
</form>
|
||
`;
|
||
|
||
const footer = `
|
||
<button type="submit" form="${uploadFormId}" class="btn btn-primary" id="photos-upload-btn">
|
||
${UI.icon('upload-simple')} Hochladen
|
||
</button>
|
||
`;
|
||
|
||
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 = `<p style="color:var(--c-text-muted);font-size:var(--text-sm)">Noch keine Fotos vorhanden.</p>`;
|
||
return;
|
||
}
|
||
el.innerHTML = `
|
||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(90px,1fr));gap:var(--space-2)">
|
||
${photos.map(ph => {
|
||
const thumb = ph.thumbnail_url || ph.url || '';
|
||
const vis = visLabels[ph.visibility] || visLabels.private;
|
||
return `
|
||
<div style="position:relative;border-radius:var(--radius-md);overflow:hidden;border:1px solid var(--c-border);aspect-ratio:1">
|
||
<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.src='/static/img/placeholder.webp'">
|
||
</a>
|
||
<button class="photos-vis-btn"
|
||
data-photo-id="${ph.id}"
|
||
data-vis="${_esc(ph.visibility)}"
|
||
title="Sichtbarkeit ändern"
|
||
style="position:absolute;bottom:0;left:0;right:0;
|
||
background:${vis.color};color:#fff;
|
||
border:none;cursor:pointer;font-size:10px;padding:2px 4px;
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||
${_esc(vis.text)}
|
||
</button>
|
||
<button class="photos-del-btn"
|
||
data-photo-id="${ph.id}"
|
||
title="Foto löschen"
|
||
style="position:absolute;top:2px;right:2px;
|
||
background:rgba(0,0,0,.55);color:#fff;
|
||
border:none;border-radius:50%;cursor:pointer;
|
||
width:22px;height:22px;display:flex;align-items:center;justify-content:center;font-size:12px">
|
||
${UI.icon('x')}
|
||
</button>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>`;
|
||
|
||
// 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 = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler beim Laden.')}</p>`;
|
||
}
|
||
}
|
||
|
||
_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 => `
|
||
<div style="display:flex;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)">
|
||
<span style="color:${color};flex-shrink:0">${UI.icon('warning')}</span>
|
||
<span style="font-size:var(--text-sm)">${_esc(i.text)}</span>
|
||
</div>`).join('');
|
||
|
||
const okHTML = (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 style="font-size:var(--text-sm);color:var(--c-text-secondary)">${_esc(p)}</span>
|
||
</div>`).join('');
|
||
|
||
const isProblematic = welfare.level === 'warning' || welfare.level === 'critical';
|
||
|
||
UI.modal.open({
|
||
title: `<span style="color:${color}">${UI.icon(icon)} Tierschutz-Check: ${title}</span>`,
|
||
body: `
|
||
<div style="background:${color}18;border:1.5px solid ${color}40;border-radius:var(--radius-md);
|
||
padding:var(--space-4);margin-bottom:var(--space-4)">
|
||
${issueHTML || ''}
|
||
${okHTML}
|
||
</div>
|
||
${welfare.level === 'critical' ? `
|
||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
||
background:var(--c-surface-2);border-radius:var(--radius-sm);
|
||
padding:var(--space-3)">
|
||
${UI.icon('info')} Wenn du fortfährst, wird der Administrator informiert.
|
||
</div>` : ''}
|
||
`,
|
||
footer: isProblematic ? `
|
||
<div style="display:flex;gap:var(--space-2);width:100%">
|
||
<button class="btn btn-secondary flex-1" id="welfare-back-btn">
|
||
${UI.icon('arrow-left')} Zurück
|
||
</button>
|
||
<button class="btn btn-ghost flex-1" id="welfare-confirm-btn"
|
||
style="color:${color}">
|
||
Trotzdem fortfahren
|
||
</button>
|
||
</div>` : `
|
||
<button class="btn btn-primary" data-modal-close style="width:100%">
|
||
${UI.icon('check')} Verstanden
|
||
</button>`,
|
||
});
|
||
|
||
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: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">
|
||
KI schreibt Wurfankündigung…
|
||
</p>`,
|
||
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: `<p style="color:var(--c-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-Wurfankündigung`,
|
||
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-announce-copy">
|
||
${UI.icon('clipboard-text')} Kopieren
|
||
</button>
|
||
<button class="btn btn-primary flex-1" id="ki-announce-use">
|
||
${UI.icon('check')} In Beschreibung übernehmen
|
||
</button>`,
|
||
});
|
||
|
||
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 };
|
||
|
||
})();
|