diff --git a/frontend/css/styles.css b/frontend/css/styles.css index d4b5ed2..8c683bd 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1707,3 +1707,151 @@ select { border-color: var(--primary); background: rgba(187, 134, 252, 0.05); } + +/* 7-day balance trend (IDEA 6) */ +.balance-trend { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 12px; + background: var(--surface); + border-bottom: 1px solid var(--border); + min-height: 0; +} +.balance-trend:empty { + display: none; +} +.balance-trend-label { + font-size: 12px; + color: var(--muted); +} +.balance-trend-val { + font-size: 14px; + font-weight: 700; +} +.trend-deficit { + color: #03dac6; +} +.trend-surplus { + color: #cf6679; +} + +/* Goal markers (IDEA 2) */ +.goal-markers-list { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 50vh; + overflow-y: auto; +} +.goal-marker-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); +} +.goal-marker-info { + flex: 1; +} +.goal-marker-name { + font-size: 14px; + font-weight: 600; + color: var(--on-surface); +} +.goal-marker-coords { + font-size: 11px; + color: var(--muted); + margin-top: 2px; +} +.goal-marker-notes { + font-size: 12px; + color: var(--muted); + margin-top: 2px; + font-style: italic; +} + +/* Goal marker map (add marker modal) */ +.gm-map-wrap { + margin-top: 8px; +} +.gm-map { + height: 250px; + border-radius: var(--radius); + overflow: hidden; +} +.gm-coords { + font-size: 12px; + color: var(--muted); + margin-top: 4px; + text-align: center; +} + +/* Goal markers in session detail */ +.goal-marker-item { + padding: 8px 0; + border-bottom: 1px solid rgba(255,255,255,0.04); +} +.goal-marker-item:last-child { + border-bottom: none; +} +.goal-marker-dist { + font-size: 12px; + color: var(--muted); + margin-top: 2px; +} +.goal-bar-wrap { + height: 6px; + background: var(--border); + border-radius: 3px; + margin-top: 6px; + overflow: hidden; +} +.goal-bar-fill { + height: 100%; + background: var(--primary); + border-radius: 3px; + transition: width 0.3s; +} +.goal-progress { + font-size: 11px; + color: var(--muted); + margin-top: 4px; + display: block; +} +.goal-better { + color: #03dac6; +} +.goal-worse { + color: #cf6679; +} + +/* Food cost of workout (IDEA 8) */ +.food-cost { + padding: 4px 0; +} +.food-cost-total { + font-size: 16px; + font-weight: 700; + color: var(--on-surface); + margin-bottom: 6px; +} +.food-cost-items { + display: flex; + flex-direction: column; + gap: 2px; +} +.food-cost-line { + font-size: 14px; + color: var(--on-bg); + padding: 2px 0; +} +.food-cost-or { + font-size: 12px; + color: var(--muted); + font-style: italic; + padding: 0 0 0 8px; +} diff --git a/frontend/js/db.js b/frontend/js/db.js index 01a6481..4f46c8a 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -780,6 +780,69 @@ async function getBikeMaintenanceDue(bikeId) { return all.filter(e => e.next_service_km != null && mileage >= e.next_service_km); } +// --------------------------------------------------------------------------- +// Goal markers +// --------------------------------------------------------------------------- + +async function getGoalMarkers() { + return select("SELECT * FROM goal_markers ORDER BY created_at DESC"); +} + +async function addGoalMarker(name, lat, lon, notes) { + const r = await execute( + `INSERT INTO goal_markers (name, lat, lon, notes) VALUES (?1, ?2, ?3, ?4)`, + [name, lat, lon, notes ?? null] + ); + return r.lastInsertId; +} + +async function deleteGoalMarker(id) { + await execute("DELETE FROM goal_markers WHERE id = ?1", [id]); +} + +// --------------------------------------------------------------------------- +// Food cost helper — most frequently eaten foods in last 7 days +// --------------------------------------------------------------------------- + +async function getFrequentFoods(days) { + return select( + `SELECT product_name, kcal, amount_g, + CASE WHEN amount_g > 0 THEN ROUND(kcal * 100.0 / amount_g) ELSE kcal END as kcal_per_100g + FROM ( + SELECT p.name as product_name, fl.kcal, fl.amount_g, + COUNT(*) as cnt + FROM food_log fl + JOIN products p ON fl.product_id = p.id + WHERE fl.date >= date('now', ?1) + AND fl.kcal > 0 AND fl.amount_g > 0 + GROUP BY p.name + ORDER BY cnt DESC + LIMIT 5 + )`, + [`-${days ?? 7} days`] + ); +} + +// --------------------------------------------------------------------------- +// Closest approach of a session to a goal marker +// Returns {minDist, closestIdx} — computed in JS since SQLite can't do haversine +// --------------------------------------------------------------------------- + +async function getGoalProgress(markerId, sessionId) { + // Fetch all sessions that got close to this marker, ordered by date + // We actually compute this in JS, but store the result concept here + // This function just returns all sessions with their closest approach to a marker + const sessions = await select( + `SELECT gs.id, gs.date, gs.start_time, gs.total_distance_m + FROM gps_sessions gs + WHERE gs.id != ?1 + ORDER BY gs.date DESC, gs.start_time DESC + LIMIT 20`, + [sessionId] + ); + return sessions; +} + // Expose as globals — no ES module system, loaded via