Pflege-System: Pflegetipps im Hundeprofil + Rassen-Autocomplete

- GET /api/dogs/{id}/pflege: rassenspezifische Pflegetipps
- pflege_tipps DB-Tabelle (43 Tipps, 10 Kategorien) geseedet
- dogs.rasse_id für Wiki-Verknüpfung (Migration)
- Hundeprofil: Tipp des Tages + alle Tipps aufklappbar
- Hundeprofil-Edit: Rassen-Autocomplete mit Wiki-Match-Badge
- Social: Post-Bestätigung (Gepostet!-Button, Quick-Mark, Pending-Banner)
- Social: Pflegetipp-Button (allg. + rassenspezifisch)
- Social: Diversitäts-Check, Kategorie-Tagging
- Social: 104 Übungen, Übungsübersicht-Modal
- Admin: Social-Media-Tracking-Sektion
- SW by-v356, APP_VER 343
This commit is contained in:
rene 2026-04-24 20:56:47 +02:00
parent 75615140c4
commit ba5547f993
7 changed files with 797 additions and 9 deletions

View file

@ -251,6 +251,12 @@ window.Page_social = (() => {
🎾 Trainingstipp generieren
<span style="font-size:10px;opacity:.7;margin-left:6px">104 Übungen</span>
</button>
<button id="sm-pflege-tip" class="btn btn-secondary"
style="width:100%;min-height:44px;font-size:var(--text-sm);
margin-bottom:4px;border:1.5px solid #a78bfa;color:#a78bfa">
🛁 Pflegetipp generieren
<span style="font-size:10px;opacity:.7;margin-left:6px">allg. oder für gewählte Rasse</span>
</button>
<button id="sm-show-exercises" class="btn btn-secondary"
style="width:100%;min-height:36px;font-size:11px;
margin-bottom:8px;color:var(--c-text-muted)">
@ -514,6 +520,43 @@ window.Page_social = (() => {
}
});
// Pflegetipp
el.querySelector('#sm-pflege-tip').addEventListener('click', async () => {
const btn = el.querySelector('#sm-pflege-tip');
const res = el.querySelector('#sm-gen-result');
btn.disabled = true;
res.innerHTML = _lunaProgressHtml();
const interval = _startProgress(res);
const breedId = parseInt(el.querySelector('#sm-breed-id')?.value) || null;
try {
const url = breedId ? `/social/pflege-tipp?breed_id=${breedId}` : '/social/pflege-tipp';
const data = await API.post(url, {});
clearInterval(interval.bar); clearInterval(interval.msg);
_progressDone(res);
await new Promise(r => setTimeout(r, 400));
res.innerHTML = `
<div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
margin-bottom:10px;display:flex;gap:10px;align-items:center">
<span style="font-size:2em;flex-shrink:0">🛁</span>
<div>
<div style="font-size:11px;color:var(--c-text-muted)">
Pflegetipp · ${_esc(data.pflege_kat||'')}
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div>
<div style="font-weight:700;font-size:var(--text-base)">
${_esc(data.pflege_titel||'')}</div>
</div>
</div>
${_renderResult(data, null)}`;
_bindResultEvents(res);
Promise.all([API.get('/social/stats'), API.get('/social/diversity')])
.then(([s,d]) => { _stats = s; _diversity = d; });
} catch(e) {
clearInterval(interval.bar); clearInterval(interval.msg);
res.innerHTML = `<div style="color:var(--c-danger);padding:var(--space-3)">
😬 ${_esc(e.message||String(e))}</div>`;
} finally { btn.disabled = false; }
});
// Rasse des Tages
el.querySelector('#sm-breed-day').addEventListener('click', async () => {
const btn = el.querySelector('#sm-breed-day');
@ -628,7 +671,7 @@ window.Page_social = (() => {
function _startProgress(container) {
let elapsed = 0, msgIdx = 0;
const totalMs = 14000;
const totalMs = 2800;
// Fortschrittsbalken: alle 250ms
const barInterval = setInterval(() => {
@ -694,14 +737,48 @@ window.Page_social = (() => {
</div>
</div>` : ''}
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
<span style="background:#f0fdf4;color:var(--c-success);border-radius:8px;
padding:4px 10px;font-size:11px;font-weight:600"> Gespeichert</span>
${score ? `<span>${score}</span>` : ''}
<button class="btn btn-sm btn-secondary sm-preview-btn"
data-id="${data.id}"
style="margin-left:auto;font-size:11px;padding:4px 10px;min-height:30px">
style="font-size:11px;padding:4px 10px;min-height:30px">
👁 Vorschau</button>
<button class="btn btn-sm btn-primary sm-posted-btn"
data-id="${data.id}"
style="margin-left:auto;font-size:11px;padding:4px 12px;min-height:30px;
background:#10b981;border-color:#10b981">
📤 Habe ich gepostet!
</button>
</div>
<div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2);
border-radius:10px;padding:12px;margin-bottom:12px">
<div style="font-size:12px;font-weight:600;margin-bottom:8px">
🎉 Super! Kurze Angaben zum Post:</div>
<div style="display:grid;gap:8px">
<div>
<div class="sm-label">Datum (leer = heute)</div>
<input type="date" class="sm-post-date" data-id="${data.id}"
style="width:100%;background:var(--c-surface);color:var(--c-text);
border:1.5px solid var(--c-border);border-radius:8px;
padding:8px 12px;font-size:var(--text-sm);font-family:inherit;
box-sizing:border-box">
</div>
<div>
<div class="sm-label">Post-URL (optional)</div>
<input type="url" class="sm-post-url" data-id="${data.id}"
placeholder="https://www.instagram.com/p/..."
style="width:100%;background:var(--c-surface);color:var(--c-text);
border:1.5px solid var(--c-border);border-radius:8px;
padding:8px 12px;font-size:var(--text-sm);font-family:inherit;
box-sizing:border-box">
</div>
<button class="btn btn-primary sm-confirm-posted" data-id="${data.id}"
style="min-height:40px">
Bestätigen
</button>
</div>
</div>
${mediaUrl ? `
@ -791,6 +868,47 @@ window.Page_social = (() => {
if (item) _showPreview(item);
});
});
// "Habe ich gepostet!" — Formular einblenden
el.querySelectorAll('.sm-posted-btn').forEach(btn => {
btn.addEventListener('click', () => {
const form = el.querySelector(`#sm-posted-form-${btn.dataset.id}`);
if (form) {
form.style.display = form.style.display === 'none' ? '' : 'none';
// Heute als Default-Datum
const dateInput = form.querySelector('.sm-post-date');
if (dateInput && !dateInput.value) {
dateInput.value = new Date().toISOString().slice(0,10);
}
}
});
});
// Bestätigen
el.querySelectorAll('.sm-confirm-posted').forEach(btn => {
btn.addEventListener('click', async () => {
const id = parseInt(btn.dataset.id);
const form = el.querySelector(`#sm-posted-form-${id}`);
const date = form?.querySelector('.sm-post-date')?.value
|| new Date().toISOString().slice(0,16);
const url = form?.querySelector('.sm-post-url')?.value || null;
btn.disabled = true;
btn.textContent = '…';
await API.patch(`/social/content/${id}`, {
status: 'published',
published_at: date,
post_url: url || undefined,
});
// Form durch Bestätigung ersetzen
if (form) form.innerHTML = `
<div style="text-align:center;padding:8px;color:var(--c-success);
font-weight:600;font-size:var(--text-sm)">
🎉 Super! Post als veröffentlicht markiert.
${url ? `<br><a href="${_esc(url)}" target="_blank" rel="noopener"
style="font-size:11px;color:var(--c-primary)">Post ansehen </a>` : ''}
</div>`;
// Stats aktualisieren
API.get('/social/stats').then(s => { _stats = s; });
});
});
}
// ---------------------------------------------------------------
@ -883,12 +1001,28 @@ window.Page_social = (() => {
async function load(f) {
filter = f;
const url = f==='alle' ? '/social/content' : `/social/content?status=${f}`;
const items = await API.get(url).catch(() => []);
render(items);
const [items, allItems] = await Promise.all([
API.get(url).catch(() => []),
f !== 'alle' ? API.get('/social/content').catch(() => []) : Promise.resolve(null),
]);
const pending = (allItems || items).filter(c =>
c.status === 'idea' || c.status === 'draft').length;
render(items, pending);
}
function render(items) {
function render(items, pending) {
el.innerHTML = `
${pending > 0 ? `
<div style="background:var(--c-surface-2);border:1.5px solid var(--c-warning);
border-radius:10px;padding:10px 12px;margin-bottom:12px;
display:flex;align-items:center;gap:10px;font-size:var(--text-sm)">
<span style="font-size:1.3em;flex-shrink:0"></span>
<div>
<div style="font-weight:600;color:var(--c-warning)">${pending} Post${pending>1?'s':''} warten auf Bestätigung</div>
<div style="font-size:11px;color:var(--c-text-secondary)">
Tippe auf 📤 wenn du einen Post abgesetzt hast so lernt Luna was wirklich live ging.</div>
</div>
</div>` : ''}
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-3)">
${['alle','idea','draft','scheduled','published','archived'].map(s => `
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
@ -916,6 +1050,13 @@ window.Page_social = (() => {
font-style:italic">🎣 ${_esc(c.hook)}</div>` : ''}
</div>
<div style="display:flex;flex-direction:column;gap:4px;flex-shrink:0">
${c.status !== 'published' ? `
<button class="btn btn-sm sm-quick-post" data-id="${c.id}"
style="padding:3px 8px;font-size:11px;min-height:28px;
background:#10b981;border:1px solid #10b981;
color:#fff;border-radius:6px;cursor:pointer">
📤</button>` : `
<div style="text-align:center;font-size:18px;padding:3px 8px"></div>`}
<button class="btn btn-sm btn-secondary sm-exp"
data-id="${c.id}" style="padding:3px 8px;font-size:11px;min-height:28px">
Details</button>
@ -953,6 +1094,15 @@ window.Page_social = (() => {
</div>`).join('')}`;
el.querySelectorAll('[data-f]').forEach(b => b.addEventListener('click', () => load(b.dataset.f)));
el.querySelectorAll('.sm-quick-post').forEach(b => b.addEventListener('click', async () => {
const url = prompt('Post-URL (optional, leer lassen wenn keine):', '') ?? null;
await API.patch(`/social/content/${b.dataset.id}`, {
status: 'published',
published_at: new Date().toISOString().slice(0,16),
post_url: url || undefined,
});
load(filter);
}));
el.querySelectorAll('.sm-exp').forEach(b => b.addEventListener('click', () => {
const d = el.querySelector(`#sm-d-${b.dataset.id}`);
if (d) { d.style.display = d.style.display==='none'?'':'none'; }