// ============================================================ // STATE // ============================================================ const STORAGE_KEY = 'gym_tracker_v4'; const App = { state: { workouts: [], exerciseRef: [], // [{id, name, workoutType}] bodyMetrics: [], expandedWorkoutId: null, expandedExerciseId: null, nextId: 1, }, init() { this.load(); this.render(); }, load() { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { try { this.state = JSON.parse(saved); if (!this.state.exerciseRef) this.state.exerciseRef = []; if (!this.state.bodyMetrics) this.state.bodyMetrics = []; } catch (e) { console.error('Failed to load state', e); } } }, save() { localStorage.setItem(STORAGE_KEY, JSON.stringify(this.state)); this.updateSyncStatus(); }, genId() { return (this.state.nextId++).toString(); }, // ============================================================ // SYNC // ============================================================ getDirtyWorkouts() { return this.state.workouts.filter(w => !w.synced); }, updateSyncStatus() { const dirty = this.getDirtyWorkouts(); const el = document.getElementById('sync-status'); if (dirty.length === 0) { el.textContent = '● Всё синхронизировано'; el.className = 'sync-status synced'; } else { el.textContent = `● ${dirty.length} не сохран.`; el.className = 'sync-status pending'; } }, async sync() { const dirty = this.getDirtyWorkouts(); if (dirty.length === 0) { alert('Нет данных для синхронизации'); return; } console.log('Syncing:', JSON.stringify(dirty, null, 2)); dirty.forEach(w => w.synced = true); this.save(); this.render(); alert(`Отправлено тренировок: ${dirty.length}`); }, // ============================================================ // WORKOUT CRUD // ============================================================ startWorkout() { const w = { id: this.genId(), startTime: new Date().toISOString(), endTime: null, type: '', exercises: [], cardio: [], notes: '', synced: false, }; this.state.workouts.unshift(w); this.state.expandedWorkoutId = w.id; this.save(); this.render(); }, finishWorkout(workoutId) { if (!confirm('Завершить тренировку?')) return; const w = this.getWorkout(workoutId); w.endTime = new Date().toISOString(); this.save(); this.render(); }, deleteWorkout(workoutId) { if (!confirm('Удалить тренировку полностью?')) return; this.state.workouts = this.state.workouts.filter(w => w.id !== workoutId); this.save(); this.render(); }, toggleWorkout(workoutId) { this.state.expandedWorkoutId = this.state.expandedWorkoutId === workoutId ? null : workoutId; this.render(); }, getWorkout(id) { return this.state.workouts.find(w => w.id === id); }, getActiveWorkout() { return this.state.workouts.find(w => !w.endTime); }, editWorkoutDate(workoutId) { const w = this.getWorkout(workoutId); if (!w) return; Modal.showEditWorkoutDateDialog(w, (newDates) => { if (!newDates) return; w.startTime = newDates.startTime; w.endTime = newDates.endTime; this.save(); this.render(); }); }, // ============================================================ // EXERCISE CRUD // ============================================================ addExercise(workoutId) { const w = this.getWorkout(workoutId); Modal.showExerciseDialog(w.type, (name) => { if (!name) return; const ex = { id: this.genId(), name: name, sets: [], }; w.exercises.push(ex); this._addToExerciseRef(name, w.type); this.state.expandedExerciseId = ex.id; this.save(); this.render(); }); }, _addToExerciseRef(name, workoutType = '') { const normalizedName = name.trim().toLowerCase(); const exists = this.state.exerciseRef.find( e => e.name.trim().toLowerCase() === normalizedName ); if (!exists) { this.state.exerciseRef.push({ id: this.genId(), name: name.trim(), workoutType: workoutType, }); } else if (!exists.workoutType && workoutType) { // Обновляем тип, если он был пуст exists.workoutType = workoutType; } }, toggleExercise(exerciseId) { this.state.expandedExerciseId = this.state.expandedExerciseId === exerciseId ? null : exerciseId; this.render(); }, deleteExercise(workoutId, exerciseId) { if (!confirm('Удалить упражнение?')) return; const w = this.getWorkout(workoutId); w.exercises = w.exercises.filter(e => e.id !== exerciseId); this.save(); this.render(); }, getExerciseHistory(exerciseName) { const normalizedName = exerciseName.trim().toLowerCase(); const history = []; for (const w of this.state.workouts) { for (const ex of w.exercises) { if (ex.name.trim().toLowerCase() === normalizedName && ex.sets.length > 0) { history.push({ date: w.startTime, sets: ex.sets, }); } } } history.sort((a, b) => new Date(b.date) - new Date(a.date)); return history.slice(0, 3); }, // ============================================================ // SET CRUD // ============================================================ addSet(workoutId, exerciseId) { const w = this.getWorkout(workoutId); const ex = w.exercises.find(e => e.id === exerciseId); const lastSet = ex.sets[ex.sets.length - 1]; const history = this.getExerciseHistory(ex.name); Modal.showSetDialog(lastSet, history, (setData) => { if (!setData) return; setData.id = this.genId(); setData.timestamp = new Date().toISOString(); ex.sets.push(setData); this.save(); this.render(); }); }, editSet(workoutId, exerciseId, setId) { const w = this.getWorkout(workoutId); const ex = w.exercises.find(e => e.id === exerciseId); const set = ex.sets.find(s => s.id === setId); const history = this.getExerciseHistory(ex.name); Modal.showSetDialog(set, history, (setData) => { if (!setData) return; Object.assign(set, setData); this.save(); this.render(); }); }, deleteSet(workoutId, exerciseId, setId) { if (!confirm('Удалить подход?')) return; const w = this.getWorkout(workoutId); const ex = w.exercises.find(e => e.id === exerciseId); ex.sets = ex.sets.filter(s => s.id !== setId); this.save(); this.render(); }, // ============================================================ // CARDIO CRUD // ============================================================ addCardio(workoutId) { Modal.showCardioDialog(null, (cardio) => { if (!cardio) return; const w = this.getWorkout(workoutId); cardio.id = this.genId(); w.cardio.push(cardio); this.save(); this.render(); }); }, editCardio(workoutId, cardioId) { const w = this.getWorkout(workoutId); const cardio = w.cardio.find(c => c.id === cardioId); Modal.showCardioDialog(cardio, (newData) => { if (!newData) return; Object.assign(cardio, newData); this.save(); this.render(); }); }, deleteCardio(workoutId, cardioId) { if (!confirm('Удалить кардио?')) return; const w = this.getWorkout(workoutId); w.cardio = w.cardio.filter(c => c.id !== cardioId); this.save(); this.render(); }, // ============================================================ // BODY METRICS // ============================================================ showBodyMetrics() { Modal.showBodyMetricsDialog(this.state.bodyMetrics, (newMetric, isEdit) => { if (!newMetric) return; if (isEdit) { // При редактировании — ищем по временной метке исходного замера // (передаём через замыкание) return; } newMetric.id = this.genId(); this.state.bodyMetrics.unshift(newMetric); // Сортируем по дате (новые сверху) this.state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date)); this.save(); this.render(); alert('Замеры сохранены!'); }); }, editBodyMetric(metricId) { const metric = this.state.bodyMetrics.find(m => m.id === metricId); if (!metric) return; Modal.showBodyMetricsDialog(this.state.bodyMetrics, (updatedData, isEdit) => { if (!updatedData) return; Object.assign(metric, updatedData); this.state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date)); this.save(); this._renderBodyMetricsList(); }, metric); // передаём редактируемый замер }, // ============================================================ // SETTINGS // ============================================================ showSettings() { const app = document.getElementById('app'); app.innerHTML = ''; const screen = document.createElement('div'); screen.className = 'settings-screen'; screen.innerHTML = `
Управляйте списком упражнений и привязывайте их к типам тренировок.
Справочник пуст
'; return; } const sorted = [...this.state.exerciseRef].sort((a, b) => { if (a.workoutType !== b.workoutType) { return (a.workoutType || '').localeCompare(b.workoutType || ''); } return a.name.localeCompare(b.name); }); list.innerHTML = sorted.map(ex => { let count = 0; this.state.workouts.forEach(w => { w.exercises.forEach(e => { if (e.name.trim().toLowerCase() === ex.name.trim().toLowerCase()) { count++; } }); }); return `Нет замеров
'; return; } list.innerHTML = this.state.bodyMetrics.map(m => { const date = new Date(m.date); const dateStr = date.toLocaleDateString('ru-RU') + ' ' + date.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit'}); const parts = []; if (m.weight) parts.push(`⚖️ ${m.weight} кг`); if (m.chest) parts.push(`📏 ${m.chest} см`); if (m.waist) parts.push(`📏 ${m.waist} см`); if (m.hips) parts.push(`📏 ${m.hips} см`); if (m.arms) parts.push(`💪 ${m.arms} см`); if (m.legs) parts.push(`🦵 ${m.legs} см`); return `Начните первую тренировку
Тренировка синхронизирована и завершена. Редактирование недоступно.
`; } else if (isActive) { finishBlock.innerHTML = `После завершения данные останутся локально до синхронизации
`; } else { finishBlock.innerHTML = `До синхронизации можно редактировать
`; } wrap.appendChild(finishBlock); return wrap; }, renderExercise(w, ex, isReadOnly = false) { const isExpanded = this.state.expandedExerciseId === ex.id; const totalSets = ex.sets.length; const summary = totalSets > 0 ? `${totalSets} подх.` : ''; const card = document.createElement('div'); card.className = 'exercise-card'; card.innerHTML = ` `; const body = document.createElement('div'); body.className = `exercise-body ${isExpanded ? 'expanded' : ''}`; if (isExpanded) { if (ex.sets.length > 0) { const table = document.createElement('div'); table.className = 'sets-table'; [...ex.sets].reverse().forEach((set, idx) => { const realIdx = ex.sets.length - 1 - idx; table.appendChild(this.renderSetRow(w.id, ex.id, set, realIdx, isReadOnly)); }); body.appendChild(table); } else { body.innerHTML += `Пока нет подходов
`; } if (!isReadOnly) { if (!w.endTime) { const addBtn = document.createElement('button'); addBtn.className = 'btn mt-10'; addBtn.textContent = '+ Добавить подход'; addBtn.onclick = () => this.addSet(w.id, ex.id); body.appendChild(addBtn); } const delBtn = document.createElement('button'); delBtn.className = 'btn btn-ghost mt-10'; delBtn.style.width = '100%'; delBtn.textContent = 'Удалить упражнение'; delBtn.onclick = (e) => { e.stopPropagation(); this.deleteExercise(w.id, ex.id); }; body.appendChild(delBtn); } } card.appendChild(body); return card; }, renderSetRow(workoutId, exerciseId, set, index, isReadOnly = false) { const row = document.createElement('div'); const actionsHtml = isReadOnly ? '' : `