499 lines
21 KiB
JavaScript
499 lines
21 KiB
JavaScript
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;
|