Weight in profile, nav stack persistence, GPS kcal in daily expenditure
Profile: - Added weight_kg field to profile modal (was only in weight_log) - Weight fallback chain: weight_log -> profile -> 80kg Bike modal: - Active bike toggle: replaced checkbox with Да/Нет chip buttons Navigation: - navStack persisted to localStorage, restored on app restart - Browser history seeded from saved navStack - Last screen restored after restart (not always "today") Daily expenditure: - getActivityTotalsForDate now includes gps_sessions.total_kcal - getDailyActivityTotals (stats chart) also includes GPS sessions - GPS bike rides now appear in daily kcal expenditure Physics kcal fix: - Use raw altitude (not smoothed) for gradient in power calculation - Smoothed altitude was killing real slopes (296 vs 460 kcal) - Smoothed altitude kept only for displayed elevGain/Loss
This commit is contained in:
parent
4740bdbe11
commit
165e7e7e9c
2 changed files with 62 additions and 18 deletions
|
|
@ -558,11 +558,17 @@ async function getFoodTotalsForDate(date) {
|
|||
}
|
||||
|
||||
async function getActivityTotalsForDate(date) {
|
||||
const rows = await select(
|
||||
const manual = await select(
|
||||
"SELECT SUM(kcal) as kcal, SUM(minutes) as minutes FROM activity_log WHERE date = ?1",
|
||||
[date]
|
||||
);
|
||||
return rows[0] ?? { kcal: 0, minutes: 0 };
|
||||
const gps = await select(
|
||||
"SELECT SUM(total_kcal) as kcal FROM gps_sessions WHERE date = ?1 AND total_kcal IS NOT NULL",
|
||||
[date]
|
||||
);
|
||||
const manualKcal = manual[0]?.kcal ?? 0;
|
||||
const gpsKcal = gps[0]?.kcal ?? 0;
|
||||
return { kcal: manualKcal + gpsKcal, minutes: manual[0]?.minutes ?? 0 };
|
||||
}
|
||||
|
||||
async function getDailyFoodTotals(days) {
|
||||
|
|
@ -577,8 +583,11 @@ async function getDailyFoodTotals(days) {
|
|||
|
||||
async function getDailyActivityTotals(days) {
|
||||
return select(
|
||||
`SELECT date, SUM(kcal) as kcal FROM activity_log
|
||||
WHERE date >= date('now', ?1) GROUP BY date ORDER BY date ASC`,
|
||||
`SELECT date, SUM(kcal) as kcal FROM (
|
||||
SELECT date, kcal FROM activity_log WHERE date >= date('now', ?1)
|
||||
UNION ALL
|
||||
SELECT date, total_kcal as kcal FROM gps_sessions WHERE date >= date('now', ?1) AND total_kcal IS NOT NULL
|
||||
) GROUP BY date ORDER BY date ASC`,
|
||||
[`-${days ?? 30} days`]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,18 @@ let state = {
|
|||
// Navigation history stack — survives in-memory across screen changes.
|
||||
// When app is restored from background, we replay the stack into browser history.
|
||||
const NAV_MAX = 20;
|
||||
const navStack = ["today"]; // always starts with "today" as root
|
||||
// Restore nav stack from localStorage so back gesture works after app restart
|
||||
const navStack = (() => {
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem("baldav_nav_stack"));
|
||||
if (Array.isArray(saved) && saved.length > 0) return saved;
|
||||
} catch {}
|
||||
return ["today"];
|
||||
})();
|
||||
|
||||
function persistNavStack() {
|
||||
try { localStorage.setItem("baldav_nav_stack", JSON.stringify(navStack)); } catch {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boot
|
||||
|
|
@ -75,15 +86,16 @@ async function init() {
|
|||
state.profile = await DB.getFullProfile();
|
||||
state.latestWeight = await DB.getLatestWeight();
|
||||
|
||||
// Seed browser history with a few entries so back gesture doesn't immediately close app
|
||||
history.replaceState({ screen: "today" }, "");
|
||||
for (let i = 0; i < 5; i++) history.pushState({ screen: "today" }, "");
|
||||
|
||||
if (false) {
|
||||
renderScreen("settings");
|
||||
} else {
|
||||
renderScreen("today");
|
||||
// Seed browser history from navStack so back gesture works after restart
|
||||
history.replaceState({ screen: navStack[0] }, "");
|
||||
for (let i = 1; i < navStack.length; i++) {
|
||||
history.pushState({ screen: navStack[i] }, "");
|
||||
}
|
||||
// Add padding entries so first back doesn't exit
|
||||
for (let i = 0; i < 3; i++) history.pushState({ screen: navStack[navStack.length - 1] }, "");
|
||||
|
||||
// Restore last screen
|
||||
renderScreen(navStack[navStack.length - 1]);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -97,6 +109,7 @@ function renderScreen(name, fromPopstate) {
|
|||
if (!fromPopstate && prevScreen && prevScreen !== name) {
|
||||
navStack.push(name);
|
||||
if (navStack.length > NAV_MAX) navStack.shift();
|
||||
persistNavStack();
|
||||
history.pushState({ screen: name }, "");
|
||||
}
|
||||
const screens = {
|
||||
|
|
@ -2851,6 +2864,10 @@ function showProfileModal() {
|
|||
|
||||
showModal(`
|
||||
<h3>Профиль</h3>
|
||||
<div class="form-group">
|
||||
<label>Вес (кг)</label>
|
||||
<input id="m-weight" type="number" inputmode="decimal" step="0.1" value="${p.weight_kg ?? ""}" placeholder="80" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Рост (см)</label>
|
||||
<input id="m-height" type="number" inputmode="numeric" value="${p.height_cm ?? ""}" placeholder="178" />
|
||||
|
|
@ -2875,12 +2892,14 @@ function showProfileModal() {
|
|||
<input id="m-bmr-est" type="number" inputmode="numeric" value="${p.bmr_estimated ?? ""}" placeholder="1580" />
|
||||
</div>`,
|
||||
async overlay => {
|
||||
const weight = parseFloat(overlay.querySelector("#m-weight").value) || null;
|
||||
const height = parseInt(overlay.querySelector("#m-height").value);
|
||||
const year = parseInt(overlay.querySelector("#m-year").value);
|
||||
const bFormula = parseInt(overlay.querySelector("#m-bmr-formula").value) || null;
|
||||
const bEst = parseInt(overlay.querySelector("#m-bmr-est").value) || null;
|
||||
if (isNaN(height) || isNaN(year)) return false;
|
||||
|
||||
if (weight) await DB.setProfileValue("weight_kg", weight);
|
||||
await DB.setProfileValue("height_cm", height);
|
||||
await DB.setProfileValue("birth_year", year);
|
||||
await DB.setProfileValue("sex", selectedSex);
|
||||
|
|
@ -2888,6 +2907,7 @@ function showProfileModal() {
|
|||
if (bEst) await DB.setProfileValue("bmr_estimated", bEst);
|
||||
|
||||
state.profile = { ...state.profile,
|
||||
weight_kg: weight ? String(weight) : state.profile.weight_kg,
|
||||
height_cm: String(height), birth_year: String(year), sex: selectedSex,
|
||||
bmr_formula: bFormula ? String(bFormula) : null,
|
||||
bmr_estimated: bEst ? String(bEst) : null };
|
||||
|
|
@ -2949,10 +2969,11 @@ async function showBikeModal(bikeId) {
|
|||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input id="m-bike-active" type="checkbox" ${bike.is_active ? "checked" : ""} />
|
||||
Активный велосипед
|
||||
</label>
|
||||
<label>Активный велосипед</label>
|
||||
<div class="type-chips">
|
||||
<button class="type-chip ${bike.is_active ? "selected" : ""}" id="m-bike-active-yes" data-active="1">Да</button>
|
||||
<button class="type-chip ${!bike.is_active ? "selected" : ""}" id="m-bike-active-no" data-active="0">Нет</button>
|
||||
</div>
|
||||
</div>
|
||||
${!isNew ? `<button id="m-del-bike" class="btn-secondary full-width" style="margin-top:12px;color:#cf6679">Удалить велосипед</button>` : ""}`,
|
||||
async overlay => {
|
||||
|
|
@ -2965,7 +2986,7 @@ async function showBikeModal(bikeId) {
|
|||
chainring_teeth: parseInt(overlay.querySelector("#m-bike-chainring").value) || null,
|
||||
cog_teeth: parseInt(overlay.querySelector("#m-bike-cog").value) || null,
|
||||
surface_type: overlay.querySelector("#m-bike-surface").value,
|
||||
is_active: overlay.querySelector("#m-bike-active").checked,
|
||||
is_active: overlay.querySelector("#m-bike-active-yes").classList.contains("selected"),
|
||||
};
|
||||
if (isNew) {
|
||||
await DB.saveBike(data);
|
||||
|
|
@ -2976,6 +2997,19 @@ async function showBikeModal(bikeId) {
|
|||
renderScreen("settings");
|
||||
});
|
||||
|
||||
// Active toggle chips
|
||||
setTimeout(() => {
|
||||
const overlay = document.querySelector(".modal-overlay");
|
||||
if (!overlay) return;
|
||||
overlay.querySelectorAll("#m-bike-active-yes, #m-bike-active-no").forEach(btn => {
|
||||
btn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
overlay.querySelector("#m-bike-active-yes").classList.toggle("selected", btn.dataset.active === "1");
|
||||
overlay.querySelector("#m-bike-active-no").classList.toggle("selected", btn.dataset.active === "0");
|
||||
};
|
||||
});
|
||||
}, 50);
|
||||
|
||||
if (!isNew) {
|
||||
setTimeout(() => {
|
||||
const delBtn = document.querySelector("#m-del-bike");
|
||||
|
|
@ -3191,6 +3225,7 @@ window.addEventListener("popstate", (e) => {
|
|||
// Pop our nav stack and navigate to previous screen
|
||||
if (navStack.length > 1) {
|
||||
navStack.pop();
|
||||
persistNavStack();
|
||||
const prev = navStack[navStack.length - 1];
|
||||
renderScreen(prev, true);
|
||||
} else if (e.state && e.state.screen) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue