Feature: Ban Yaro Wrapped + Jahrestags- und Monatsrückblick (SW by-v699)
- GET /api/dogs/{id}/wrapped?year= aggregiert km, Gassi-Tage, Fotos,
Lieblingsmonat/-aktivität, Training, Gesundheit, Wetter-Stats aus SQLite
- Frontend: Wrapped-Fullscreen-Modal in dog-profile.js — 5 Cards mit
Swipe/Klick-Navigation, Dots, ESC-Taste, Copy-to-Clipboard auf Share-Card
- Scheduler: _job_anniversary_reminders (täglich 09:00) sendet Push wenn
heute ein Tagebucheintrag von vor 1+ Jahren existiert
- Scheduler: _job_monthly_recap (1. des Monats 10:00) sendet Vormonat-
Zusammenfassung (km, Einträge, Training) per Push an alle User
- Beide Jobs im Status-Report-Log und Scheduler-Start-Log vermerkt
- SW by-v699, APP_VER 699
This commit is contained in:
parent
0fdc32eaf4
commit
20a4936397
3 changed files with 464 additions and 4 deletions
|
|
@ -1953,6 +1953,163 @@ window.Page_dog_profile = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// JAHRESRÜCKBLICK — WRAPPED
|
||||
// ----------------------------------------------------------
|
||||
async function _showWrappedModal(dog) {
|
||||
const year = new Date().getFullYear();
|
||||
let data = null;
|
||||
try {
|
||||
data = await API.get(`/dogs/${dog.id}/wrapped?year=${year}`);
|
||||
} catch (e) {
|
||||
UI.toast.error('Rückblick konnte nicht geladen werden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const name = _esc(data.dog_name);
|
||||
const km = data.gesamt_km || 0;
|
||||
const konfetti = km > 100;
|
||||
|
||||
const _TYPEN = {
|
||||
eintrag: 'Tagebuch', gassi: 'Gassi', training: 'Training',
|
||||
tierarzt: 'Tierarzt', freizeit: 'Freizeit', milestone: 'Meilenstein',
|
||||
};
|
||||
const aktivitaet = data.lieblings_aktivitaet
|
||||
? (_TYPEN[data.lieblings_aktivitaet] || data.lieblings_aktivitaet)
|
||||
: null;
|
||||
|
||||
const stadtpark = km > 0 ? Math.round(km / 1.5) : 0;
|
||||
const schneeheld = data.wetter_kalt >= 10;
|
||||
const pfotalalarm = data.wetter_warm >= 10;
|
||||
|
||||
const _card = (content) =>
|
||||
`<div style="min-height:320px;display:flex;flex-direction:column;
|
||||
align-items:center;justify-content:center;text-align:center;
|
||||
padding:32px 24px;gap:16px">${content}</div>`;
|
||||
|
||||
const cards = [
|
||||
_card(`
|
||||
<div style="font-size:3rem">🐾</div>
|
||||
<div style="font-size:1.6rem;font-weight:800;color:#e8c96e;line-height:1.2">
|
||||
Dein Jahr mit ${name}
|
||||
</div>
|
||||
<div style="font-size:1rem;color:#b8b0a0;font-weight:500">${year} in Zahlen</div>
|
||||
`),
|
||||
_card(`
|
||||
<div style="font-size:2.5rem">👟</div>
|
||||
<div style="font-size:3rem;font-weight:900;color:#e8c96e">${km} km</div>
|
||||
<div style="font-size:1rem;color:#d0c8b8;font-weight:600">zusammen gelaufen</div>
|
||||
${stadtpark > 0 ? `<div style="font-size:0.85rem;color:#888;margin-top:4px">= ${stadtpark}× um den Stadtpark</div>` : ''}
|
||||
${konfetti ? `<div style="font-size:1.5rem;margin-top:8px">🎉 Über 100 km!</div>` : ''}
|
||||
`),
|
||||
_card(`
|
||||
<div style="font-size:2.5rem">📔</div>
|
||||
<div style="font-size:3rem;font-weight:900;color:#e8c96e">${data.eintraege_gesamt}</div>
|
||||
<div style="font-size:1rem;color:#d0c8b8;font-weight:600">Tagebucheinträge</div>
|
||||
${data.fotos_gesamt > 0 ? `<div style="font-size:1.1rem;color:#a0c890;font-weight:700;margin-top:4px">📷 ${data.fotos_gesamt} Fotos</div>` : ''}
|
||||
${data.gassi_tage > 0 ? `<div style="font-size:0.9rem;color:#888;margin-top:4px">🐾 ${data.gassi_tage} aktive Tage</div>` : ''}
|
||||
${data.lieblings_monat ? `<div style="font-size:0.85rem;color:#b89a6a;margin-top:4px">Meiste Einträge: ${_esc(data.lieblings_monat)}</div>` : ''}
|
||||
${aktivitaet ? `<div style="font-size:0.85rem;color:#888">Lieblingsaktivität: ${_esc(aktivitaet)}</div>` : ''}
|
||||
`),
|
||||
_card(`
|
||||
<div style="font-size:2rem">🌡️</div>
|
||||
<div style="font-size:1.2rem;font-weight:700;color:#d0c8b8;margin-bottom:8px">Wetter-Tapferkeit</div>
|
||||
<div style="display:flex;gap:32px;justify-content:center;flex-wrap:wrap">
|
||||
<div><div style="font-size:2rem">❄️</div>
|
||||
<div style="font-size:2rem;font-weight:900;color:#8ecfef">${data.wetter_kalt}</div>
|
||||
<div style="font-size:0.8rem;color:#888">kalte Tage</div></div>
|
||||
<div><div style="font-size:2rem">☀️</div>
|
||||
<div style="font-size:2rem;font-weight:900;color:#f0b040">${data.wetter_warm}</div>
|
||||
<div style="font-size:0.8rem;color:#888">heiße Tage</div></div>
|
||||
</div>
|
||||
${schneeheld ? `<div style="margin-top:12px;background:#1a3a5c;border-radius:8px;padding:6px 16px;font-size:0.9rem;font-weight:700;color:#8ecfef">❄️ Schneeheld!</div>` : ''}
|
||||
${pfotalalarm ? `<div style="margin-top:12px;background:#3a2000;border-radius:8px;padding:6px 16px;font-size:0.9rem;font-weight:700;color:#f0b040">🔥 Pfoten-Alarm!</div>` : ''}
|
||||
${data.training_sessions > 0 ? `<div style="margin-top:12px;font-size:0.85rem;color:#a0c890">🏋️ ${data.training_sessions} Training-Sessions</div>` : ''}
|
||||
`),
|
||||
_card(`
|
||||
<div style="font-size:2.5rem">🐾</div>
|
||||
<div style="font-size:1.3rem;font-weight:800;color:#e8c96e">Was für ein Jahr!</div>
|
||||
<div style="font-size:0.95rem;color:#b8b0a0;line-height:1.5;max-width:280px">
|
||||
${name} und du — ein unschlagbares Team.<br>${year} war unvergesslich.
|
||||
</div>
|
||||
<button id="dp-wrapped-copy-btn" style="
|
||||
margin-top:12px;background:#e8c96e;color:#1a1a2e;font-weight:800;
|
||||
border:none;border-radius:8px;padding:10px 20px;cursor:pointer;font-size:1rem">
|
||||
📋 Text kopieren
|
||||
</button>
|
||||
`),
|
||||
];
|
||||
|
||||
let currentCard = 0;
|
||||
const totalCards = cards.length;
|
||||
|
||||
const renderDots = () => Array.from({ length: totalCards }, (_, i) =>
|
||||
`<div style="width:8px;height:8px;border-radius:50%;background:${i === currentCard ? '#e8c96e' : '#444'};transition:background .3s"></div>`
|
||||
).join('');
|
||||
|
||||
const modalEl = document.createElement('div');
|
||||
modalEl.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0d0d1a;display:flex;flex-direction:column;overflow:hidden;';
|
||||
modalEl.innerHTML = `
|
||||
<div style="display:flex;justify-content:flex-end;padding:16px 20px 0">
|
||||
<button id="dp-wrapped-close" style="background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:36px;height:36px;font-size:1.2rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button>
|
||||
</div>
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative">
|
||||
<div id="dp-wrapped-card-container" style="width:100%;max-width:400px;color:#fff;">${cards[0]}</div>
|
||||
<button id="dp-wrapped-prev" style="position:absolute;left:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:40px;height:40px;font-size:1.3rem;color:#fff;cursor:pointer;display:none;align-items:center;justify-content:center">‹</button>
|
||||
<button id="dp-wrapped-next" style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:40px;height:40px;font-size:1.3rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">›</button>
|
||||
</div>
|
||||
<div id="dp-wrapped-dots" style="display:flex;gap:8px;justify-content:center;padding:16px 0 32px">${renderDots()}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modalEl);
|
||||
|
||||
const cardContainer = modalEl.querySelector('#dp-wrapped-card-container');
|
||||
const dotsEl = modalEl.querySelector('#dp-wrapped-dots');
|
||||
const prevBtn = modalEl.querySelector('#dp-wrapped-prev');
|
||||
const nextBtn = modalEl.querySelector('#dp-wrapped-next');
|
||||
|
||||
const updateCard = () => {
|
||||
cardContainer.innerHTML = cards[currentCard];
|
||||
dotsEl.innerHTML = renderDots();
|
||||
prevBtn.style.display = currentCard > 0 ? 'flex' : 'none';
|
||||
nextBtn.style.display = currentCard < totalCards - 1 ? 'flex' : 'none';
|
||||
if (currentCard === totalCards - 1) {
|
||||
cardContainer.querySelector('#dp-wrapped-copy-btn')?.addEventListener('click', async () => {
|
||||
const shareText = `🐾 ${name} & ich — Jahresrückblick ${year}\n`
|
||||
+ (km > 0 ? `👟 ${km} km gelaufen\n` : '')
|
||||
+ (data.eintraege_gesamt > 0 ? `📔 ${data.eintraege_gesamt} Tagebucheinträge\n` : '')
|
||||
+ (data.fotos_gesamt > 0 ? `📷 ${data.fotos_gesamt} Fotos\n` : '')
|
||||
+ (data.training_sessions > 0 ? `🏋️ ${data.training_sessions} Training-Sessions\n` : '')
|
||||
+ `\nbanyaro.app`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareText);
|
||||
UI.toast.success('Text kopiert!');
|
||||
} catch {
|
||||
UI.toast.error('Kopieren fehlgeschlagen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
prevBtn.addEventListener('click', () => { if (currentCard > 0) { currentCard--; updateCard(); } });
|
||||
nextBtn.addEventListener('click', () => { if (currentCard < totalCards - 1) { currentCard++; updateCard(); } });
|
||||
modalEl.querySelector('#dp-wrapped-close').addEventListener('click', () => modalEl.remove());
|
||||
|
||||
let touchStartX = 0;
|
||||
modalEl.addEventListener('touchstart', e => { touchStartX = e.touches[0].clientX; }, { passive: true });
|
||||
modalEl.addEventListener('touchend', e => {
|
||||
const dx = e.changedTouches[0].clientX - touchStartX;
|
||||
if (Math.abs(dx) > 50) {
|
||||
if (dx < 0 && currentCard < totalCards - 1) { currentCard++; updateCard(); }
|
||||
if (dx > 0 && currentCard > 0) { currentCard--; updateCard(); }
|
||||
}
|
||||
});
|
||||
|
||||
const onKey = e => { if (e.key === 'Escape') { modalEl.remove(); document.removeEventListener('keydown', onKey); } };
|
||||
document.addEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue