diff --git a/backend/gym.db b/backend/gym.db
index 0ecad92..6fc8292 100644
Binary files a/backend/gym.db and b/backend/gym.db differ
diff --git a/backend/main.py b/backend/main.py
index 4eaa16a..8f02231 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -14,7 +14,7 @@ import routers.sync
Base.metadata.create_all(bind=engine)
app = FastAPI(
- title="Gym Tracker API",
+ title="Gym Bro API",
description="API для трекера тренировок с офлайн-синхронизацией",
version="1.0.0"
)
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 77bca33..d30456e 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -16,6 +16,7 @@ import './modals/exercise-modal.js';
import './modals/set-modal.js';
import './modals/cardio-modal.js';
import './modals/metrics-modal.js';
+import './modals/client-select-modal.js';
import './modals/edit-exercise-modal.js';
import './modals/edit-workout-date-modal.js';
@@ -23,12 +24,12 @@ function renderHeader() {
const modeLabel = state.mode === 'trainer' ? '👨🏫 Тренер' : '👤 Личный';
document.getElementById('app-header').innerHTML = `
-
Gym Tracker (${modeLabel})
+
Gym Bro (${modeLabel})
● Локально
@@ -45,20 +46,20 @@ async function init() {
await initAppData();
}
-// Глобальные хелперы
window.App = {
getWorkout: (id) => state.workouts.find(w => w.id === id),
saveAndRender: () => { saveState(); },
toggleMode: () => {
state.mode = state.mode === 'personal' ? 'trainer' : 'personal';
- state.currentClientId = state.mode === 'personal' ? null : state.currentClientId;
saveState();
renderHeader();
renderMainScreen();
- alert(`Режим изменён на: ${state.mode === 'trainer' ? 'Тренер' : 'Личный трекер'}`);
- }
+ },
+ backToMain: () => renderMainScreen(),
};
window.AppSync = { sync };
-window.App.backToMain = () => renderMainScreen();
+
+// Делаем renderMainScreen доступным глобально
+window.renderMainScreen = renderMainScreen;
document.addEventListener('DOMContentLoaded', init);
diff --git a/frontend/js/logic/exercise.js b/frontend/js/logic/exercise.js
index 5bc1780..ac70729 100644
--- a/frontend/js/logic/exercise.js
+++ b/frontend/js/logic/exercise.js
@@ -1,30 +1,54 @@
import { state, saveState, genId } from '../state.js';
import { renderMainScreen } from '../render/main-screen.js';
import { showExerciseDialog } from '../modals/exercise-modal.js';
-import { fetchExerciseHistory } from '../api.js';
+
+function normalizeName(name) {
+ return String(name || '').trim().toLowerCase();
+}
+
+function normalizeClientId(id) {
+ if (id === undefined || id === null || id === '') return null;
+ return String(id);
+}
+
+function isSameClient(a, b) {
+ return normalizeClientId(a) === normalizeClientId(b);
+}
export function addExercise(workoutId) {
const w = state.workouts.find(x => x.id === workoutId);
+ if (!w) return;
+
showExerciseDialog(w.type, (name) => {
if (!name) return;
- const ex = { id: genId(), name, sets: [] };
+
+ const ex = {
+ id: genId(),
+ name,
+ sets: [],
+ };
+
w.exercises.push(ex);
addToRef(name, w.type);
+
state.expandedExerciseId = ex.id;
+
saveState();
renderMainScreen();
});
}
export function addToRef(name, workoutType = '') {
- const normalizedName = name.trim().toLowerCase();
+ const normalizedName = normalizeName(name);
+
const exists = state.exerciseRef.find(
- e => e.name.trim().toLowerCase() === normalizedName
+ e => normalizeName(e.name) === normalizedName
);
+
if (!exists) {
state.exerciseRef.push({
id: genId(),
- name: name.trim(),
+ name: String(name).trim(),
workoutType,
});
} else if (!exists.workoutType && workoutType) {
@@ -35,48 +59,68 @@ export function addToRef(name, workoutType = '') {
export function toggleExercise(exerciseId) {
state.expandedExerciseId =
state.expandedExerciseId === exerciseId ? null : exerciseId;
+
renderMainScreen();
}
export function deleteExercise(workoutId, exerciseId) {
if (!confirm('Удалить упражнение?')) return;
+
const w = state.workouts.find(x => x.id === workoutId);
+ if (!w) return;
+
w.exercises = w.exercises.filter(e => e.id !== exerciseId);
+
saveState();
renderMainScreen();
}
-export async function getExerciseHistory(exerciseName) {
- // Пытаемся получить с бэкенда
- try {
- const history = await fetchExerciseHistory(exerciseName, state.currentClientId);
- if (history && history.length > 0) {
- return history.map(h => ({
- date: h.date,
- sets: [{ // Адаптируем формат бэкенда под формат фронта
- isDropset: h.is_dropset,
- weight: h.weight,
- reps: h.reps,
- subSets: h.is_dropset ? [{weight: h.weight, reps: h.reps}] : undefined // Упрощение для отображения
- }]
- }));
- }
- } catch (e) {
- console.warn('Не удалось загрузить историю с бэкенда, используем локальную', e);
- }
+/**
+ * Локальная история упражнения.
+ *
+ * @param {string} exerciseName
+ * @param {string|number|null} clientId - если тренировка принадлежит клиенту, история берётся только для этого клиента
+ * @param {string|number|null} excludeWorkoutId - чтобы не показывать текущую тренировку как "прошлую"
+ */
+export function getExerciseHistory(exerciseName, clientId = null, excludeWorkoutId = null) {
+ const normalizedName = normalizeName(exerciseName);
+ const targetClient = normalizeClientId(clientId);
- // Fallback на локальные данные
- const normalizedName = exerciseName.trim().toLowerCase();
const history = [];
+
for (const w of 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 });
+ // Не показываем текущую тренировку в истории
+ if (excludeWorkoutId && String(w.id) === String(excludeWorkoutId)) {
+ continue;
+ }
+
+ // Если у тренировки есть клиент — сравниваем клиентов
+ // Если клиента нет — показываем только личные тренировки без клиента
+ if (!isSameClient(w.client_id, targetClient)) {
+ continue;
+ }
+
+ for (const ex of w.exercises || []) {
+ if (
+ normalizeName(ex.name) === normalizedName &&
+ Array.isArray(ex.sets) &&
+ 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);
}
-window.EL = { addExercise, toggleExercise, deleteExercise };
+window.EL = {
+ addExercise,
+ toggleExercise,
+ deleteExercise,
+};
diff --git a/frontend/js/logic/metrics.js b/frontend/js/logic/metrics.js
index aed6ca8..2edaf47 100644
--- a/frontend/js/logic/metrics.js
+++ b/frontend/js/logic/metrics.js
@@ -3,9 +3,13 @@ import { renderMainScreen } from '../render/main-screen.js';
import { showBodyMetricsDialog } from '../modals/metrics-modal.js';
export function showBodyMetrics() {
- showBodyMetricsDialog(state.bodyMetrics, (newMetric) => {
+ // В личном режиме — замеры без клиента
+ const personalMetrics = state.bodyMetrics.filter(m => !m.client_id);
+
+ showBodyMetricsDialog(personalMetrics, (newMetric) => {
if (!newMetric) return;
newMetric.id = genId();
+ newMetric.client_id = null; // личный замер
state.bodyMetrics.unshift(newMetric);
state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
saveState();
@@ -22,10 +26,10 @@ export function editBodyMetric(metricId) {
Object.assign(metric, updatedData);
state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
saveState();
- // Если сейчас на экране настроек — перерисуем список
+ // Обновляем список в настройках, если он открыт
const list = document.getElementById('body-metrics-list');
if (list && typeof window.SettingsLogic !== 'undefined') {
- window.SettingsLogic._renderBodyMetricsList();
+ window.SettingsLogic._renderPersonalMetricsList();
} else {
renderMainScreen();
}
@@ -38,7 +42,7 @@ export function deleteMetric(id) {
saveState();
const list = document.getElementById('body-metrics-list');
if (list && typeof window.SettingsLogic !== 'undefined') {
- window.SettingsLogic._renderBodyMetricsList();
+ window.SettingsLogic._renderPersonalMetricsList();
} else {
renderMainScreen();
}
diff --git a/frontend/js/logic/set.js b/frontend/js/logic/set.js
index c40943e..faddb3c 100644
--- a/frontend/js/logic/set.js
+++ b/frontend/js/logic/set.js
@@ -5,15 +5,31 @@ import { getExerciseHistory } from './exercise.js';
export function addSet(workoutId, exerciseId) {
const w = state.workouts.find(x => x.id === workoutId);
+ if (!w) return;
+
const ex = w.exercises.find(e => e.id === exerciseId);
+ if (!ex) return;
+
+ if (!Array.isArray(ex.sets)) {
+ ex.sets = [];
+ }
+
const lastSet = ex.sets[ex.sets.length - 1];
- const history = getExerciseHistory(ex.name);
+
+ const history = getExerciseHistory(
+ ex.name,
+ w.client_id ?? null,
+ w.id
+ );
showSetDialog(lastSet, history, (setData) => {
if (!setData) return;
+
setData.id = genId();
setData.timestamp = new Date().toISOString();
+
ex.sets.push(setData);
+
saveState();
renderMainScreen();
});
@@ -21,13 +37,29 @@ export function addSet(workoutId, exerciseId) {
export function editSet(workoutId, exerciseId, setId) {
const w = state.workouts.find(x => x.id === workoutId);
+ if (!w) return;
+
const ex = w.exercises.find(e => e.id === exerciseId);
+ if (!ex) return;
+
+ if (!Array.isArray(ex.sets)) {
+ ex.sets = [];
+ }
+
const set = ex.sets.find(s => s.id === setId);
- const history = getExerciseHistory(ex.name);
+ if (!set) return;
+
+ const history = getExerciseHistory(
+ ex.name,
+ w.client_id ?? null,
+ w.id
+ );
showSetDialog(set, history, (setData) => {
if (!setData) return;
+
Object.assign(set, setData);
+
saveState();
renderMainScreen();
});
@@ -35,11 +67,25 @@ export function editSet(workoutId, exerciseId, setId) {
export function deleteSet(workoutId, exerciseId, setId) {
if (!confirm('Удалить подход?')) return;
+
const w = state.workouts.find(x => x.id === workoutId);
+ if (!w) return;
+
const ex = w.exercises.find(e => e.id === exerciseId);
+ if (!ex) return;
+
+ if (!Array.isArray(ex.sets)) {
+ ex.sets = [];
+ }
+
ex.sets = ex.sets.filter(s => s.id !== setId);
+
saveState();
renderMainScreen();
}
-window.SL = { addSet, editSet, deleteSet };
+window.SL = {
+ addSet,
+ editSet,
+ deleteSet,
+};
diff --git a/frontend/js/logic/settings.js b/frontend/js/logic/settings.js
index 3561724..96efaa3 100644
--- a/frontend/js/logic/settings.js
+++ b/frontend/js/logic/settings.js
@@ -1,5 +1,7 @@
-import { state, saveState } from '../state.js';
+import { state, saveState, CLIENT_COLORS, DEFAULT_COLOR, genId, generateUUID } from '../state.js';
import { showEditExerciseDialog } from '../modals/edit-exercise-modal.js';
+import { showBodyMetricsDialog } from '../modals/metrics-modal.js';
+import { showModal } from '../modals/modal-base.js';
export function showSettings() {
const app = document.getElementById('app');
@@ -12,6 +14,14 @@ export function showSettings() {
⚙️ Настройки
+
+
+
👥 Клиенты
+
Управление клиентами и их замерами. Цвет клиента используется для подсветки тренировок.
+
+
+
+
+
-
📏 Замеры тела
+
📏 Личные замеры
+
Замеры без привязки к клиенту (для личного режима).
+
+
🗑 Данные
`;
app.appendChild(screen);
+
+ _renderClientsList();
_renderRefExercises();
- _renderBodyMetricsList();
+ _renderPersonalMetricsList();
+}
+
+export function _renderPersonalMetricsList() {
+ const list = document.getElementById('body-metrics-list');
+ if (!list) return;
+
+ const personalMetrics = state.bodyMetrics.filter(m => !m.client_id);
+
+ if (personalMetrics.length === 0) {
+ list.innerHTML = 'Нет личных замеров
';
+ return;
+ }
+
+ list.innerHTML = personalMetrics.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 `
+
+
+
${dateStr}
+
${parts.join(' • ') || '—'}
+
+
+
+
+
+
+ `;
+ }).join('');
}
export function addRefExercise() {
@@ -146,7 +193,6 @@ export function _renderRefExercises() {
}).join('');
}
-// Добавим отображение типов замеров в настройки (для тренера)
export function _renderMetricTypes() {
const container = document.getElementById('metric-types-list');
if (!container) return;
@@ -196,9 +242,257 @@ export function _renderBodyMetricsList() {
}).join('');
}
+export function showAddClientModal(editingClient = null) {
+ const isEdit = !!editingClient;
+ const client = editingClient || { full_name: '', birth_date: '', goal: '', color: DEFAULT_COLOR };
+
+ const paletteHtml = CLIENT_COLORS.map(color => `
+
+ `).join('');
+
+ const html = `
+
+
${isEdit ? '✎ Редактировать' : '👤 Новый'} клиент
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${paletteHtml}
+
+
+
+
+
+
+
+ `;
+ showModal(html);
+ window._tempClientColor = client.color;
+}
+
+export function selectClientColor(color) {
+ window._tempClientColor = color;
+ document.querySelectorAll('#m-client-palette .color-swatch').forEach(el => {
+ el.classList.toggle('selected', el.style.background === color ||
+ rgbToHex(el.style.backgroundColor) === color.toLowerCase());
+ });
+}
+
+function rgbToHex(rgb) {
+ if (rgb.startsWith('#')) return rgb.toLowerCase();
+ const match = rgb.match(/\d+/g);
+ if (!match) return rgb;
+ return '#' + match.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
+}
+
+export function submitClient(editingId) {
+ const name = document.getElementById('m-client-name').value.trim();
+ if (!name) { alert('Введите ФИО'); return; }
+
+ const color = window._tempClientColor || DEFAULT_COLOR;
+
+ if (editingId) {
+ // Редактирование
+ const client = state.clients.find(c => c.id == editingId);
+ if (client) {
+ client.full_name = name;
+ client.birth_date = document.getElementById('m-client-dob').value || null;
+ client.goal = document.getElementById('m-client-goal').value.trim();
+ client.color = color;
+ }
+ } else {
+ // Создание
+ const newClient = {
+ id: genId(),
+ sync_id: generateUUID(),
+ full_name: name,
+ birth_date: document.getElementById('m-client-dob').value || null,
+ goal: document.getElementById('m-client-goal').value.trim(),
+ color: color,
+ is_active: 1,
+ };
+ state.clients.push(newClient);
+ }
+
+ saveState();
+ window.ModalBase.closeModal();
+ _renderClientsList();
+ window.renderMainScreen(); // Обновляем цвета тренировок
+}
+
+export function deleteClient(id) {
+ if (!confirm('Удалить клиента? Тренировки останутся, но без привязки.')) return;
+ state.clients = state.clients.filter(c => c.id != id);
+ // Отвязываем тренировки
+ state.workouts.forEach(w => {
+ if (w.client_id == id) w.client_id = null;
+ });
+ // Отвязываем замеры
+ state.bodyMetrics.forEach(m => {
+ if (m.client_id == id) m.client_id = null;
+ });
+ saveState();
+ _renderClientsList();
+ window.renderMainScreen();
+}
+
+export function _renderClientsList() {
+ const list = document.getElementById('clients-list');
+ if (!list) return;
+
+ if (state.clients.length === 0) {
+ list.innerHTML = 'Нет клиентов. Добавьте первого!
';
+ return;
+ }
+
+ list.innerHTML = state.clients.map(c => {
+ // Считаем замеры этого клиента
+ const clientMetrics = state.bodyMetrics.filter(m => m.client_id == c.id);
+ const lastMetric = clientMetrics[0]; // уже отсортированы по дате
+
+ let metricsPreview = 'Нет замеров';
+ if (lastMetric) {
+ const parts = [];
+ if (lastMetric.weight) parts.push(`⚖️ ${lastMetric.weight} кг`);
+ if (lastMetric.chest) parts.push(`📏 ${lastMetric.chest} см`);
+ metricsPreview = parts.join(' • ') || 'Замеры без значений';
+ }
+
+ // Считаем тренировки
+ const clientWorkouts = state.workouts.filter(w => w.client_id == c.id);
+
+ return `
+
+
+
${c.full_name}
+
${c.goal || 'Цель не указана'}
+
+ 🏋️ ${clientWorkouts.length} трен. • 📏 ${clientMetrics.length} замеров
+
+ ${lastMetric ? `
Последний: ${metricsPreview}
` : ''}
+
+
+
+
+
+
+
+ `;
+ }).join('');
+}
+
+export function showClientMetrics(clientId) {
+ const client = state.clients.find(c => c.id == clientId);
+ if (!client) return;
+
+ const clientMetrics = state.bodyMetrics
+ .filter(m => m.client_id == clientId)
+ .sort((a, b) => new Date(b.date) - new Date(a.date));
+
+ const metricsHtml = clientMetrics.length === 0
+ ? 'Нет замеров
'
+ : clientMetrics.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 `
+
+
+
${dateStr}
+
${parts.join(' • ') || '—'}
+
+
+
+
+
+
+ `;
+ }).join('');
+
+ const html = `
+
+
📏 Замеры: ${client.full_name}
+
+ ${metricsHtml}
+
+
+
+
+
+
+ `;
+ showModal(html);
+}
+
+export function addClientMetric(clientId) {
+ showBodyMetricsDialog(
+ state.bodyMetrics.filter(m => m.client_id == clientId),
+ (newMetric) => {
+ if (!newMetric) return;
+ newMetric.id = genId();
+ newMetric.client_id = parseInt(clientId);
+ state.bodyMetrics.unshift(newMetric);
+ state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
+ saveState();
+ showClientMetrics(clientId); // Перерисовываем модалку
+ }
+ );
+}
+
+export function editClientMetric(metricId, clientId) {
+ const metric = state.bodyMetrics.find(m => m.id === metricId);
+ if (!metric) return;
+ showBodyMetricsDialog(
+ state.bodyMetrics,
+ (updatedData) => {
+ if (!updatedData) return;
+ Object.assign(metric, updatedData);
+ state.bodyMetrics.sort((a, b) => new Date(b.date) - new Date(a.date));
+ saveState();
+ showClientMetrics(clientId);
+ },
+ metric
+ );
+}
+
+export function deleteClientMetric(metricId, clientId) {
+ if (!confirm('Удалить замер?')) return;
+ state.bodyMetrics = state.bodyMetrics.filter(m => m.id !== metricId);
+ saveState();
+ showClientMetrics(clientId);
+}
+
+// Обновляем экспорт
window.SettingsLogic = {
show: showSettings,
addRefExercise, deleteRefExercise, updateRefExerciseType,
editRefExercise, clearAllData,
_renderRefExercises, _renderBodyMetricsList,
+ // Клиенты:
+ showAddClientModal, submitClient, deleteClient, _renderClientsList,
+ selectClientColor,
+ // Замеры клиента:
+ showClientMetrics, addClientMetric, editClientMetric, deleteClientMetric,
};
+
+// Делаем state доступным для inline onclick
+window.state = state;
diff --git a/frontend/js/logic/workout.js b/frontend/js/logic/workout.js
index 30457d5..6ef5a80 100644
--- a/frontend/js/logic/workout.js
+++ b/frontend/js/logic/workout.js
@@ -1,10 +1,23 @@
import { state, saveState, genId } from '../state.js';
import { renderMainScreen } from '../render/main-screen.js';
import { showEditWorkoutDateDialog } from '../modals/edit-workout-date-modal.js';
+import { showClientSelectModal } from '../modals/client-select-modal.js';
export function startWorkout() {
+ // В режиме тренера — сначала выбираем клиента
+ if (state.mode === 'trainer') {
+ showClientSelectModal((clientId) => {
+ _createWorkout(clientId);
+ });
+ } else {
+ _createWorkout(null);
+ }
+}
+
+function _createWorkout(clientId) {
const w = {
id: genId(),
+ client_id: clientId,
startTime: new Date().toISOString(),
endTime: null,
type: '',
diff --git a/frontend/js/modals/client-select-modal.js b/frontend/js/modals/client-select-modal.js
new file mode 100644
index 0000000..0ca7f32
--- /dev/null
+++ b/frontend/js/modals/client-select-modal.js
@@ -0,0 +1,58 @@
+import { showModal, closeModal } from './modal-base.js';
+import { state, CLIENT_COLORS, DEFAULT_COLOR, genId, generateUUID } from '../state.js';
+
+let _cb = null;
+
+export function showClientSelectModal(callback) {
+ _cb = callback;
+
+ if (state.clients.length === 0) {
+ // Нет клиентов — предлагаем создать
+ showModal(`
+
+
👤 Нет клиентов
+
Сначала добавьте клиента в настройках.
+
+
+
+
+
+ `);
+ return;
+ }
+
+ const clientsHtml = state.clients.map(c => `
+
+
+
${c.full_name}
+
${c.goal || '—'}
+
+
›
+
+ `).join('');
+
+ showModal(`
+
+
👤 Для кого тренировка?
+
+ ${clientsHtml}
+
+
+
+
+
+ `);
+}
+
+export function select(clientId) {
+ closeModal();
+ if (_cb) _cb(parseInt(clientId));
+}
+
+export function goToSettings() {
+ closeModal();
+ window.SettingsLogic.show();
+}
+
+window.ClientSelectModal = { select, goToSettings };
diff --git a/frontend/js/modals/modal-base.js b/frontend/js/modals/modal-base.js
index 00df844..976e315 100644
--- a/frontend/js/modals/modal-base.js
+++ b/frontend/js/modals/modal-base.js
@@ -30,4 +30,10 @@ export function stepValue(id, delta) {
input.value = parseFloat((Math.round(newVal / step) * step).toFixed(4));
}
-window.ModalBase = { closeOnBg, closeModal, close: closeModal, stepValue };
+window.ModalBase = {
+ showModal,
+ closeModal,
+ close: closeModal,
+ closeOnBg,
+ stepValue
+};
diff --git a/frontend/js/render/main-screen.js b/frontend/js/render/main-screen.js
index c7d8df7..1e79691 100644
--- a/frontend/js/render/main-screen.js
+++ b/frontend/js/render/main-screen.js
@@ -1,28 +1,44 @@
-// Рендеринг главного экрана: список тренировок.
-// Получает данные из state, возвращает DOM.
-
-import { state } from '../state.js';
+import { state, DEFAULT_COLOR } from '../state.js';
import { renderWorkoutCard } from './workout-card.js';
-import { startWorkout } from '../logic/workout.js';
export function renderMainScreen() {
const app = document.getElementById('app');
+ if (!app) return;
app.innerHTML = '';
- if (state.workouts.length === 0) {
+ // В режиме тренера — подсказка сверху
+ if (state.mode === 'trainer') {
+ const hint = document.createElement('div');
+ hint.className = 'card mb-10';
+ hint.style.background = 'rgba(187, 134, 252, 0.1)';
+ hint.style.borderColor = 'var(--primary)';
+ hint.innerHTML = `
+
+
+
👨🏫 Режим тренера
+
При создании тренировки выберите клиента
+
+
+
+ `;
+ app.appendChild(hint);
+ }
+
+ // Все тренировки (в обоих режимах)
+ const workoutsToShow = state.workouts;
+
+ if (workoutsToShow.length === 0) {
app.innerHTML = `
🏋️ Нет тренировок
-
Начните первую тренировку
-
+
${state.mode === 'trainer' ? 'Создайте первую тренировку для клиента' : 'Начните первую тренировку'}
+
`;
- document.getElementById('btn-start-first').onclick = startWorkout;
return;
}
- // Сортировка: активные сверху, завершённые снизу
- const sorted = [...state.workouts].sort((a, b) => {
+ const sorted = [...workoutsToShow].sort((a, b) => {
if (!a.endTime && b.endTime) return -1;
if (a.endTime && !b.endTime) return 1;
return new Date(b.startTime) - new Date(a.startTime);
@@ -33,7 +49,7 @@ export function renderMainScreen() {
const btn = document.createElement('button');
btn.className = 'btn mb-10';
btn.textContent = '▶ Начать новую тренировку';
- btn.onclick = startWorkout;
+ btn.onclick = () => window.WL.startWorkout();
app.appendChild(btn);
}
diff --git a/frontend/js/render/workout-card.js b/frontend/js/render/workout-card.js
index ca8c54a..eee30a0 100644
--- a/frontend/js/render/workout-card.js
+++ b/frontend/js/render/workout-card.js
@@ -8,9 +8,25 @@ export function renderWorkoutCard(w) {
const isExpanded = state.expandedWorkoutId === w.id;
const isReadOnly = w.synced && w.endTime;
+ // Определяем цвет тренировки
+ let clientColor = null;
+ let clientName = null;
+ if (w.client_id) {
+ const client = state.clients.find(c => c.id == w.client_id);
+ if (client) {
+ clientColor = client.color;
+ clientName = client.full_name;
+ }
+ }
+
const div = document.createElement('div');
div.className = 'workout-accordion';
+ // Добавляем цветной border-left, если есть клиент
+ if (clientColor) {
+ div.style.borderLeft = `4px solid ${clientColor}`;
+ }
+
const headerClass = `workout-header ${isActive ? 'active' : 'completed'}`;
const dateStr = formatDate(w.startTime);
const typeStr = w.type || 'тип не выбран';
@@ -28,11 +44,17 @@ export function renderWorkoutCard(w) {
`
: '';
+ // Метка клиента (если есть)
+ const clientLabel = clientName
+ ? `👤 ${clientName}
`
+ : '';
+
div.innerHTML = `