Feature: Welcome-Chips — Termin nur <60 Tage, Übung des Tages als Fallback, async Gassirunde-Bank — SW by-v476, APP_VER 453
This commit is contained in:
parent
db386da2c0
commit
4e1e7ca37e
5 changed files with 102 additions and 15 deletions
|
|
@ -131,10 +131,12 @@ async def get_welcome_dashboard(dog_id: int, user=Depends(get_current_user)):
|
||||||
).fetchone()
|
).fetchone()
|
||||||
last_diary = dict(last_diary_row) if last_diary_row else None
|
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(
|
next_appt_row = conn.execute(
|
||||||
"""SELECT bezeichnung, naechstes, typ FROM health
|
"""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'
|
AND typ != 'gewicht'
|
||||||
ORDER BY naechstes ASC LIMIT 1""",
|
ORDER BY naechstes ASC LIMIT 1""",
|
||||||
(dog_id,)
|
(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,)
|
"SELECT COUNT(*) AS n FROM diary WHERE dog_id=?", (dog_id,)
|
||||||
).fetchone()["n"]
|
).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 {
|
return {
|
||||||
"random_photo": random_photo,
|
"random_photo": random_photo,
|
||||||
"last_diary": last_diary,
|
"last_diary": last_diary,
|
||||||
"next_appointment": next_appointment,
|
"next_appointment": next_appointment,
|
||||||
"last_weight": last_weight,
|
"last_weight": last_weight,
|
||||||
"diary_count": diary_count,
|
"diary_count": diary_count,
|
||||||
|
"daily_exercise": daily_exercise,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -694,6 +694,11 @@ const API = (() => {
|
||||||
jahresbericht() { return post('/zucht-ki/jahresbericht', {}); },
|
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
|
// Öffentliche API
|
||||||
return {
|
return {
|
||||||
get, post, put, patch, del, upload,
|
get, post, put, patch, del, upload,
|
||||||
|
|
@ -701,6 +706,7 @@ const API = (() => {
|
||||||
places, routes, walks, events, sitting, forum, lost, knigge, weather, push,
|
places, routes, walks, events, sitting, forum, lost, knigge, weather, push,
|
||||||
friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes,
|
friends, chat, webcal, importData, sharing, widget, notifications, services, ratings, sittingAccess, training, notes,
|
||||||
breeder, litters, breederPhotos, zuchthunde, zuchtKi,
|
breeder, litters, breederPhotos, zuchthunde, zuchtKi,
|
||||||
|
osm,
|
||||||
subscribeToPush, getLocation, clientNow,
|
subscribeToPush, getLocation, clientNow,
|
||||||
APIError,
|
APIError,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -327,15 +327,39 @@ window.Page_welcome = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 `
|
||||||
|
<div class="wc-chip" id="wc-chip-mid" data-nav="health">
|
||||||
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${apptIcon}"></use></svg>
|
||||||
|
<span class="wc-chip-label">Nächster Termin</span>
|
||||||
|
<span class="wc-chip-val">${apptLabel} · ${apptDate}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
const ex = dashData?.daily_exercise;
|
||||||
|
if (ex) {
|
||||||
|
return `
|
||||||
|
<div class="wc-chip" id="wc-chip-mid" data-nav="uebungen">
|
||||||
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
|
||||||
|
<span class="wc-chip-label">Übung des Tages</span>
|
||||||
|
<span class="wc-chip-val">${UI.escape(ex.name)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
<div class="wc-chip" id="wc-chip-mid" data-nav="uebungen">
|
||||||
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
|
||||||
|
<span class="wc-chip-label">Übung des Tages</span>
|
||||||
|
<span class="wc-chip-val wc-chip-val--empty">—</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
function _chipsHTML(dashData) {
|
function _chipsHTML(dashData) {
|
||||||
const diaryDate = dashData?.last_diary?.datum;
|
const diaryDate = dashData?.last_diary?.datum;
|
||||||
const diaryText = diaryDate ? (_relDate(diaryDate) || diaryDate) : '—';
|
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 weight = dashData?.last_weight;
|
||||||
const weightVal = weight ? `${weight.wert} ${weight.einheit}` : '—';
|
const weightVal = weight ? `${weight.wert} ${weight.einheit}` : '—';
|
||||||
|
|
||||||
|
|
@ -346,11 +370,7 @@ window.Page_welcome = (() => {
|
||||||
<span class="wc-chip-label">Tagebuch</span>
|
<span class="wc-chip-label">Tagebuch</span>
|
||||||
<span class="wc-chip-val${diaryDate ? '' : ' wc-chip-val--empty'}">${diaryText}</span>
|
<span class="wc-chip-val${diaryDate ? '' : ' wc-chip-val--empty'}">${diaryText}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="wc-chip" data-nav="health">
|
${_chip2HTML(dashData)}
|
||||||
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${apptIcon}"></use></svg>
|
|
||||||
<span class="wc-chip-label">Nächster Termin</span>
|
|
||||||
<span class="wc-chip-val${appt ? '' : ' wc-chip-val--empty'}">${appt ? apptLabel + ' · ' + apptDate : '—'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="wc-chip" data-nav="health">
|
<div class="wc-chip" data-nav="health">
|
||||||
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg>
|
||||||
<span class="wc-chip-label">Gewicht</span>
|
<span class="wc-chip-label">Gewicht</span>
|
||||||
|
|
@ -359,6 +379,48 @@ window.Page_welcome = (() => {
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<svg class="ph-icon wc-chip-icon" aria-hidden="true"><use href="/icons/phosphor.svg#path"></use></svg>
|
||||||
|
<span class="wc-chip-label">Gassirunde</span>
|
||||||
|
<span class="wc-chip-val">${UI.escape(name)} · ${distTxt}</span>`;
|
||||||
|
// 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
|
// EINGELOGGTE ANSICHT — visueller Überblick
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -418,6 +480,7 @@ window.Page_welcome = (() => {
|
||||||
API.dogs.welcomeDashboard(dog.id).then(dash => {
|
API.dogs.welcomeDashboard(dog.id).then(dash => {
|
||||||
_updateHeroFromDash(dash, dog);
|
_updateHeroFromDash(dash, dog);
|
||||||
_updateChipsFromDash(dash);
|
_updateChipsFromDash(dash);
|
||||||
|
_tryBenchChip(dash); // nach Chips-Update: ggf. mit naher Bank ersetzen
|
||||||
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
}).catch(() => { /* Skeleton bleibt sichtbar */ });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v475';
|
const CACHE_VERSION = 'by-v476';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue