Files
gym-bro/frontend/js/render/workout-card.js
T

207 lines
9.0 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 } from '../state.js';
import { renderExercise } from './exercise-card.js';
import { renderCardioItem } from './cardio-card.js';
import { formatDate } from '../utils.js';
export function renderWorkoutCard(w) {
const isActive = !w.endTime;
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 || 'тип не выбран';
const exCount = w.exercises.length;
const cardioCount = w.cardio.length;
let statusBadge;
if (isReadOnly) statusBadge = '<span class="workout-badge synced-badge">✓ архив</span>';
else if (w.synced) statusBadge = '<span class="workout-badge synced-badge">✓</span>';
else statusBadge = '<span class="workout-badge dirty">● не сохранено</span>';
const editDateBtn = !isReadOnly
? `<span class="editable-date" onclick="event.stopPropagation(); WL.editWorkoutDate('${w.id}')">
<span class="edit-icon">✎</span>
</span>`
: '';
// Метка клиента (если есть)
const clientLabel = clientName
? `<div class="workout-meta" style="color:${clientColor}; font-weight:600;">👤 ${clientName}</div>`
: '';
div.innerHTML = `
<div class="${headerClass}" onclick="WL.toggleWorkout('${w.id}')">
<div>
<div style="font-weight:600;">${isActive ? '🔥 Тренировка' : '✅ Завершена'}</div>
<div class="workout-meta">${dateStr} ${editDateBtn} • ${typeStr}</div>
${clientLabel}
<div class="workout-meta">Упр: ${exCount} • Кардио: ${cardioCount}</div>
</div>
<div>${statusBadge} <span class="chevron ${isExpanded ? 'open' : ''}">›</span></div>
</div>
`;
const body = document.createElement('div');
body.className = `workout-body ${isExpanded ? 'expanded' : ''}`;
if (isExpanded) {
body.appendChild(renderWorkoutDetails(w));
}
div.appendChild(body);
return div;
}
export function renderWorkoutDetails(w) {
const wrap = document.createElement('div');
const isActive = !w.endTime;
const isReadOnly = w.synced && w.endTime;
if (isReadOnly) {
const banner = document.createElement('div');
banner.className = 'read-only-banner';
banner.textContent = '🔒 Тренировка в архиве — только просмотр';
wrap.appendChild(banner);
}
// Тип тренировки
const typeCard = document.createElement('div');
typeCard.className = 'card';
if (isReadOnly) {
typeCard.innerHTML = `
<div class="field-group" style="margin:0;">
<span class="text-muted text-small">Тип тренировки</span>
<div style="font-weight:600; margin-top:4px;">${w.type || '—'}</div>
</div>`;
} else {
typeCard.innerHTML = `
<label class="field-group" style="margin:0;">
<span>Тип тренировки</span>
<select onchange="App.getWorkout('${w.id}').type = this.value; App.saveAndRender();">
<option value="">— выберите —</option>
<option ${w.type==='грудь-руки'?'selected':''}>грудь-руки</option>
<option ${w.type==='ноги'?'selected':''}>ноги</option>
<option ${w.type==='спина-плечи'?'selected':''}>спина-плечи</option>
<option ${w.type==='круговая'?'selected':''}>круговая</option>
<option ${w.type==='силовая'?'selected':''}>силовая</option>
<option ${w.type==='многоповторная'?'selected':''}>многоповторная</option>
</select>
</label>`;
}
wrap.appendChild(typeCard);
// Кнопки добавления
if (isActive && !isReadOnly) {
const addSection = document.createElement('div');
addSection.className = 'add-section';
addSection.innerHTML = `
<button class="btn btn-secondary" onclick="EL.addExercise('${w.id}')">+ Упражнение</button>
<button class="btn btn-secondary" onclick="CL.addCardio('${w.id}')">+ Кардио</button>
`;
wrap.appendChild(addSection);
}
// Кардио: разминка сверху
const cardioWarmup = w.cardio.filter(c => c.purpose === 'warmup' || c.purpose === 'separate');
const cardioCooldown = w.cardio.filter(c => c.purpose === 'cooldown');
if (cardioWarmup.length > 0) {
const title = document.createElement('h4');
title.style.margin = '16px 0 8px';
title.textContent = '🔥 Разминка / Кардио';
wrap.appendChild(title);
cardioWarmup.forEach(c => wrap.appendChild(renderCardioItem(w.id, c, isReadOnly)));
}
// Упражнения
[...w.exercises].reverse().forEach(ex => {
wrap.appendChild(renderExercise(w, ex, isReadOnly));
});
// Заминка внизу
if (cardioCooldown.length > 0) {
const title = document.createElement('h4');
title.style.margin = '16px 0 8px';
title.textContent = '🧘 Заминка';
wrap.appendChild(title);
cardioCooldown.forEach(c => wrap.appendChild(renderCardioItem(w.id, c, isReadOnly)));
}
// Комментарий
const notesCard = document.createElement('div');
notesCard.className = 'card mt-10';
if (isReadOnly) {
notesCard.innerHTML = `
<div class="field-group" style="margin:0;">
<span class="text-muted text-small">Комментарий</span>
<div style="margin-top:4px; white-space:pre-wrap;">${w.notes || '—'}</div>
</div>`;
} else {
notesCard.innerHTML = `
<label class="field-group" style="margin:0;">
<span>Комментарий к тренировке</span>
<textarea placeholder="Как прошла тренировка...">${w.notes || ''}</textarea>
</label>`;
notesCard.querySelector('textarea').oninput = (e) => {
w.notes = e.target.value;
window.App.saveAndRender();
};
}
wrap.appendChild(notesCard);
// Блок завершения
const finishBlock = document.createElement('div');
finishBlock.className = 'finish-block';
if (isReadOnly) {
finishBlock.innerHTML = `
<h3>🔒 Архив</h3>
<p class="text-muted text-small">Тренировка синхронизирована и завершена. Редактирование недоступно.</p>`;
} else if (isActive) {
// Проверяем, есть ли уже данные в тренировке
const hasData = (w.exercises && w.exercises.length > 0) || (w.cardio && w.cardio.length > 0);
// Кнопка отмены показывается ТОЛЬКО если тренировка пустая
const cancelBtnHtml = !hasData ? `
<button class="btn btn-ghost mt-10" style="color: var(--danger); border-color: var(--danger);" onclick="WL.cancelEmptyWorkout('${w.id}')">
✕ Отменить пустую тренировку
</button>
` : '';
finishBlock.innerHTML = `
<h3>Завершение тренировки</h3>
<p class="text-muted text-small">После завершения данные останутся локально до синхронизации</p>
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 10px;">
${cancelBtnHtml}
<button class="btn btn-danger" onclick="WL.finishWorkout('${w.id}')">⏹ Закончить тренировку</button>
</div>
`;
} else {
finishBlock.innerHTML = `
<h3>Тренировка завершена</h3>
<p class="text-muted text-small">До синхронизации можно редактировать</p>
<button class="btn btn-danger" onclick="WL.deleteWorkout('${w.id}')">🗑 Удалить</button>`;
}
wrap.appendChild(finishBlock);
return wrap;
}