Files
gym-bro/frontend/js/logic/settings.js
T

499 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
app.innerHTML = '';
const screen = document.createElement('div');
screen.className = 'settings-screen';
screen.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2>⚙️ Настройки</h2>
<button class="btn btn-small btn-secondary" onclick="App.backToMain()">← Назад</button>
</div>
<div class="settings-section">
<h3>👥 Клиенты</h3>
<p class="text-muted text-small mb-10">Управление клиентами и их замерами. Цвет клиента используется для подсветки тренировок.</p>
<button class="btn btn-small mb-10" onclick="SettingsLogic.showAddClientModal()">+ Добавить клиента</button>
<div id="clients-list"></div>
</div>
<div class="settings-section">
<h3>📋 Справочник упражнений</h3>
<p class="text-muted text-small mb-10">Управляйте списком упражнений и привязывайте их к типам тренировок.</p>
<div class="field-group">
<div style="display:flex; gap:6px;">
<input type="text" id="new-ref-exercise" placeholder="Новое упражнение...">
<select id="new-ref-type" style="width: auto; min-width: 120px;">
<option value="">тип...</option>
${state.workoutTypes.map(wt => `<option value="${wt.name}">${wt.name}</option>`).join('')}
</select>
<button class="btn btn-small" style="width:auto; padding: 0 20px;" onclick="SettingsLogic.addRefExercise()">+</button>
</div>
</div>
<div id="ref-exercises-list"></div>
</div>
<div class="settings-section">
<h3>📏 Личные замеры</h3>
<p class="text-muted text-small mb-10">Замеры без привязки к клиенту (для личного режима).</p>
<button class="btn btn-small mb-10" onclick="ML.showBodyMetrics()">+ Добавить личный замер</button>
<div id="body-metrics-list"></div>
</div>
<div class="settings-section">
<h3>🗑 Данные</h3>
<button class="btn btn-danger" onclick="SettingsLogic.clearAllData()">Очистить все данные</button>
</div>
`;
app.appendChild(screen);
_renderClientsList();
_renderRefExercises();
_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 = '<p class="text-muted text-small">Нет личных замеров</p>';
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 `
<div class="exercise-ref-item">
<div>
<div style="font-weight:500;">${dateStr}</div>
<div class="text-small text-muted">${parts.join(' • ') || '—'}</div>
</div>
<div style="display:flex; gap:4px;">
<button class="icon-btn" onclick="ML.editBodyMetric('${m.id}')">✎</button>
<button class="icon-btn danger" onclick="ML.deleteMetric('${m.id}')">✕</button>
</div>
</div>
`;
}).join('');
}
export function addRefExercise() {
const input = document.getElementById('new-ref-exercise');
const typeSelect = document.getElementById('new-ref-type');
const name = input.value.trim();
if (!name) return;
const normalizedName = name.trim().toLowerCase();
const exists = state.exerciseRef.find(
e => e.name.trim().toLowerCase() === normalizedName
);
if (!exists) {
state.exerciseRef.push({
id: (state.nextId++).toString(),
name: name.trim(),
workoutType: typeSelect.value,
});
}
saveState();
input.value = '';
typeSelect.value = '';
_renderRefExercises();
}
export function deleteRefExercise(id) {
if (!confirm('Удалить из справочника? (в тренировках останется)')) return;
state.exerciseRef = state.exerciseRef.filter(e => e.id !== id);
saveState();
_renderRefExercises();
}
export function updateRefExerciseType(id, newType) {
const ex = state.exerciseRef.find(e => e.id === id);
if (ex) {
ex.workoutType = newType;
saveState();
}
}
export function editRefExercise(id) {
const ex = state.exerciseRef.find(e => e.id === id);
if (!ex) return;
showEditExerciseDialog(ex, (updatedData) => {
if (!updatedData) return;
Object.assign(ex, updatedData);
saveState();
_renderRefExercises();
});
}
export function clearAllData() {
if (!confirm('Удалить ВСЕ данные? Это действие необратимо!')) return;
if (!confirm('Точно уверены?')) return;
localStorage.clear();
location.reload();
}
export function _renderRefExercises() {
const list = document.getElementById('ref-exercises-list');
if (!list) return;
if (state.exerciseRef.length === 0) {
list.innerHTML = '<p class="text-muted text-small">Справочник пуст</p>';
return;
}
const sorted = [...state.exerciseRef].sort((a, b) => {
if (a.workoutType !== b.workoutType) return (a.workoutType || '').localeCompare(b.workoutType || '');
return a.name.localeCompare(b.name);
});
// Генерируем опции для select динамически из state.workoutTypes
const typeOptions = state.workoutTypes.map(wt =>
`<option value="${wt.name}" ${a => a.workoutType === wt.name ? 'selected' : ''}>${wt.name}</option>`
).join('');
list.innerHTML = sorted.map(ex => {
let count = 0;
state.workouts.forEach(w => {
w.exercises.forEach(e => {
if (e.name.trim().toLowerCase() === ex.name.trim().toLowerCase()) count++;
});
});
return `
<div class="exercise-ref-item">
<div style="flex:1;">
<span class="name">${ex.name}</span>
<span class="count">(${count}×)</span>
${ex.workoutType ? `<span class="type-badge">${ex.workoutType}</span>` : ''}
</div>
<div style="display:flex; gap:4px; align-items:center;">
<select onchange="SettingsLogic.updateRefExerciseType('${ex.id}', this.value)"
style="width:auto; min-width:100px; padding:6px; font-size:0.8rem;">
<option value="">тип...</option>
${state.workoutTypes.map(wt => `<option value="${wt.name}" ${ex.workoutType === wt.name ? 'selected' : ''}>${wt.name}</option>`).join('')}
</select>
<button class="icon-btn" onclick="SettingsLogic.editRefExercise('${ex.id}')">✎</button>
<button class="icon-btn danger" onclick="SettingsLogic.deleteRefExercise('${ex.id}')">✕</button>
</div>
</div>
`;
}).join('');
}
export function _renderMetricTypes() {
const container = document.getElementById('metric-types-list');
if (!container) return;
if (state.metricTypes.length === 0) {
container.innerHTML = '<p class="text-muted text-small">Нет типов замеров</p>';
return;
}
container.innerHTML = state.metricTypes.map(mt => `
<div class="exercise-ref-item">
<div>
<span class="name">${mt.name}</span>
<span class="type-badge">${mt.unit}</span>
</div>
</div>
`).join('');
}
export function _renderBodyMetricsList() {
const list = document.getElementById('body-metrics-list');
if (!list) return;
if (state.bodyMetrics.length === 0) {
list.innerHTML = '<p class="text-muted text-small">Нет замеров</p>';
return;
}
list.innerHTML = 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 `
<div class="exercise-ref-item">
<div>
<div style="font-weight:500;">${dateStr}</div>
<div class="text-small text-muted">${parts.join(' • ') || '—'}</div>
</div>
<div style="display:flex; gap:4px;">
<button class="icon-btn" onclick="ML.editBodyMetric('${m.id}')">✎</button>
<button class="icon-btn danger" onclick="ML.deleteMetric('${m.id}')">✕</button>
</div>
</div>
`;
}).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 => `
<div class="color-swatch ${client.color === color ? 'selected' : ''}"
style="background:${color};"
onclick="SettingsLogic.selectClientColor('${color}')"></div>
`).join('');
const html = `
<div class="modal-content">
<h3 class="modal-title">${isEdit ? '✎ Редактировать' : '👤 Новый'} клиент</h3>
<div class="field-group">
<label>ФИО</label>
<input type="text" id="m-client-name" value="${client.full_name}" placeholder="Иванов Иван Иванович">
</div>
<div class="field-group">
<label>Дата рождения</label>
<input type="date" id="m-client-dob" value="${client.birth_date || ''}">
</div>
<div class="field-group">
<label>Цель тренировок</label>
<input type="text" id="m-client-goal" value="${client.goal || ''}" placeholder="Набор массы, похудение...">
</div>
<div class="field-group">
<label>Цвет клиента</label>
<div class="color-palette" id="m-client-palette">
${paletteHtml}
</div>
</div>
<div class="btn-row">
<button class="btn btn-secondary" onclick="ModalBase.closeModal()">Отмена</button>
<button class="btn btn-success" onclick="SettingsLogic.submitClient('${editingClient?.id || ''}')">Сохранить</button>
</div>
</div>
`;
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 = '<p class="text-muted text-small">Нет клиентов. Добавьте первого!</p>';
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 `
<div class="exercise-ref-item" style="border-left: 4px solid ${c.color || DEFAULT_COLOR}; padding-left: 10px;">
<div style="flex:1;">
<div style="font-weight:600;">${c.full_name}</div>
<div class="text-small text-muted">${c.goal || 'Цель не указана'}</div>
<div class="text-small text-muted" style="margin-top:4px;">
🏋️ ${clientWorkouts.length} трен. • 📏 ${clientMetrics.length} замеров
</div>
${lastMetric ? `<div class="text-small" style="margin-top:2px; color:var(--text-secondary);">Последний: ${metricsPreview}</div>` : ''}
</div>
<div style="display:flex; gap:4px; flex-direction:column;">
<button class="icon-btn" onclick="SettingsLogic.showClientMetrics('${c.id}')" title="Замеры">📏</button>
<button class="icon-btn" onclick="SettingsLogic.showAddClientModal(state.clients.find(x => x.id == '${c.id}'))" title="Редактировать">✎</button>
<button class="icon-btn danger" onclick="SettingsLogic.deleteClient('${c.id}')" title="Удалить">✕</button>
</div>
</div>
`;
}).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
? '<p class="text-muted text-small">Нет замеров</p>'
: 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 `
<div class="exercise-ref-item">
<div>
<div style="font-weight:500;">${dateStr}</div>
<div class="text-small text-muted">${parts.join(' • ') || '—'}</div>
</div>
<div style="display:flex; gap:4px;">
<button class="icon-btn" onclick="SettingsLogic.editClientMetric('${m.id}', '${clientId}')">✎</button>
<button class="icon-btn danger" onclick="SettingsLogic.deleteClientMetric('${m.id}', '${clientId}')">✕</button>
</div>
</div>
`;
}).join('');
const html = `
<div class="modal-content">
<h3 class="modal-title">📏 Замеры: ${client.full_name}</h3>
<div id="client-metrics-list" style="max-height:50vh; overflow-y:auto;">
${metricsHtml}
</div>
<div class="btn-row" style="margin-top:16px;">
<button class="btn btn-secondary" onclick="ModalBase.closeModal()">Закрыть</button>
<button class="btn" onclick="SettingsLogic.addClientMetric('${clientId}')">+ Добавить замер</button>
</div>
</div>
`;
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;