diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index a4ff043..db0da2e 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -131,10 +131,12 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): ).fetchone() last_diary = dict(last_diary_row) if last_diary_row else None - # Nächster Termin (kein Gewicht) + # Nächster Termin (kein Gewicht, nur innerhalb 60 Tage) next_appt_row = conn.execute( """SELECT bezeichnung, naechstes, typ FROM health - WHERE dog_id=? AND naechstes IS NOT NULL AND naechstes >= date('now') + WHERE dog_id=? AND naechstes IS NOT NULL + AND naechstes >= date('now') + AND naechstes <= date('now', '+60 days') AND typ != 'gewicht' ORDER BY naechstes ASC LIMIT 1""", (dog_id,) @@ -155,12 +157,28 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)): "SELECT COUNT(*) AS n FROM diary WHERE dog_id=?", (dog_id,) ).fetchone()["n"] + # Tagesübung — stabil pro Tag, rotiert täglich + exercise_count = conn.execute( + "SELECT COUNT(*) AS n FROM training_exercises" + ).fetchone()["n"] + daily_exercise = None + if exercise_count: + import datetime as _dt + day_num = (_dt.date.today() - _dt.date(2024, 1, 1)).days + offset = day_num % exercise_count + ex_row = conn.execute( + "SELECT exercise_id, name, kategorie, schwierigkeit FROM training_exercises ORDER BY id LIMIT 1 OFFSET ?", + (offset,) + ).fetchone() + daily_exercise = dict(ex_row) if ex_row else None + return { "random_photo": random_photo, "last_diary": last_diary, "next_appointment": next_appointment, "last_weight": last_weight, "diary_count": diary_count, + "daily_exercise": daily_exercise, } diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 96a3f46..ea6ad6d 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -694,6 +694,11 @@ const API = (() => { jahresbericht() { return post('/zucht-ki/jahresbericht', {}); }, }; + const osm = { + pois: (type, south, west, north, east) => + get(`/osm/pois?type=${type}&south=${south}&west=${west}&north=${north}&east=${east}&fast=true`), + }; + // Öffentliche API return { get, post, put, patch, del, upload, @@ -701,6 +706,7 @@ const API = (() => { places, routes, walks, events, sitting, forum, lost, knigge, weather, push, friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes, breeder, litters, breederPhotos, zuchthunde, zuchtKi, + osm, subscribeToPush, getLocation, clientNow, APIError, }; diff --git a/backend/static/js/app.js b/backend/static/js/app.js index bc72295..a7591e3 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '452'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '453'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const App = (() => { diff --git a/backend/static/js/pages/welcome.js b/backend/static/js/pages/welcome.js index 76ab1cb..92c4399 100644 --- a/backend/static/js/pages/welcome.js +++ b/backend/static/js/pages/welcome.js @@ -327,15 +327,39 @@ window.Page_welcome = (() => { `; } + function _chip2HTML(dashData) { + const appt = dashData?.next_appointment; + if (appt) { + const apptLabel = UI.escape(appt.bezeichnung); + const apptDate = _relDate(appt.naechstes) || appt.naechstes || '—'; + const apptIcon = _appointmentIcon(appt.typ); + return ` +
+ + Nächster Termin + ${apptLabel} · ${apptDate} +
`; + } + const ex = dashData?.daily_exercise; + if (ex) { + return ` +
+ + Übung des Tages + ${UI.escape(ex.name)} +
`; + } + return ` +
+ + Übung des Tages + +
`; + } + function _chipsHTML(dashData) { const diaryDate = dashData?.last_diary?.datum; const diaryText = diaryDate ? (_relDate(diaryDate) || diaryDate) : '—'; - - const appt = dashData?.next_appointment; - const apptLabel = appt ? UI.escape(appt.bezeichnung) : '—'; - const apptDate = appt ? (_relDate(appt.naechstes) || appt.naechstes || '—') : '—'; - const apptIcon = _appointmentIcon(appt?.typ); - const weight = dashData?.last_weight; const weightVal = weight ? `${weight.wert} ${weight.einheit}` : '—'; @@ -346,11 +370,7 @@ window.Page_welcome = (() => { Tagebuch ${diaryText} -
- - Nächster Termin - ${appt ? apptLabel + ' · ' + apptDate : '—'} -
+ ${_chip2HTML(dashData)}
Gewicht @@ -359,6 +379,48 @@ window.Page_welcome = (() => {
`; } + // Versucht async eine Bank in 2 km Umkreis zu finden und ersetzt Chip 2 + async function _tryBenchChip(dashData) { + if (dashData?.next_appointment) return; // Termin hat Vorrang + let loc; + try { loc = await API.getLocation({ timeout: 5000, maximumAge: 300000 }); } + catch { return; } + + const d = 0.018; // ~2 km in Grad + let pois; + try { + pois = await API.osm.pois('bank', loc.lat - d, loc.lon - d, loc.lat + d, loc.lon + d); + } catch { return; } + + if (!pois || pois.length === 0) return; + + // täglich stabile Auswahl, aber täglich andere Bank + const dayIdx = Math.floor(Date.now() / 86400000); + const bench = pois[dayIdx % pois.length]; + const distM = Math.round(_haversine(loc.lat, loc.lon, bench.lat, bench.lon)); + const distTxt = distM < 1000 ? `${distM} m` : `${(distM / 1000).toFixed(1)} km`; + const name = bench.name || 'Bank'; + + const chip2 = _container.querySelector('#wc-chip-mid'); + if (!chip2) return; + chip2.dataset.nav = 'map'; + chip2.dataset.bench = JSON.stringify({ lat: bench.lat, lon: bench.lon }); + chip2.innerHTML = ` + + Gassirunde + ${UI.escape(name)} · ${distTxt}`; + // Event neu binden (ersetzt existierenden) + chip2.addEventListener('click', () => App.navigate('map')); + } + + function _haversine(lat1, lon1, lat2, lon2) { + const R = 6371000; + const f1 = lat1 * Math.PI / 180, f2 = lat2 * Math.PI / 180; + const df = (lat2 - lat1) * Math.PI / 180, dl = (lon2 - lon1) * Math.PI / 180; + const a = Math.sin(df/2)**2 + Math.cos(f1)*Math.cos(f2)*Math.sin(dl/2)**2; + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + } + // ---------------------------------------------------------- // EINGELOGGTE ANSICHT — visueller Überblick // ---------------------------------------------------------- @@ -418,6 +480,7 @@ window.Page_welcome = (() => { API.dogs.welcomeDashboard(dog.id).then(dash => { _updateHeroFromDash(dash, dog); _updateChipsFromDash(dash); + _tryBenchChip(dash); // nach Chips-Update: ggf. mit naher Bank ersetzen }).catch(() => { /* Skeleton bleibt sichtbar */ }); } } diff --git a/backend/static/sw.js b/backend/static/sw.js index 14bc531..3023ff3 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v475'; +const CACHE_VERSION = 'by-v476'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten