f1tness_parser/app/templates/exercises.html
2026-10-22 23:46:29 +03:00

162 lines
10 KiB
HTML
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.

{% extends "base.html" %}
{% from "_trainings_table.html" import approach_chips %}
{% block content %}
{% macro num(value) %}{{ '%g'|format(value|round(1)) }}{% endmacro %}
{# "+12 кг (+6.5%)" colored by direction; unit is "кг" or "повт." #}
{% macro change(value, percent, unit) %}
<span class="{% if value > 0 %}text-success{% elif value < 0 %}text-danger{% else %}text-muted{% endif %}">
{{ '%+g'|format(value|round(1)) }} {{ unit }}{% if percent is not none %} ({{ '%+.1f'|format(percent) }}%){% endif %}
</span> к первой тренировке
{% endmacro %}
{# Text with a dotted underline and a tooltip explaining it #}
{% macro tip(text, hint) %}<span class="has-tip" data-bs-toggle="tooltip" title="{{ hint }}">{{ text }}</span>{% endmacro %}
{% macro stat_card(title, value, note, hint) %}
<div class="col">
<div class="card h-100 mb-0">
<div class="card-body">
<div class="text-muted small">{{ tip(title, hint) }}</div>
<div class="fs-4 fw-semibold">{{ value }}</div>
<div class="text-muted small">{{ note }}</div>
</div>
</div>
</div>
{% endmacro %}
{% set hints = {
'record_e1rm': 'e1RM — расчётный максимум на один повтор по лучшему подходу тренировки (формула Эпли: вес × (1 + повторы / 30)). Рекорд — наибольшее значение за всю историю.',
'last_e1rm': 'e1RM лучшего подхода последней тренировки. Ниже — изменение относительно первой тренировки с этим упражнением.',
'record_volume': 'Тоннаж — суммарная работа за тренировку: сумма вес × повторы по всем подходам с весом. Рекорд — тренировка с наибольшим тоннажем.',
'max_weight': 'Самый тяжёлый вес в одном подходе за всю историю, без учёта повторов.',
'best_reps': 'Наибольшее число повторов в одном подходе за всю историю.',
'last_best_reps': 'Наибольшее число повторов в одном подходе последней тренировки. Ниже — изменение относительно первой тренировки.',
'record_total_reps': 'Наибольшая сумма повторов по всем подходам за одну тренировку.',
'e1rm': 'Расчётный максимум на один повтор по лучшему подходу тренировки (формула Эпли: вес × (1 + повторы / 30)). Подходы без веса не учитываются.',
'volume': 'Тоннаж: сумма вес × повторы по подходам с весом за тренировку.',
} %}
<h1 class="h3 mb-3">История упражнения</h1>
<form method="get" action="{{ url_for('exercises') }}" class="mb-4">
<label for="exercise-input" class="form-label">Упражнение</label>
<div class="input-group">
<input type="search" id="exercise-input" name="name" class="form-control" list="exercise-names"
value="{{ selected or query }}" placeholder="Начните вводить название…"{% if not query %} autofocus{% endif %}>
<button type="submit" class="btn btn-primary">Показать</button>
</div>
<datalist id="exercise-names">
{% for item in exercise_names %}<option value="{{ item.name }}"></option>{% endfor %}
</datalist>
</form>
{% if selected %}
{% set rows = progress.rows %}
<h2 class="h4">{{ selected }}</h2>
<p class="text-muted">
Тренировок: <strong>{{ rows|length }}</strong>,
первая: <strong>{{ rows[-1].session.date.strftime('%d.%m.%Y') }}</strong>,
последняя: <strong>{{ rows[0].session.date.strftime('%d.%m.%Y') }}</strong>
</p>
<div class="row row-cols-1 row-cols-sm-2 row-cols-xl-4 g-3 mb-4">
{% if progress.weighted %}
{{ stat_card('Рекорд e1RM', num(progress.best_primary.value) ~ ' кг', progress.best_primary.date.strftime('%d.%m.%Y'), hints.record_e1rm) }}
{{ stat_card('Последний e1RM', num(progress.last_primary) ~ ' кг',
change(progress.primary_change, progress.primary_change_percent, 'кг') if rows|length > 1 else '',
hints.last_e1rm) }}
{{ stat_card('Рекорд тоннажа', num(progress.best_secondary.value) ~ ' кг', progress.best_secondary.date.strftime('%d.%m.%Y'), hints.record_volume) }}
{{ stat_card('Максимальный вес', num(progress.max_weight) ~ ' кг', '', hints.max_weight) }}
{% else %}
{{ stat_card('Лучший подход', num(progress.best_primary.value) ~ ' повт.', progress.best_primary.date.strftime('%d.%m.%Y'), hints.best_reps) }}
{{ stat_card('Лучший подход в последний раз', num(progress.last_primary) ~ ' повт.',
change(progress.primary_change, progress.primary_change_percent, 'повт.') if rows|length > 1 else '',
hints.last_best_reps) }}
{{ stat_card('Рекорд суммы повторов', num(progress.best_secondary.value), progress.best_secondary.date.strftime('%d.%m.%Y'), hints.record_total_reps) }}
{% endif %}
</div>
<div class="card mb-2">
<div class="card-body">
<div style="height: 320px"><canvas id="progress-chart"></canvas></div>
</div>
</div>
<script type="application/json" id="progress-chart-data">{{ chart|tojson }}</script>
<p class="text-muted small mb-4">
{% if progress.weighted %}
e1RM — расчётный максимум на один повтор по формуле Эпли: вес × (1 + повторы / 30), берётся лучший подход
тренировки; подходы без веса не учитываются. Тоннаж — сумма вес × повторы по подходам с весом.
{% else %}
Упражнение выполняется без дополнительного веса, поэтому прогресс считается по повторам.
{% endif %}
</p>
<div class="table-responsive">
<table class="table table-sm table-hover align-middle" id="exercise-history-table">
<thead>
<tr>
<th scope="col">Дата</th>
<th scope="col">Тренер</th>
<th scope="col">Подходы (вес × повторения)</th>
{% if progress.weighted %}
<th scope="col" class="text-end">Макс. вес</th>
<th scope="col" class="text-end">{{ tip('e1RM', hints.e1rm) }}</th>
<th scope="col" class="text-end">{{ tip('Тоннаж', hints.volume) }}</th>
{% else %}
<th scope="col" class="text-end">Лучший подход</th>
<th scope="col" class="text-end">Всего повторов</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for row in rows %}
{% set item = row.session %}
{% set m = row.metrics %}
<tr>
{# data-order: ISO date and plain numbers, so sorting is chronological/numeric #}
<td class="text-nowrap date" data-order="{{ item.date.isoformat() }}">{{ item.date.strftime('%d.%m.%Y') }}</td>
<td class="text-nowrap">{{ item.trainer or '' }}</td>
<td>
{{ approach_chips(item.approaches) }}
{% if item.splitted_weight %}
<span class="badge text-bg-light border" title="Вес указан на каждую руку (гантель)">на руку</span>
{% endif %}
</td>
{% if progress.weighted %}
<td class="text-nowrap text-end" data-order="{{ m.max_weight }}">{% if m.max_weight %}{{ num(m.max_weight) }} кг{% else %}—{% endif %}</td>
<td class="text-nowrap text-end" data-order="{{ m.e1rm or 0 }}">{% if m.e1rm %}{{ num(m.e1rm) }} кг{% else %}—{% endif %}</td>
<td class="text-nowrap text-end" data-order="{{ m.volume }}">{% if m.volume %}{{ num(m.volume) }} кг{% else %}—{% endif %}</td>
{% else %}
<td class="text-nowrap text-end" data-order="{{ m.best_reps }}">{{ m.best_reps }}</td>
<td class="text-nowrap text-end" data-order="{{ m.total_reps }}">{{ m.total_reps }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif query %}
<div class="alert alert-warning">
Упражнение «{{ query }}» не найдено.
{% if suggestions %}Возможно, вы искали:{% endif %}
</div>
{% if suggestions %}
<div class="list-group">
{% for item in suggestions %}
<a class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
href="{{ url_for('exercises').include_query_params(name=item.name) }}">
{{ item.name }}
<span class="badge text-bg-secondary rounded-pill">{{ item.count }}</span>
</a>
{% endfor %}
</div>
{% endif %}
{% elif not exercise_names %}
<div class="alert alert-info">
В базе пока нет тренировок. Загрузи их из заметок командой:
<br><code>docker compose exec fastapi-app python -m app.cli import</code>
</div>
{% endif %}
{% endblock %}
{% block extra_scripts %}
{% if chart %}<script src="{{ url_for('static', path='/js/chart.umd.js') }}"></script>{% endif %}
{% endblock %}