PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
1583 lines
71 KiB
JavaScript
1583 lines
71 KiB
JavaScript
/* ============================================================
|
||
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 `
|
||
<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', 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 `<span class="badge ${s.cls}">${_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', 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 `<span class="badge badge-sm ${s.cls}">${_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();
|
||
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
|
||
? `<img src="${_esc(logoUrl)}" alt="Logo"
|
||
style="width:48px;height:48px;border-radius:50%;object-fit:cover;
|
||
border:2px solid rgba(196,132,58,.5);flex-shrink:0"
|
||
onerror="this.style.display='none'">`
|
||
: `<div style="width:48px;height:48px;border-radius:50%;background:rgba(196,132,58,.15);
|
||
border:2px solid rgba(196,132,58,.4);display:flex;align-items:center;
|
||
justify-content:center;flex-shrink:0">
|
||
<svg style="width:24px;height:24px;color:var(--c-primary)" viewBox="0 0 256 256">
|
||
<use href="/icons/phosphor.svg#paw-print"></use>
|
||
</svg>
|
||
</div>`;
|
||
return `
|
||
<div id="breeder-private-header" style="background:var(--c-bg-secondary);
|
||
border-bottom:1px solid var(--c-border);
|
||
padding:var(--space-3) var(--space-4);
|
||
display:flex;align-items:center;gap:var(--space-3)">
|
||
${logoHtml}
|
||
<div class="flex-1-min">
|
||
<h2 style="margin:0 0 2px;font-size:var(--text-lg);font-weight:700;
|
||
color:var(--c-text);white-space:nowrap;overflow:hidden;
|
||
text-overflow:ellipsis;line-height:1.2">${_esc(zwinger)}</h2>
|
||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||
<svg style="width:11px;height:11px;color:var(--c-primary);flex-shrink:0" viewBox="0 0 256 256">
|
||
<use href="/icons/phosphor.svg#lock-key"></use>
|
||
</svg>
|
||
<span class="text-xs-secondary">Privater Bereich · Nur du siehst das</span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function _render() {
|
||
_container.innerHTML = `
|
||
<div class="litters-layout">
|
||
${_privateHeader()}
|
||
<div class="by-toolbar" style="flex-wrap:wrap">
|
||
<h2 style="margin:0;font-size:var(--text-lg);font-weight:var(--weight-semibold)">
|
||
${UI.icon('certificate')} 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-stats" style="display:none;gap:var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap"></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 _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 `
|
||
<div data-stat-idx="${i}"
|
||
style="background:${isActive ? 'var(--c-primary)' : 'var(--c-bg-secondary)'};
|
||
border:1px solid ${isActive ? 'var(--c-primary)' : 'var(--c-border)'};
|
||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);
|
||
display:flex;align-items:center;gap:var(--space-2);flex:1;min-width:90px;
|
||
${clickable ? 'cursor:pointer;user-select:none;transition:opacity .15s' : ''}">
|
||
<span style="color:${isActive ? 'white' : (s.color || 'var(--c-text-muted)')};opacity:.9">${UI.icon(s.icon)}</span>
|
||
<div>
|
||
<div style="font-size:var(--text-lg);font-weight:700;
|
||
color:${isActive ? 'white' : (s.color || 'var(--c-text)')};line-height:1">${s.val}</div>
|
||
<div style="font-size:var(--text-xs);color:${isActive ? 'rgba(255,255,255,.75)' : 'var(--c-text-muted)'}">${s.label}</div>
|
||
</div>
|
||
</div>`;
|
||
}).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 = `
|
||
<div style="text-align:center;padding:var(--space-8) var(--space-4);
|
||
border:1px dashed var(--c-border);border-radius:var(--radius-lg)">
|
||
<p class="text-muted">Keine Würfe für diesen Filter.</p>
|
||
</div>`;
|
||
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 = `
|
||
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
||
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
|
||
<p class="text-secondary">Noch keine Würfe angelegt.</p>
|
||
<button class="btn btn-primary mt-4" id="litters-first-btn">
|
||
${UI.icon('plus')} Ersten Wurf anlegen
|
||
</button>
|
||
</div>`;
|
||
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 ? `<span class="text-danger">überfällig</span>`
|
||
: days === 0 ? `<span class="text-success">heute!</span>`
|
||
: days <= 7 ? `<span style="color:var(--c-warning,#f59e0b)">${days}d</span>`
|
||
: `<span class="text-muted">${days}d</span>`;
|
||
countdownHtml = ` · ${c}`;
|
||
}
|
||
datumChip = `<span style="display:inline-flex;align-items:center;gap:4px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('calendar-dots')} ${label}${countdownHtml}</span>`;
|
||
}
|
||
|
||
const sichtbarChip = l.sichtbar
|
||
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-success)">${UI.icon('eye')} Öffentlich</span>`
|
||
: `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-muted)">${UI.icon('eye-slash')} Nicht öffentlich</span>`;
|
||
|
||
const welpenChip = `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('dog')} ${verfuegbar}/${gesamt} verfügbar</span>`;
|
||
|
||
const preisChip = l.preis_spanne
|
||
? `<span style="display:inline-flex;align-items:center;gap:3px;font-size:var(--text-xs);color:var(--c-text-secondary)">${UI.icon('currency-eur')} ${_esc(l.preis_spanne)}</span>`
|
||
: '';
|
||
|
||
return `
|
||
<div class="litters-card" id="litter-card-${l.id}"
|
||
style="background:var(--c-bg-secondary);border:1px solid var(--c-border);border-radius:var(--radius-lg);
|
||
margin-bottom:var(--space-3);overflow:hidden">
|
||
|
||
<!-- Card-Header -->
|
||
<div style="padding:var(--space-4) var(--space-4) var(--space-3);border-bottom:1px solid var(--c-border)">
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap">
|
||
<div style="min-width:0">
|
||
${(l.wurf_rang || l.wurf_name) ? `
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
|
||
${l.wurf_rang ? `<span style="background:var(--c-primary);color:white;border-radius:999px;padding:1px 10px;font-size:var(--text-xs);font-weight:700">${_esc(l.wurf_rang)}-Wurf</span>` : ''}
|
||
${l.wurf_name ? `<span style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${_esc(l.wurf_name)}</span>` : ''}
|
||
</div>` : ''}
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-2)">
|
||
<span class="text-sm-secondary">${elternLabel}</span>
|
||
${_statusBadge(l.status)}
|
||
${sichtbarChip}
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap">
|
||
${datumChip}
|
||
${welpenChip}
|
||
${preisChip}
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:var(--space-1);flex-shrink:0;flex-wrap:wrap">
|
||
<button class="btn btn-ghost btn-sm litters-card-toggle" data-id="${l.id}" title="Welpen">
|
||
${UI.icon('caret-down')} Welpen
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-waitlist-btn" data-id="${l.id}" title="Warteliste">
|
||
${UI.icon('list-bullets')} Warteliste
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-photos-btn" data-id="${l.id}" title="Wurf-Fotos">
|
||
${UI.icon('images')}
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm litters-parent-photos-btn" data-id="${l.id}" title="Elterntier-Fotos">
|
||
${UI.icon('users')}
|
||
</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">
|
||
${UI.icon('sparkle')}
|
||
</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"
|
||
class="text-danger">
|
||
${UI.icon('trash')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
${l.beschreibung ? `<p style="margin-top:var(--space-2);font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.5">${_esc(l.beschreibung)}</p>` : ''}
|
||
</div>
|
||
|
||
<!-- Welpen-Bereich -->
|
||
<div id="puppies-wrap-${l.id}" style="display:none;padding:var(--space-3) var(--space-4)">
|
||
<div id="puppies-inner-${l.id}">
|
||
<p class="text-sm-muted">Lädt…</p>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm litters-add-puppy-btn" data-id="${l.id}"
|
||
class="mt-3">
|
||
${UI.icon('plus')} Welpen hinzufügen
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Wartelisten-Bereich -->
|
||
<div id="waitlist-wrap-${l.id}" style="display:none;padding:var(--space-3) var(--space-4)">
|
||
<div id="waitlist-inner-${l.id}">
|
||
<p class="text-sm-muted">Lädt…</p>
|
||
</div>
|
||
<button class="btn btn-secondary btn-sm litters-add-waitlist-btn" data-id="${l.id}"
|
||
class="mt-3">
|
||
${UI.icon('plus')} Interessent eintragen
|
||
</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 class="text-sm-muted">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 class="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}" class="text-xs-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" class="mb-3">
|
||
<p class="text-sm-muted">Lädt…</p>
|
||
</div>
|
||
<hr style="margin:var(--space-3) 0;border:none;border-top:1px solid var(--c-border)">
|
||
<form id="weight-form" style="display:flex;gap:var(--space-2);align-items:flex-end">
|
||
<div class="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 class="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 class="text-sm-muted">Noch keine Messungen eingetragen.</p>`;
|
||
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 = `
|
||
<!-- Stats-Zeile -->
|
||
<div style="display:flex;gap:var(--space-3);flex-wrap:wrap;margin-bottom:var(--space-3)">
|
||
<div class="text-center">
|
||
<div class="text-xs-muted">Aktuell</div>
|
||
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-primary)">${last} g</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-xs-muted">Zunahme</div>
|
||
<div style="font-size:var(--text-base);font-weight:700;color:${gain >= 0 ? 'var(--c-success)' : 'var(--c-danger)'}">
|
||
${gain >= 0 ? '+' : ''}${gain} g
|
||
</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-xs-muted">Ø tägl.</div>
|
||
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${dailyGain} g</div>
|
||
</div>
|
||
<div class="text-center">
|
||
<div class="text-xs-muted">Messungen</div>
|
||
<div style="font-size:var(--text-base);font-weight:700;color:var(--c-text)">${weights.length}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wachstumskurve -->
|
||
${asc.length > 1 ? `
|
||
<div style="background:var(--c-surface);border-radius:var(--radius-md);
|
||
padding:var(--space-2) var(--space-2) var(--space-1);margin-bottom:var(--space-3)">
|
||
<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:80px;display:block" preserveAspectRatio="none">
|
||
<polyline points="${pts}" fill="none" stroke="var(--c-primary)"
|
||
stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
|
||
<!-- Punkte -->
|
||
${asc.map((w, i) => {
|
||
const x = (i / (asc.length - 1)) * W;
|
||
const y = H - 8 - ((w.gewicht_g - minW) / range) * (H - 16);
|
||
return `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="3"
|
||
fill="var(--c-primary)" stroke="var(--c-bg)" stroke-width="1.5"/>`;
|
||
}).join('')}
|
||
</svg>
|
||
<div style="display:flex;justify-content:space-between;font-size:10px;color:var(--c-text-muted);margin-top:2px">
|
||
<span>${firstDate}</span><span>${lastDate}</span>
|
||
</div>
|
||
</div>` : ''}
|
||
|
||
<!-- Tabelle -->
|
||
<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>
|
||
<th style="text-align:right;padding:var(--space-1) var(--space-2)">Veränderung</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${weights.map((w, i) => {
|
||
const prev = weights[i + 1];
|
||
const diff = prev ? w.gewicht_g - prev.gewicht_g : null;
|
||
const diffStr = diff === null ? '—'
|
||
: `<span style="color:${diff >= 0 ? 'var(--c-success)' : 'var(--c-danger)'}">${diff >= 0 ? '+' : ''}${diff} g</span>`;
|
||
return `
|
||
<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>
|
||
<td style="padding:var(--space-1) var(--space-2);text-align:right">${diffStr}</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>`;
|
||
}
|
||
}
|
||
|
||
// ----------------------------------------------------------
|
||
// 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 `<span style="background:${s.color}1a;color:${s.color};border:1px solid ${s.color}40;border-radius:999px;padding:1px 8px;font-size:var(--text-xs);font-weight:600">${s.label}</span>`;
|
||
}
|
||
|
||
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 ? ` <span style="background:var(--c-primary);color:white;border-radius:999px;padding:0 6px;font-size:10px;font-weight:700">${active}</span>` : ''}`;
|
||
}
|
||
} catch (err) {
|
||
inner.innerHTML = `<p style="color:var(--c-danger);font-size:var(--text-sm)">${_esc(err.message || 'Fehler.')}</p>`;
|
||
}
|
||
}
|
||
|
||
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 `<span style="background:${cfg.color}1a;color:${cfg.color};border:1px solid ${cfg.color}30;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600">${cfg.label}: ${n}</span>`;
|
||
}).join('');
|
||
|
||
const header = `
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-3);flex-wrap:wrap;gap:var(--space-2)">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||
<span style="font-size:var(--text-sm);font-weight:700;color:var(--c-text)">${entries.length} Interessent${entries.length !== 1 ? 'en' : ''}</span>
|
||
${summaryPills}
|
||
</div>
|
||
</div>`;
|
||
|
||
if (!entries.length) {
|
||
container.innerHTML = `
|
||
<div style="text-align:center;padding:var(--space-6) var(--space-4);border:1px dashed var(--c-border);border-radius:var(--radius-md)">
|
||
<div style="font-size:2rem;margin-bottom:var(--space-2)">${UI.icon('users')}</div>
|
||
<p style="font-weight:600;font-size:var(--text-sm);color:var(--c-text);margin-bottom:var(--space-1)">Noch keine Interessenten</p>
|
||
<p class="text-xs-muted">Trage Anfragen ein — mit Wunsch-Geschlecht, Kontaktdaten und Status.</p>
|
||
</div>`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = header + `
|
||
<div class="flex-col-gap-2">
|
||
${entries.map((e, i) => `
|
||
<div style="background:var(--c-bg-secondary);border-radius:var(--radius-md);padding:var(--space-3) var(--space-3);display:flex;gap:var(--space-3);align-items:flex-start" data-entry-id="${e.id}">
|
||
<div style="background:var(--c-primary);color:white;border-radius:50%;width:1.6rem;height:1.6rem;display:flex;align-items:center;justify-content:center;font-size:var(--text-xs);font-weight:700;flex-shrink:0;margin-top:2px">${i + 1}</div>
|
||
<div class="flex-1-min">
|
||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-1)">
|
||
<span style="font-weight:600;font-size:var(--text-sm)">${_esc(e.name)}</span>
|
||
${_wlStatusBadge(e.status)}
|
||
${e.wunsch_geschlecht && e.wunsch_geschlecht !== 'egal' ? `<span class="text-xs-secondary">${e.wunsch_geschlecht === 'maennlich' ? '♂ Rüde' : '♀ Hündin'}</span>` : ''}
|
||
${e.wunsch_farbe ? `<span class="text-xs-secondary">${_esc(e.wunsch_farbe)}</span>` : ''}
|
||
</div>
|
||
<div style="display:flex;gap:var(--space-4);flex-wrap:wrap;font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||
${e.email ? `<span>${UI.icon('envelope')} ${_esc(e.email)}</span>` : ''}
|
||
${e.telefon ? `<span>${UI.icon('phone')} ${_esc(e.telefon)}</span>` : ''}
|
||
<span>${UI.icon('calendar-dots')} ${e.created_at ? e.created_at.slice(0, 10) : '—'}</span>
|
||
</div>
|
||
${e.nachricht ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);color:var(--c-text-secondary);font-style:italic">"${_esc(e.nachricht)}"</div>` : ''}
|
||
${e.notiz ? `<div style="margin-top:var(--space-1);font-size:var(--text-xs);background:var(--c-warning-bg,#fffbeb);color:#92400e;border-radius:4px;padding:2px 6px">${UI.icon('note-pencil')} ${_esc(e.notiz)}</div>` : ''}
|
||
</div>
|
||
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
|
||
<button class="btn btn-ghost btn-xs wl-edit-btn" data-entry-id="${e.id}" title="Bearbeiten">${UI.icon('pencil-simple')}</button>
|
||
<button class="btn btn-ghost btn-xs wl-delete-btn" data-entry-id="${e.id}" title="Entfernen" class="text-danger">${UI.icon('trash')}</button>
|
||
</div>
|
||
</div>`).join('')}
|
||
</div>`;
|
||
|
||
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: `
|
||
<form id="wl-form" class="flex-col-gap-3">
|
||
<div class="form-group">
|
||
<label class="form-label">Name *</label>
|
||
<input class="form-control" name="name" required value="${_esc(v.name || '')}">
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="form-group">
|
||
<label class="form-label">E-Mail</label>
|
||
<input class="form-control" type="email" name="email" value="${_esc(v.email || '')}">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Telefon</label>
|
||
<input class="form-control" name="telefon" value="${_esc(v.telefon || '')}">
|
||
</div>
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="form-group">
|
||
<label class="form-label">Wunsch Geschlecht</label>
|
||
<select class="form-control" name="wunsch_geschlecht">
|
||
<option value="egal" ${(!v.wunsch_geschlecht || v.wunsch_geschlecht === 'egal') ? 'selected' : ''}>Egal</option>
|
||
<option value="maennlich" ${v.wunsch_geschlecht === 'maennlich' ? 'selected' : ''}>Rüde ♂</option>
|
||
<option value="weiblich" ${v.wunsch_geschlecht === 'weiblich' ? 'selected' : ''}>Hündin ♀</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Wunsch Farbe</label>
|
||
<input class="form-control" name="wunsch_farbe" placeholder="z.B. schwarz-weiß" value="${_esc(v.wunsch_farbe || '')}">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Nachricht des Interessenten</label>
|
||
<textarea class="form-control" name="nachricht" rows="2" placeholder="Was hat der Interessent geschrieben?">${_esc(v.nachricht || '')}</textarea>
|
||
</div>
|
||
<div class="grid-2">
|
||
<div class="form-group">
|
||
<label class="form-label">Status</label>
|
||
<select class="form-control" name="status">
|
||
${Object.entries(_WL_STATUS).map(([k, s]) => `<option value="${k}" ${(v.status || 'anfrage') === k ? 'selected' : ''}>${s.label}</option>`).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Position</label>
|
||
<input class="form-control" type="number" name="prioritaet" min="0" value="${v.prioritaet ?? 0}">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Interne Notiz</label>
|
||
<input class="form-control" name="notiz" placeholder="Nur für dich sichtbar" value="${_esc(v.notiz || '')}">
|
||
</div>
|
||
</form>`,
|
||
footer: `
|
||
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
|
||
<button class="btn btn-primary" form="wl-form" type="submit">${isEdit ? 'Speichern' : 'Eintragen'}</button>`,
|
||
});
|
||
|
||
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 `<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" class="mb-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 rangOpts = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(l =>
|
||
`<option value="${l}" ${v.wurf_rang === l ? 'selected' : ''}>${l}-Wurf</option>`
|
||
).join('');
|
||
|
||
const body = `
|
||
<form id="litter-form" autocomplete="off">
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 2fr;gap:var(--space-3)">
|
||
<div class="form-group">
|
||
<label class="form-label">Wurf-Buchstabe</label>
|
||
<select class="form-control" name="wurf_rang">
|
||
<option value="">— kein —</option>
|
||
${rangOpts}
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Wurf-Name <span style="font-weight:normal;color:var(--c-text-muted)">(optional)</span></label>
|
||
<input class="form-control" type="text" name="wurf_name"
|
||
placeholder="z.B. Vatertags-Wurf, Frühlings-Wurf …"
|
||
value="${_esc(v.wurf_name || '')}">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<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);align-items:end">
|
||
<div class="form-group">
|
||
<label class="form-label">Erwarteter Geburtstermin <span style="font-weight:normal;color:var(--c-text-muted)">(geplant)</span></label>
|
||
<input class="form-control" type="date" name="erwartetes_datum"
|
||
value="${_esc(v.erwartetes_datum || '')}">
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Für geplante Würfe / laufende Trächtigkeit</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Geburtsdatum <span style="font-weight:normal;color:var(--c-text-muted)">(tatsächlich)</span></label>
|
||
<input class="form-control" type="date" name="geburt_datum"
|
||
value="${_esc(v.geburt_datum || '')}">
|
||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin:4px 0 0">Wenn die Welpen bereits geboren sind</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<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 class="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 class="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 class="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 = {
|
||
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 = `
|
||
<form id="puppy-form" autocomplete="off">
|
||
|
||
<div class="grid-2">
|
||
<div class="form-group">
|
||
<label class="form-label">Name <span class="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 class="grid-2">
|
||
<div class="form-group">
|
||
<label class="form-label">Chip-Nr.</label>
|
||
<input class="form-control" type="text" name="chip_nr"
|
||
value="${_esc(v.chip_nr || '')}" placeholder="15-stellig">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">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 class="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 class="text-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 class="text-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 class="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 class="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}" class="mb-4">
|
||
<p class="text-sm-muted">Lädt…</p>
|
||
</div>
|
||
<hr style="margin:var(--space-3) 0;border:none;border-top:1px solid var(--c-border)">
|
||
<form id="${uploadFormId}" class="flex-col-gap-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 class="text-sm-muted">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 class="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 class="text-sm-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 class="w-full">
|
||
${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 class="text-danger">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
|
||
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
|
||
});
|
||
return;
|
||
}
|
||
|
||
UI.modal.open({
|
||
title: `${UI.icon('sparkle')} KI-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 };
|
||
|
||
})();
|