Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)

Welten (worlds.js):
- Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig)
- Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte
- Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto
- worlds-back: navigiert zu Welcome wenn kein User eingeloggt
- Nach Logout: worlds-back Button sofort ausgeblendet

Wetter (wetter.js):
- Standort-Fehlerseite zu Motivations-Seite umgebaut
- Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde
- CTA: Standort freigeben + Registrieren (nur für Gäste)

Settings (settings.js):
- Logo in Auth-Form: display:block + margin:0 auto zentriert
- Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert)

Jobs (jobs.js):
- 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr
- Kein doppeltes Padding im Wrapper

Backend:
- weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN)
- Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee
- Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen
- Presse-Seite erweitert
- Ban Yaro Foto-Assets (WebP + HIRES JPG)
This commit is contained in:
rene 2026-05-05 17:32:03 +02:00
parent aa4849d947
commit 55069d246b
28 changed files with 719 additions and 198 deletions

View file

@ -5,15 +5,26 @@
window.Page_expenses = (() => {
let _container = null;
let _appState = null;
let _tab = 'uebersicht';
let _container = null;
let _appState = null;
let _tab = 'uebersicht';
let _selectedDogId = null;
// Cache
let _summary = null;
let _entries = [];
let _statsData = null;
function _dogParam() {
return _selectedDogId ? `?dog_id=${_selectedDogId}` : '';
}
function _dogParamAnd() {
return _selectedDogId ? `&dog_id=${_selectedDogId}` : '';
}
function _clearCache() {
_summary = null; _entries = []; _statsData = null;
}
const TABS = [
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
{ id: 'eintraege', label: 'Ausgaben', icon: 'currency-eur' },
@ -38,11 +49,10 @@ window.Page_expenses = (() => {
// LIFECYCLE
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_summary = null;
_entries = [];
_statsData = null;
_container = container;
_appState = appState;
_selectedDogId = null;
_clearCache();
_render();
}
@ -56,6 +66,16 @@ window.Page_expenses = (() => {
// ----------------------------------------------------------
// SHELL
// ----------------------------------------------------------
function _dogSelectorHtml() {
const dogs = _appState?.dogs || [];
if (dogs.length < 2) return '';
const pills = [{ id: null, name: 'Alle' }, ...dogs].map(d => `
<button class="exp-dog-pill${_selectedDogId === d.id ? ' active' : ''}" data-dog="${d.id ?? ''}">
${d.id ? UI.icon('paw-print') : ''} ${_esc(d.name)}
</button>`).join('');
return `<div class="exp-dog-selector" id="exp-dog-selector">${pills}</div>`;
}
function _render() {
_container.innerHTML = `
<div class="by-tabs exp-tabs" id="exp-tabs">
@ -65,6 +85,7 @@ window.Page_expenses = (() => {
</button>
`).join('')}
</div>
${_dogSelectorHtml()}
<div id="exp-content"></div>
<button class="exp-fab" id="exp-fab" title="Neue Ausgabe">
${UI.icon('plus')}
@ -81,6 +102,17 @@ window.Page_expenses = (() => {
});
});
_container.querySelector('#exp-dog-selector')?.addEventListener('click', e => {
const pill = e.target.closest('.exp-dog-pill');
if (!pill) return;
_selectedDogId = pill.dataset.dog ? parseInt(pill.dataset.dog) : null;
_clearCache();
_container.querySelectorAll('.exp-dog-pill').forEach(p =>
p.classList.toggle('active', p.dataset.dog === (pill.dataset.dog))
);
_renderTab();
});
_container.querySelector('#exp-fab')
?.addEventListener('click', () => _showForm(null));
@ -111,7 +143,7 @@ window.Page_expenses = (() => {
// ----------------------------------------------------------
async function _renderUebersicht(el) {
if (!_summary) {
_summary = await API.get('/expenses/summary');
_summary = await API.get('/expenses/summary' + _dogParam());
}
const s = _summary;
@ -120,14 +152,20 @@ window.Page_expenses = (() => {
const trendHtml = _trendHtml(letzteMonat);
const kacheln = KATEGORIEN.map(k => {
const betrag = s.monat[k.id] || 0;
const monat = s.monat[k.id] || 0;
const jahr = s.jahr[k.id] || 0;
const monatLine = monat > 0
? `<div class="exp-kachel-jahr">${_fmt(monat)} diesen Monat</div>`
: '';
return `
<div class="exp-kachel">
<div class="exp-kachel" data-kat="${k.id}" style="cursor:pointer">
<div class="exp-kachel-icon" style="background:${k.color}20;color:${k.color}">
${UI.icon(k.icon)}
</div>
<div class="exp-kachel-betrag" style="color:var(--c-primary)">${_fmt(betrag)}</div>
<div class="exp-kachel-betrag" style="color:var(--c-primary)">${_fmt(jahr)}</div>
<div class="exp-kachel-label">${k.label}</div>
${monatLine}
<div class="exp-kachel-add">${UI.icon('plus')} eintragen</div>
</div>`;
}).join('');
@ -146,11 +184,15 @@ window.Page_expenses = (() => {
${verlauf}
<div style="height:80px"></div>
`;
el.querySelectorAll('.exp-kachel[data-kat]').forEach(k => {
k.addEventListener('click', () => _showForm(null, k.dataset.kat));
});
}
async function _getLetzteMonateData() {
if (!_entries.length) {
_entries = await API.get('/expenses?limit=500');
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
}
const monatMap = {};
_entries.forEach(e => {
@ -208,7 +250,7 @@ window.Page_expenses = (() => {
// ----------------------------------------------------------
async function _renderEintraege(el) {
if (!_entries.length) {
_entries = await API.get('/expenses?limit=500');
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
}
if (!_entries.length) {
@ -321,6 +363,7 @@ window.Page_expenses = (() => {
async function _renderDauerauftraege(el) {
let recurring = [];
try { recurring = await API.get('/expenses/recurring'); } catch { /* leer */ }
if (_selectedDogId) recurring = recurring.filter(r => r.dog_id === _selectedDogId);
const cards = recurring.map(r => {
const k = _kat(r.kategorie);
@ -481,10 +524,10 @@ window.Page_expenses = (() => {
// ----------------------------------------------------------
async function _renderStatistik(el) {
if (!_summary) {
_summary = await API.get('/expenses/summary');
_summary = await API.get('/expenses/summary' + _dogParam());
}
if (!_entries.length) {
_entries = await API.get('/expenses?limit=500');
_entries = await API.get('/expenses?limit=500' + _dogParamAnd());
}
const s = _summary;
@ -637,14 +680,15 @@ window.Page_expenses = (() => {
// ----------------------------------------------------------
// FORMULAR — Neu / Bearbeiten
// ----------------------------------------------------------
function _showForm(entry) {
function _showForm(entry, preKat) {
const isEdit = !!entry;
const today = new Date().toISOString().split('T')[0];
const formId = 'exp-form';
const selKat = entry?.kategorie || 'sonstiges';
const selKat = entry?.kategorie || preKat || 'sonstiges';
const defaultDogId = entry?.dog_id ?? _selectedDogId;
const dogOptions = (_appState.dogs || []).map(d =>
`<option value="${d.id}"${entry?.dog_id === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
`<option value="${d.id}"${defaultDogId === d.id ? ' selected' : ''}>${_esc(d.name)}</option>`
).join('');
// Kategorie-Kacheln statt Dropdown
@ -730,6 +774,9 @@ window.Page_expenses = (() => {
const modal = UI.modal.open({ title: isEdit ? 'Ausgabe bearbeiten' : 'Neue Ausgabe', body, footer });
// Betrag-Feld fokussieren (besonders beim Schnelleintrag per Kachel)
setTimeout(() => modal.querySelector('input[name="betrag"]')?.focus(), 200);
// Kategorie-Kacheln interaktiv
modal.querySelectorAll('.exp-kat-tile').forEach(tile => {
tile.addEventListener('click', () => {