diff --git a/frontend/css/styles.css b/frontend/css/styles.css index 456baa6..d4b5ed2 100644 --- a/frontend/css/styles.css +++ b/frontend/css/styles.css @@ -1597,3 +1597,113 @@ select { padding: 40px; color: var(--muted); } + +/* Climb segments (session detail) */ +.climb-segments { + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} +.segment-item { + display: flex; + gap: 8px; + padding: 8px; + border-bottom: 1px solid var(--border); + align-items: center; +} +.segment-item:last-child { + border-bottom: none; +} +.segment-item.climb .seg-icon { + color: #f44336; + font-weight: 700; +} +.segment-item.descent .seg-icon { + color: #4caf50; + font-weight: 700; +} +.segment-item.flat .seg-icon { + color: var(--muted); +} +.seg-icon { + width: 20px; + text-align: center; + font-size: 16px; +} +.seg-info { + flex: 1; + font-size: 13px; +} +.seg-stats { + font-size: 12px; + color: var(--muted); + white-space: nowrap; +} + +/* Bike maintenance list */ +.maint-item { + display: flex; + align-items: center; + padding: 8px; + border-bottom: 1px solid var(--border); + gap: 8px; +} +.maint-item:last-child { + border-bottom: none; +} +.maint-overdue { + background: rgba(244, 67, 54, 0.08); +} +.maint-info { + flex: 1; + font-size: 13px; + display: flex; + gap: 6px; + flex-wrap: wrap; + align-items: center; +} +.maint-date { + color: var(--muted); + font-size: 12px; + min-width: 40px; +} +.maint-desc { + font-weight: 500; +} +.maint-km { + font-size: 12px; + color: var(--muted); +} +.maint-warn { + color: #f44336; + font-weight: 700; +} +.maint-del { + background: none; + border: none; + color: var(--muted); + font-size: 18px; + cursor: pointer; + padding: 4px 8px; + font-family: inherit; +} +.maint-del:active { + color: #f44336; +} + +/* Compare session list */ +.compare-list { + max-height: 60vh; + overflow-y: auto; +} +.compare-item { + padding: 12px; + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 8px; + cursor: pointer; +} +.compare-item:active { + border-color: var(--primary); + background: rgba(187, 134, 252, 0.05); +} diff --git a/frontend/js/db.js b/frontend/js/db.js index 4638e5e..01a6481 100644 --- a/frontend/js/db.js +++ b/frontend/js/db.js @@ -706,12 +706,13 @@ async function saveBike(data) { } const r = await execute( `INSERT INTO bikes (name, weight_kg, wheel_diameter_inch, chainring_teeth, - cog_teeth, gear_count, surface_type, notes, is_active) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`, + cog_teeth, gear_count, surface_type, notes, is_active, tire_pressure_bar) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)`, [data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26, data.chainring_teeth ?? null, data.cog_teeth ?? null, data.gear_count ?? null, data.surface_type ?? "mixed", - data.notes ?? null, data.is_active ? 1 : 0] + data.notes ?? null, data.is_active ? 1 : 0, + data.tire_pressure_bar ?? null] ); return r.lastInsertId; } @@ -723,11 +724,12 @@ async function updateBike(id, data) { await execute( `UPDATE bikes SET name=?1, weight_kg=?2, wheel_diameter_inch=?3, chainring_teeth=?4, cog_teeth=?5, gear_count=?6, surface_type=?7, - notes=?8, is_active=?9 WHERE id=?10`, + notes=?8, is_active=?9, tire_pressure_bar=?10 WHERE id=?11`, [data.name, data.weight_kg ?? null, data.wheel_diameter_inch ?? 26, data.chainring_teeth ?? null, data.cog_teeth ?? null, data.gear_count ?? null, data.surface_type ?? "mixed", - data.notes ?? null, data.is_active ? 1 : 0, id] + data.notes ?? null, data.is_active ? 1 : 0, + data.tire_pressure_bar ?? null, id] ); } @@ -739,6 +741,45 @@ async function updateSessionBike(sessionId, bikeId) { await execute("UPDATE gps_sessions SET bike_id = ?1 WHERE id = ?2", [bikeId, sessionId]); } +// --------------------------------------------------------------------------- +// Bike maintenance log +// --------------------------------------------------------------------------- + +async function getBikeMaintenanceLog(bikeId) { + return select( + "SELECT * FROM bike_maintenance WHERE bike_id = ?1 ORDER BY date DESC", + [bikeId] + ); +} + +async function addBikeMaintenance(bikeId, date, description, mileageKm, nextServiceKm, notes) { + const r = await execute( + `INSERT INTO bike_maintenance (bike_id, date, description, mileage_km, next_service_km, notes) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)`, + [bikeId, date, description, mileageKm ?? null, nextServiceKm ?? null, notes ?? null] + ); + return r.lastInsertId; +} + +async function deleteBikeMaintenance(id) { + await execute("DELETE FROM bike_maintenance WHERE id = ?1", [id]); +} + +async function getBikeTotalMileage(bikeId) { + const rows = await select( + "SELECT SUM(total_distance_m) as total FROM gps_sessions WHERE bike_id = ?1 AND total_distance_m IS NOT NULL", + [bikeId] + ); + const totalM = rows[0]?.total ?? 0; + return Math.round(totalM / 100) / 10; // km, 1 decimal +} + +async function getBikeMaintenanceDue(bikeId) { + const mileage = await getBikeTotalMileage(bikeId); + const all = await getBikeMaintenanceLog(bikeId); + return all.filter(e => e.next_service_km != null && mileage >= e.next_service_km); +} + // Expose as globals — no ES module system, loaded via