Add per-exercise personal record tracking

Session tracking only ever stored a done boolean - no record of what
weight or reps were actually performed, so there was no way to derive
real progress data even though the dashboard implied "progress
tracking" was a thing. Workout templates only hold a static planned
value, not a history of performance.

Adds actual_weight/actual_reps to workout_session_items, captured
inline when checking off a set in a session (defaults to the workout's
planned weight/reps, editable before confirming). The exercise detail
page now shows the user's personal best (heaviest completed set, tie
broken by reps) computed from their completed session history across
all workouts containing that exercise.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 12:15:14 +03:00
co-authored by Claude Sonnet 5
parent 1cc21ad0c5
commit fa8bf46f8a
11 changed files with 248 additions and 2 deletions
+16
View File
@@ -79,6 +79,22 @@
</div>
</div>
<div class="lg:col-span-4">
@auth
<div class="rounded-2xl p-5 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800 mb-6">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{{ __('app.exercises.personal_best') }}</h3>
@if($personalBest)
<div class="mt-2 flex items-baseline gap-2">
<span class="text-2xl font-extrabold text-gray-900 dark:text-white">{{ rtrim(rtrim(number_format($personalBest->actual_weight, 1), '0'), '.') }} kg</span>
@if($personalBest->actual_reps)
<span class="text-sm text-gray-600 dark:text-gray-300">&times; {{ $personalBest->actual_reps }} {{ __('app.sessions.reps') }}</span>
@endif
</div>
<p class="mt-1 text-xs text-gray-500">{{ optional($personalBest->session)->completed_at?->format('M j, Y') ?? $personalBest->updated_at->format('M j, Y') }}</p>
@else
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">{{ __('app.exercises.no_personal_best') }}</p>
@endif
</div>
@endauth
<div class="rounded-2xl p-5 ring-1 ring-gray-200 dark:ring-gray-700 bg-white dark:bg-gray-800">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Exercise Info</h3>
<dl class="mt-3 space-y-2 text-sm text-gray-700 dark:text-gray-300">
@@ -29,6 +29,8 @@
<form method="POST" action="{{ route('sessions.items.toggle', [$session, $item]) }}" class="session-toggle-form" data-item-id="{{ $item->id }}" data-initial-done="{{ $isDone ? '1' : '0' }}">
@csrf
@method('PATCH')
<input type="hidden" name="actual_weight" class="actual-weight-input" value="{{ old('actual_weight', $si->actual_weight ?? $item->weight) }}">
<input type="hidden" name="actual_reps" class="actual-reps-input" value="{{ old('actual_reps', $si->actual_reps ?? $item->reps) }}">
<button type="submit" class="h-6 w-6 rounded-md border flex items-center justify-center transition-colors duration-150"
aria-pressed="{{ $isDone ? 'true' : 'false' }}"
title="{{ $isDone ? __('app.sessions.mark_complete') : __('app.sessions.progress') }}">
@@ -40,6 +42,17 @@
</form>
<div>
<div class="exercise-title text-base font-semibold {{ $isDone ? 'line-through text-gray-500 dark:text-gray-400' : 'text-gray-900 dark:text-white' }}">{{ $item->exercise->display_name }}</div>
<div class="mt-2 flex items-center gap-3 text-sm">
<label class="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
{{ __('app.sessions.weight_lifted') }}
<input type="number" step="0.5" min="0" class="actual-weight-field w-20 rounded-lg border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-2 py-1" value="{{ $si->actual_weight ?? $item->weight }}">
kg
</label>
<label class="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
{{ __('app.sessions.reps_done') }}
<input type="number" min="0" class="actual-reps-field w-16 rounded-lg border-gray-300 dark:border-gray-700 bg-white/80 dark:bg-gray-900 text-gray-900 dark:text-gray-100 px-2 py-1" value="{{ $si->actual_reps ?? $item->reps }}">
</label>
</div>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
@@ -130,6 +143,17 @@
const next = !wasDone;
updateBtnState(btn, next);
// Sync the visible weight/reps fields (outside the form) into the hidden form inputs
const row = btn.closest('li');
if (row) {
const weightField = row.querySelector('.actual-weight-field');
const repsField = row.querySelector('.actual-reps-field');
const weightInput = form.querySelector('.actual-weight-input');
const repsInput = form.querySelector('.actual-reps-input');
if (weightField && weightInput) weightInput.value = weightField.value;
if (repsField && repsInput) repsInput.value = repsField.value;
}
fetch(form.action, {
method: 'POST',
headers: {