139 lines
7.2 KiB
JavaScript
139 lines
7.2 KiB
JavaScript
// Form for editing one training: date, trainee, trainer, exercises and their approaches
|
||
|
||
import { alert, api, h, SOLO, userName } from './common.js';
|
||
|
||
const jsonRequest = (method, body) => ({
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: body === undefined ? undefined : JSON.stringify(body),
|
||
});
|
||
|
||
function field(label, input, className = '') {
|
||
return h('div', { class: className }, h('label', { class: 'form-label small text-muted mb-1' }, label), input);
|
||
}
|
||
|
||
function approachRow(approach) {
|
||
const row = h('div', { class: 'row g-2 align-items-end approach-row mb-2' },
|
||
h('div', { class: 'col-5 col-md-3' },
|
||
h('input', { type: 'number', class: 'form-control form-control-sm', name: 'weight', min: 0, step: 'any', required: true, value: approach.weight, 'aria-label': 'Вес, кг' })),
|
||
h('div', { class: 'col-auto pb-1' }, '×'),
|
||
h('div', { class: 'col-4 col-md-2' },
|
||
h('input', { type: 'number', class: 'form-control form-control-sm', name: 'reps', min: 1, step: 1, required: true, value: approach.reps, 'aria-label': 'Повторения' })),
|
||
h('div', { class: 'col-auto' },
|
||
h('button', { type: 'button', class: 'btn btn-sm btn-outline-danger', title: 'Убрать подход' }, '×')));
|
||
row.querySelector('button').addEventListener('click', () => row.remove());
|
||
return row;
|
||
}
|
||
|
||
function exerciseCard(exercise) {
|
||
const approaches = h('div', {}, ...exercise.approaches.map(approachRow));
|
||
const addApproach = h('button', { type: 'button', class: 'btn btn-sm btn-outline-secondary' }, '+ подход');
|
||
addApproach.addEventListener('click', () => {
|
||
// a new approach starts as a copy of the last one: sets usually repeat
|
||
const last = approaches.lastElementChild;
|
||
approaches.append(approachRow({
|
||
weight: last ? last.querySelector('[name=weight]').value : 0,
|
||
reps: last ? last.querySelector('[name=reps]').value : 10,
|
||
}));
|
||
});
|
||
|
||
const remove = h('button', { type: 'button', class: 'btn btn-sm btn-outline-danger', title: 'Убрать упражнение' }, 'Убрать упражнение');
|
||
const card = h('div', { class: 'card exercise-card' },
|
||
h('div', { class: 'card-body' },
|
||
h('div', { class: 'row g-2 align-items-end mb-3' },
|
||
field('Упражнение', h('input', { type: 'text', class: 'form-control', name: 'name', list: 'edit-exercise-names', maxlength: 255, required: true, value: exercise.name }), 'col-12 col-md-6'),
|
||
h('div', { class: 'col-auto form-check ms-2 mb-2' },
|
||
h('input', { type: 'checkbox', class: 'form-check-input', name: 'splitted_weight', id: `per-arm-${Math.random().toString(36).slice(2)}`, checked: exercise.splitted_weight }),
|
||
h('label', { class: 'form-check-label' }, 'Вес на каждую руку')),
|
||
h('div', { class: 'col-auto ms-md-auto' }, remove)),
|
||
h('div', { class: 'small text-muted mb-1' }, 'Подходы: вес, кг (0 — без веса) × повторения'),
|
||
approaches,
|
||
addApproach));
|
||
// the checkbox label must point to its checkbox
|
||
const checkbox = card.querySelector('[name=splitted_weight]');
|
||
card.querySelector('.form-check-label').setAttribute('for', checkbox.id);
|
||
remove.addEventListener('click', () => card.remove());
|
||
return card;
|
||
}
|
||
|
||
function readForm(form) {
|
||
return {
|
||
date: form.elements.date.value,
|
||
trainee_id: Number(form.elements.trainee_id.value),
|
||
trainer_id: form.elements.trainer_id.value ? Number(form.elements.trainer_id.value) : null,
|
||
exercises: [...form.querySelectorAll('.exercise-card')].map((card) => ({
|
||
name: card.querySelector('[name=name]').value,
|
||
splitted_weight: card.querySelector('[name=splitted_weight]').checked,
|
||
approaches: [...card.querySelectorAll('.approach-row')].map((row) => ({
|
||
weight: Number(row.querySelector('[name=weight]').value),
|
||
reps: Number(row.querySelector('[name=reps]').value),
|
||
})),
|
||
})),
|
||
};
|
||
}
|
||
|
||
function userSelect(name, users, selectedId, emptyTitle) {
|
||
return h('select', { class: 'form-select', name, required: !emptyTitle },
|
||
emptyTitle ? h('option', { value: '', selected: selectedId === null }, emptyTitle) : null,
|
||
...users.map((user) => h('option', { value: user.id, selected: user.id === selectedId }, userName(user))));
|
||
}
|
||
|
||
/**
|
||
* trainees / trainers: users to choose from. onDone() is called after the training was saved
|
||
* or deleted, onCancel() on "Отмена".
|
||
*/
|
||
export function editForm(training, exerciseNames, { trainees, trainers }, { onDone, onCancel }) {
|
||
const exercisesBox = h('div', {}, ...training.exercises.map(exerciseCard));
|
||
const addExercise = h('button', { type: 'button', class: 'btn btn-outline-secondary mb-3' }, '+ упражнение');
|
||
addExercise.addEventListener('click', () => {
|
||
const card = exerciseCard({ name: '', splitted_weight: false, approaches: [{ weight: 0, reps: 10 }] });
|
||
exercisesBox.append(card);
|
||
card.querySelector('[name=name]').focus();
|
||
});
|
||
|
||
const status = h('div', {});
|
||
const save = h('button', { type: 'submit', class: 'btn btn-primary' }, 'Сохранить');
|
||
const cancel = h('button', { type: 'button', class: 'btn btn-outline-secondary' }, 'Отмена');
|
||
cancel.addEventListener('click', onCancel);
|
||
const remove = h('button', { type: 'button', class: 'btn btn-outline-danger ms-auto' }, 'Удалить тренировку');
|
||
|
||
const form = h('form', {},
|
||
h('datalist', { id: 'edit-exercise-names' }, ...exerciseNames.map((item) => h('option', { value: item.name }))),
|
||
h('div', { class: 'row g-3 mb-3' },
|
||
field('Дата', h('input', { type: 'date', class: 'form-control', name: 'date', required: true, value: training.date }), 'col-12 col-md-3'),
|
||
field('Тренирующийся', userSelect('trainee_id', trainees, training.trainee_id), 'col-12 col-md-3'),
|
||
field('Тренер', userSelect('trainer_id', trainers, training.trainer_id, `${SOLO} (без тренера)`), 'col-12 col-md-3')),
|
||
exercisesBox,
|
||
addExercise,
|
||
h('div', { class: 'd-flex gap-2 align-items-center' },
|
||
save,
|
||
cancel,
|
||
remove),
|
||
status);
|
||
|
||
form.addEventListener('submit', async (event) => {
|
||
event.preventDefault();
|
||
save.disabled = true;
|
||
try {
|
||
await api(`/trainings/${training.id}/`, jsonRequest('PUT', readForm(form)));
|
||
onDone();
|
||
} catch (error) {
|
||
status.replaceChildren(alert('danger', `Не удалось сохранить: ${error.message}`));
|
||
save.disabled = false;
|
||
}
|
||
});
|
||
|
||
remove.addEventListener('click', async () => {
|
||
if (!confirm('Удалить тренировку вместе с упражнениями и подходами?')) {
|
||
return;
|
||
}
|
||
try {
|
||
await api(`/trainings/${training.id}/`, jsonRequest('DELETE'));
|
||
onDone();
|
||
} catch (error) {
|
||
status.replaceChildren(alert('danger', `Не удалось удалить: ${error.message}`));
|
||
}
|
||
});
|
||
|
||
return form;
|
||
}
|