/* ============================================================
BAN YARO — Wurfverwaltung
Züchter verwalten ihre Würfe und Welpen
============================================================ */
window.Page_litters = (() => {
let _container = null;
let _appState = null;
let _litters = [];
let _openId = null;
let _filterStatus = null;
let _breederInfo = null; // { zwingername, logo_url }
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _emptyState(icon, title, text) {
return `
${UI.icon(icon)}
${UI.escape(title)}
${UI.escape(text)}
`;
}
function _statusBadge(status) {
const map = {
geplant: { label: 'Geplant', cls: 'badge-warning' },
geboren: { label: 'Geboren', cls: 'badge-primary' },
verfuegbar: { label: 'Verfügbar', cls: 'badge-success' },
abgeschlossen: { label: 'Abgeschlossen', cls: 'badge-muted' },
};
const s = map[status] || { label: status, cls: 'badge-muted' };
return `${UI.escape(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', cls: 'badge-success' },
reserviert: { label: 'Reserviert', cls: 'badge-warning' },
abgegeben: { label: 'Abgegeben', cls: 'badge-muted' },
};
const s = map[status] || { label: status, cls: 'badge-muted' };
return `${UI.escape(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();
API.breeder.status().then(s => {
_breederInfo = s?.profile ? { zwingername: s.profile.zwingername, logo_url: s.profile.logo_url } : null;
// Header nach Laden der Info neu rendern
const headerEl = _container.querySelector('#breeder-private-header');
if (headerEl) headerEl.outerHTML = _privateHeader();
}).catch(() => {});
await _loadLitters();
}
function refresh() {
const u = _appState?.user;
if (!u || (u.rolle !== 'breeder' && u.rolle !== 'admin')) return;
_loadLitters();
}
function onDogChange() {}
// ----------------------------------------------------------
// Grundstruktur rendern
// ----------------------------------------------------------
function _privateHeader() {
const zwinger = _breederInfo?.zwingername || 'Mein Zwinger';
const logoUrl = _breederInfo?.logo_url || null;
const logoHtml = logoUrl
? `
`
: `
`;
return `
`;
}
function _render() {
_container.innerHTML = `
${_privateHeader()}
${UI.icon('certificate')} Meine Würfe
`;
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 = true;
return `
`;
}).join('');
bar.querySelectorAll('[data-stat-idx]').forEach(chip => {
const s = statItems[parseInt(chip.dataset.statIdx)];
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 => UI.escape(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')} ${UI.escape(l.preis_spanne)}`
: '';
return `
${(l.wurf_rang || l.wurf_name) ? `
${l.wurf_rang ? `${UI.escape(l.wurf_rang)}-Wurf` : ''}
${l.wurf_name ? `${UI.escape(l.wurf_name)}` : ''}
` : ''}
${elternLabel}
${_statusBadge(l.status)}
${sichtbarChip}
${datumChip}
${welpenChip}
${preisChip}
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? `
` : ''}
${l.beschreibung ? `
${UI.escape(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 = `${UI.escape(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 ? UI.escape(p.name) : 'Unbenannt'}
${p.farbe ? `${UI.escape(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 = `
`;
const footer = `
`;
UI.modal.open({
title: `${UI.icon('scales')} Gewichtsverlauf — ${UI.escape(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 = `
Zunahme
${gain >= 0 ? '+' : ''}${gain} g
Messungen
${weights.length}
${asc.length > 1 ? `
${firstDate}${lastDate}
` : ''}
| Datum |
Gewicht |
Veränderung |
${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 `
| ${_fmtDate(w.gemessen_am)} |
${w.gewicht_g} g |
${diffStr} |
`;
}).join('')}
`;
} catch (err) {
el.innerHTML = `${UI.escape(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 = `${UI.escape(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}
${UI.escape(e.name)}
${_wlStatusBadge(e.status)}
${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}` : ''}
${e.wunsch_farbe ? `${UI.escape(e.wunsch_farbe)}` : ''}
${e.email ? `${UI.icon('envelope')} ${UI.escape(e.email)}` : ''}
${e.telefon ? `${UI.icon('phone')} ${UI.escape(e.telefon)}` : ''}
${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'}
${e.nachricht ? `
"${UI.escape(e.nachricht)}"
` : ''}
${e.notiz ? `
${UI.icon('note-pencil')} ${UI.escape(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 rangOpts = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(l =>
``
).join('');
const body = `
`;
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 = {
wurf_rang: fd.get('wurf_rang') || null,
wurf_name: fd.get('wurf_name')?.trim() || null,
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 — ${UI.escape(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 = `
`;
const footer = `
`;
UI.modal.open({
title: `${UI.icon('images')} Fotos — ${UI.escape(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 `
`;
}).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 = `${UI.escape(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')}
${UI.escape(i.text)}
`).join('');
const okHTML = (welfare.ok_points || []).map(p => `
${UI.icon('check')}
${UI.escape(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: `${UI.escape(err.message || 'Fehler beim Generieren.')}
`,
footer: ``,
});
return;
}
UI.modal.open({
title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
body: `${UI.escape(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 };
})();