Feature: Tierschutz-Check, KI-Züchter-Features, Export, SEO-Update
Tierschutz-System (immer aktiv, nicht abschaltbar): - welfare_check.py: regelbasierte Prüfung IK, Alter, Deckpause, Wurfanzahl, Genetik - Grün/Gelb/Rot-Modal bei Wurf anlegen + Probeverpaarung - Bei kritischem Befund + "Trotzdem fortfahren" → automatische Admin-Mail - Tierschutz-Check nie durch Nutzer deaktivierbar KI-Züchter-Features (pro User an/abschaltbar außer Tierschutz): - routes/zucht_ki.py: 5 Endpunkte — Wurfankündigung, Genetik-Erklärung, Paarungsanalyse, Hund-Beschreibung, Jahresbericht - Toggles in Einstellungen (ki_zucht_* Felder) - KI-Buttons in litters.js + zuchthunde.js KI-Routing: Privilegierte Rollen (Admin, Züchter, Moderator, Manager) nutzen Claude Sonnet primär, lokales LLM als Fallback Datenexport: routes/breeder_export.py — ZIP mit HTML-Dossier + ODS (odfpy hinzugefügt in requirements.txt) Admin-Profil: POST /admin/breeder/create-profile für Schnellprofil ohne Antragsprozess; Admin-Rolle bleibt erhalten Wurfformular: Dropdown aus Zuchtkartei für Vater/Mutter mit Auto-Fill; litters.vater_id + mutter_id als FK auf zucht_hunde Probeverpaarung: heart-fill Icon + Welfare-Block im Ergebnis Landing Page: Züchter-Section + Feature-Gruppe, Meta-Tags, JSON-LD, keywords, softwareVersion 2.1 SEO: llms.txt vollständig überarbeitet, robots.txt Züchter-Pfade, sitemap.xml um Wurfbörse + Züchter-Profile erweitert SW by-v474, APP_VER 451
This commit is contained in:
parent
91340be5a3
commit
c8ae514c01
20 changed files with 2129 additions and 200 deletions
|
|
@ -192,6 +192,13 @@ window.Page_litters = (() => {
|
|||
});
|
||||
});
|
||||
|
||||
el.querySelectorAll('.litters-ki-announce-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = parseInt(btn.dataset.id);
|
||||
_showKiAnnouncement(id);
|
||||
});
|
||||
});
|
||||
|
||||
el.querySelectorAll('.litters-add-puppy-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const id = parseInt(btn.dataset.id);
|
||||
|
|
@ -249,6 +256,11 @@ window.Page_litters = (() => {
|
|||
title="Elterntier-Fotos verwalten">
|
||||
${UI.icon('users')} Eltern
|
||||
</button>
|
||||
${_appState.user?.ki_zucht_wurfankuendigung !== 0 ? `
|
||||
<button class="btn btn-ghost btn-sm litters-ki-announce-btn" data-id="${l.id}"
|
||||
title="KI: Wurfankündigung schreiben">
|
||||
${UI.icon('sparkle')} Ankündigung
|
||||
</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm litters-edit-btn" data-id="${l.id}"
|
||||
title="Bearbeiten">
|
||||
${UI.icon('pencil-simple')}
|
||||
|
|
@ -477,24 +489,42 @@ window.Page_litters = (() => {
|
|||
// ----------------------------------------------------------
|
||||
// Wurf-Formular (neu / bearbeiten)
|
||||
// ----------------------------------------------------------
|
||||
function _showLitterForm(litter) {
|
||||
async function _showLitterForm(litter) {
|
||||
const isEdit = !!litter;
|
||||
const v = litter || {};
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// Zuchtkartei laden für Elterntier-Auswahl
|
||||
let zuchthunde = [];
|
||||
try { zuchthunde = await API.zuchthunde.list(); } catch {}
|
||||
const maennlich = zuchthunde.filter(h => h.geschlecht !== 'weiblich');
|
||||
const weiblich = zuchthunde.filter(h => h.geschlecht !== 'maennlich');
|
||||
|
||||
const buildSelect = (name, idName, list, currentId, currentName, placeholder) => {
|
||||
const opts = list.map(h => {
|
||||
const label = h.name + (h.rufname ? ` (${h.rufname})` : '') + (h.zuchtbuchnummer ? ` · ${h.zuchtbuchnummer}` : '');
|
||||
return `<option value="${h.id}" data-name="${_esc(h.name)}" ${currentId == h.id ? 'selected' : ''}>${_esc(label)}</option>`;
|
||||
}).join('');
|
||||
return `
|
||||
<select class="form-control" name="${idName}" id="${idName}-sel" style="margin-bottom:var(--space-2)">
|
||||
<option value="">— ${placeholder} —</option>
|
||||
${opts}
|
||||
</select>
|
||||
<input class="form-control" type="text" name="${name}" id="${name}-txt"
|
||||
value="${_esc(currentName || '')}" placeholder="oder Namen frei eingeben">`;
|
||||
};
|
||||
|
||||
const body = `
|
||||
<form id="litter-form" autocomplete="off">
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Vatername</label>
|
||||
<input class="form-control" type="text" name="vater_name"
|
||||
value="${_esc(v.vater_name || '')}" placeholder="Name des Vaters">
|
||||
<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">Muttername</label>
|
||||
<input class="form-control" type="text" name="mutter_name"
|
||||
value="${_esc(v.mutter_name || '')}" placeholder="Name der Mutter">
|
||||
<label class="form-label">Mutter</label>
|
||||
${buildSelect('mutter_name', 'mutter_id', weiblich, v.mutter_id, v.mutter_name, 'Aus Zuchtkartei')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -583,6 +613,16 @@ window.Page_litters = (() => {
|
|||
|
||||
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');
|
||||
|
|
@ -591,6 +631,8 @@ window.Page_litters = (() => {
|
|||
const payload = {
|
||||
vater_name: fd.get('vater_name')?.trim() || null,
|
||||
mutter_name: fd.get('mutter_name')?.trim() || null,
|
||||
vater_id: fd.get('vater_id') ? parseInt(fd.get('vater_id')) : null,
|
||||
mutter_id: fd.get('mutter_id') ? parseInt(fd.get('mutter_id')) : null,
|
||||
geburt_datum: fd.get('geburt_datum') || null,
|
||||
erwartetes_datum: fd.get('erwartetes_datum') || null,
|
||||
welpen_gesamt: fd.get('welpen_gesamt') ? parseInt(fd.get('welpen_gesamt')) : null,
|
||||
|
|
@ -608,14 +650,16 @@ window.Page_litters = (() => {
|
|||
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.toast.success('Wurf angelegt.');
|
||||
UI.modal.close();
|
||||
_renderList();
|
||||
_showWelfareModal(created.welfare, created.id);
|
||||
}
|
||||
UI.modal.close();
|
||||
_renderList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -967,6 +1011,133 @@ window.Page_litters = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Tierschutz-Check Modal
|
||||
// ----------------------------------------------------------
|
||||
function _showWelfareModal(welfare, litterId) {
|
||||
if (!welfare) return;
|
||||
|
||||
const color = { ok: '#16a34a', info: '#3b82f6', warning: '#f59e0b', critical: '#dc2626' }[welfare.level] || '#6b7280';
|
||||
const title = { ok: 'Alles prima', info: 'Hinweis', warning: 'Bitte beachten', critical: 'Kritischer Hinweis' }[welfare.level];
|
||||
const icon = { ok: 'check-circle', info: 'info', warning: 'warning', critical: 'warning-circle' }[welfare.level];
|
||||
|
||||
const issueHTML = (welfare.issues || []).map(i => `
|
||||
<div style="display:flex;gap:8px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)">
|
||||
<span style="color:${color};flex-shrink:0">${UI.icon('warning')}</span>
|
||||
<span style="font-size:var(--text-sm)">${_esc(i.text)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const okHTML = (welfare.ok_points || []).map(p => `
|
||||
<div style="display:flex;gap:8px;padding:4px 0">
|
||||
<span style="color:#16a34a;flex-shrink:0">${UI.icon('check')}</span>
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">${_esc(p)}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const isProblematic = welfare.level === 'warning' || welfare.level === 'critical';
|
||||
|
||||
UI.modal.open({
|
||||
title: `<span style="color:${color}">${UI.icon(icon)} Tierschutz-Check: ${title}</span>`,
|
||||
body: `
|
||||
<div style="background:${color}18;border:1.5px solid ${color}40;border-radius:var(--radius-md);
|
||||
padding:var(--space-4);margin-bottom:var(--space-4)">
|
||||
${issueHTML || ''}
|
||||
${okHTML}
|
||||
</div>
|
||||
${welfare.level === 'critical' ? `
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-sm);
|
||||
padding:var(--space-3)">
|
||||
${UI.icon('info')} Wenn du fortfährst, wird der Administrator informiert.
|
||||
</div>` : ''}
|
||||
`,
|
||||
footer: isProblematic ? `
|
||||
<div style="display:flex;gap:var(--space-2);width:100%">
|
||||
<button class="btn btn-secondary flex-1" id="welfare-back-btn">
|
||||
${UI.icon('arrow-left')} Zurück
|
||||
</button>
|
||||
<button class="btn btn-ghost flex-1" id="welfare-confirm-btn"
|
||||
style="color:${color}">
|
||||
Trotzdem fortfahren
|
||||
</button>
|
||||
</div>` : `
|
||||
<button class="btn btn-primary" data-modal-close style="width:100%">
|
||||
${UI.icon('check')} Verstanden
|
||||
</button>`,
|
||||
});
|
||||
|
||||
document.getElementById('welfare-back-btn')?.addEventListener('click', () => {
|
||||
UI.modal.close?.();
|
||||
const litter = _litters.find(l => l.id === litterId);
|
||||
API.litters.remove(litterId).catch(() => {});
|
||||
_litters = _litters.filter(l => l.id !== litterId);
|
||||
_renderList();
|
||||
setTimeout(() => _showLitterForm(null), 150);
|
||||
});
|
||||
|
||||
document.getElementById('welfare-confirm-btn')?.addEventListener('click', async () => {
|
||||
await API.litters.welfareConfirm(litterId).catch(() => {});
|
||||
UI.modal.close?.();
|
||||
UI.toast.info('Wurf gespeichert.');
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI: Wurfankündigung
|
||||
// ----------------------------------------------------------
|
||||
async function _showKiAnnouncement(litterId) {
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
|
||||
body: `<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-6)">
|
||||
KI schreibt Wurfankündigung…
|
||||
</p>`,
|
||||
footer: '',
|
||||
});
|
||||
|
||||
let text = '';
|
||||
try {
|
||||
const result = await API.zuchtKi.wurfankuendigung(litterId);
|
||||
text = result.text || result.content || result.ankuendigung || JSON.stringify(result);
|
||||
} catch (err) {
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
|
||||
body: `<p style="color:var(--c-danger)">${_esc(err.message || 'Fehler beim Generieren.')}</p>`,
|
||||
footer: `<button class="btn btn-secondary" data-modal-close>Schließen</button>`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
UI.modal.open({
|
||||
title: `${UI.icon('sparkle')} KI-Wurfankündigung`,
|
||||
body: `<div style="white-space:pre-wrap;font-size:var(--text-sm);line-height:1.6">${_esc(text)}</div>`,
|
||||
footer: `
|
||||
<button class="btn btn-secondary flex-1" id="ki-announce-copy">
|
||||
${UI.icon('clipboard-text')} Kopieren
|
||||
</button>
|
||||
<button class="btn btn-primary flex-1" id="ki-announce-use">
|
||||
${UI.icon('check')} In Beschreibung übernehmen
|
||||
</button>`,
|
||||
});
|
||||
|
||||
document.getElementById('ki-announce-copy')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
UI.toast.success('Text kopiert.');
|
||||
} catch {
|
||||
UI.toast.error('Kopieren nicht möglich.');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('ki-announce-use')?.addEventListener('click', async () => {
|
||||
const btn = document.getElementById('ki-announce-use');
|
||||
await UI.asyncButton(btn, async () => {
|
||||
await API.litters.update(litterId, { beschreibung: text });
|
||||
UI.modal.close();
|
||||
UI.toast.success('Beschreibung aktualisiert.');
|
||||
await _loadLitters();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue