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:
parent
aa4849d947
commit
55069d246b
28 changed files with 719 additions and 198 deletions
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue