diff --git a/app/Http/Controllers/ExerciseController.php b/app/Http/Controllers/ExerciseController.php
index dd77ff6..549ac87 100644
--- a/app/Http/Controllers/ExerciseController.php
+++ b/app/Http/Controllers/ExerciseController.php
@@ -6,6 +6,7 @@ use App\Models\BodyPart;
use App\Models\Equipment;
use App\Models\Exercise;
use App\Models\Muscle;
+use App\Models\WorkoutSessionItem;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -104,9 +105,23 @@ class ExerciseController extends Controller
$similarExercises = $similarQuery->get();
+ $personalBest = null;
+ if (Auth::check()) {
+ $personalBest = WorkoutSessionItem::query()
+ ->with('session')
+ ->whereHas('workoutExercise', fn ($q) => $q->where('exercise_id', $exercise->id))
+ ->whereHas('session', fn ($q) => $q->where('user_id', Auth::id()))
+ ->where('done', true)
+ ->whereNotNull('actual_weight')
+ ->orderByDesc('actual_weight')
+ ->orderByDesc('actual_reps')
+ ->first();
+ }
+
return view('exercises.show', [
'exercise' => $exercise,
'similarExercises' => $similarExercises,
+ 'personalBest' => $personalBest,
]);
}
}
diff --git a/app/Http/Controllers/WorkoutSessionController.php b/app/Http/Controllers/WorkoutSessionController.php
index deb26b3..bbfef5c 100644
--- a/app/Http/Controllers/WorkoutSessionController.php
+++ b/app/Http/Controllers/WorkoutSessionController.php
@@ -58,6 +58,11 @@ class WorkoutSessionController extends Controller
abort(404);
}
+ $data = $request->validate([
+ 'actual_weight' => ['nullable', 'numeric', 'min:0', 'max:9999.99'],
+ 'actual_reps' => ['nullable', 'integer', 'min:0', 'max:1000'],
+ ]);
+
$si = WorkoutSessionItem::firstOrCreate([
'session_id' => $session->id,
'workout_exercise_id' => $item->id,
@@ -66,6 +71,14 @@ class WorkoutSessionController extends Controller
]);
$si->done = !$si->done;
+ if ($si->done) {
+ if (array_key_exists('actual_weight', $data) && $data['actual_weight'] !== null) {
+ $si->actual_weight = $data['actual_weight'];
+ }
+ if (array_key_exists('actual_reps', $data) && $data['actual_reps'] !== null) {
+ $si->actual_reps = $data['actual_reps'];
+ }
+ }
$si->save();
return back();
diff --git a/app/Models/WorkoutSessionItem.php b/app/Models/WorkoutSessionItem.php
index 4382b6a..9bf6f7d 100644
--- a/app/Models/WorkoutSessionItem.php
+++ b/app/Models/WorkoutSessionItem.php
@@ -16,12 +16,16 @@ class WorkoutSessionItem extends Model
'done',
'exercise_ms',
'rest_ms',
+ 'actual_weight',
+ 'actual_reps',
];
protected $casts = [
'done' => 'boolean',
'exercise_ms' => 'integer',
'rest_ms' => 'integer',
+ 'actual_weight' => 'decimal:2',
+ 'actual_reps' => 'integer',
];
public function session(): BelongsTo
diff --git a/database/factories/WorkoutSessionItemFactory.php b/database/factories/WorkoutSessionItemFactory.php
new file mode 100644
index 0000000..b1e0ea7
--- /dev/null
+++ b/database/factories/WorkoutSessionItemFactory.php
@@ -0,0 +1,24 @@
+
+ */
+class WorkoutSessionItemFactory extends Factory
+{
+ public function definition(): array
+ {
+ return [
+ 'session_id' => WorkoutSession::factory(),
+ 'workout_exercise_id' => WorkoutExercise::factory(),
+ 'done' => false,
+ 'actual_weight' => null,
+ 'actual_reps' => null,
+ ];
+ }
+}
diff --git a/database/migrations/2026_08_10_091029_add_actual_performance_to_workout_session_items_table.php b/database/migrations/2026_08_10_091029_add_actual_performance_to_workout_session_items_table.php
new file mode 100644
index 0000000..7da23c2
--- /dev/null
+++ b/database/migrations/2026_08_10_091029_add_actual_performance_to_workout_session_items_table.php
@@ -0,0 +1,29 @@
+decimal('actual_weight', 6, 2)->nullable()->after('done');
+ $table->unsignedSmallInteger('actual_reps')->nullable()->after('actual_weight');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('workout_session_items', function (Blueprint $table) {
+ $table->dropColumn(['actual_weight', 'actual_reps']);
+ });
+ }
+};
diff --git a/lang/en/app.php b/lang/en/app.php
index cfaed6f..279c431 100644
--- a/lang/en/app.php
+++ b/lang/en/app.php
@@ -192,6 +192,8 @@ return [
'rename_reset' => 'Reset',
'renamed_success' => 'Your custom name has been saved.',
'rename_cleared' => 'Custom name cleared.',
+ 'personal_best' => 'Your Personal Best',
+ 'no_personal_best' => 'No completed sets logged yet. Track weight and reps when you check off a set in a session.',
],
'sessions' => [
@@ -211,7 +213,9 @@ return [
'rest_timer' => 'Rest timer',
'start_rest' => 'Start',
'pause_rest' => 'Pause',
- 'reset_rest' => 'Reset'
+ 'reset_rest' => 'Reset',
+ 'weight_lifted' => 'Weight',
+ 'reps_done' => 'Reps',
],
'dashboard' => [
diff --git a/lang/ro/app.php b/lang/ro/app.php
index c119d78..e3a834b 100644
--- a/lang/ro/app.php
+++ b/lang/ro/app.php
@@ -192,6 +192,8 @@ return [
'rename_reset' => 'Resetează',
'renamed_success' => 'Numele personalizat a fost salvat.',
'rename_cleared' => 'Numele personalizat a fost eliminat.',
+ 'personal_best' => 'Recordul tău personal',
+ 'no_personal_best' => 'Niciun set finalizat înregistrat încă. Înregistrează greutatea și repetările când bifezi un set într-o sesiune.',
],
'sessions' => [
@@ -211,7 +213,9 @@ return [
'rest_timer' => 'Cronometru pauză',
'start_rest' => 'Start',
'pause_rest' => 'Pauză',
- 'reset_rest' => 'Reset'
+ 'reset_rest' => 'Reset',
+ 'weight_lifted' => 'Greutate',
+ 'reps_done' => 'Repetări',
],
'dashboard' => [
diff --git a/resources/views/exercises/show.blade.php b/resources/views/exercises/show.blade.php
index 41dcac3..5120815 100644
--- a/resources/views/exercises/show.blade.php
+++ b/resources/views/exercises/show.blade.php
@@ -79,6 +79,22 @@
+ @auth
+
+
{{ __('app.exercises.personal_best') }}
+ @if($personalBest)
+
+
+ @if($personalBest->actual_reps)
+ × {{ $personalBest->actual_reps }} {{ __('app.sessions.reps') }}
+ @endif
+
+
{{ optional($personalBest->session)->completed_at?->format('M j, Y') ?? $personalBest->updated_at->format('M j, Y') }}
+ @else
+
{{ __('app.exercises.no_personal_best') }}
+ @endif
+
+ @endauth
Exercise Info
diff --git a/resources/views/workouts/session.blade.php b/resources/views/workouts/session.blade.php
index b1f3d84..8885f6d 100644
--- a/resources/views/workouts/session.blade.php
+++ b/resources/views/workouts/session.blade.php
@@ -29,6 +29,8 @@
{{ $item->exercise->display_name }}
+
+
+
+
@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: {
diff --git a/tests/Feature/ExerciseTest.php b/tests/Feature/ExerciseTest.php
index 55c38c8..f9a0ef8 100644
--- a/tests/Feature/ExerciseTest.php
+++ b/tests/Feature/ExerciseTest.php
@@ -4,6 +4,11 @@ namespace Tests\Feature;
use App\Models\BodyPart;
use App\Models\Exercise;
+use App\Models\User;
+use App\Models\Workout;
+use App\Models\WorkoutExercise;
+use App\Models\WorkoutSession;
+use App\Models\WorkoutSessionItem;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@@ -73,4 +78,70 @@ class ExerciseTest extends TestCase
$response->assertNotFound();
}
+
+ public function test_show_displays_personal_best_from_completed_session_items(): void
+ {
+ $user = User::factory()->create();
+ $exercise = Exercise::factory()->create();
+ $workout = Workout::factory()->create(['user_id' => $user->id]);
+ $workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
+ $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+
+ WorkoutSessionItem::factory()->create([
+ 'session_id' => $session->id,
+ 'workout_exercise_id' => $workoutExercise->id,
+ 'done' => true,
+ 'actual_weight' => 60,
+ 'actual_reps' => 5,
+ ]);
+
+ $response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
+
+ $response->assertOk();
+ $response->assertSee('60 kg', false);
+ }
+
+ public function test_show_picks_the_heaviest_completed_set_across_sessions_as_personal_best(): void
+ {
+ $user = User::factory()->create();
+ $exercise = Exercise::factory()->create();
+ $workout = Workout::factory()->create(['user_id' => $user->id]);
+ $workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
+ $earlierSession = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+ $laterSession = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+
+ WorkoutSessionItem::factory()->create([
+ 'session_id' => $earlierSession->id, 'workout_exercise_id' => $workoutExercise->id,
+ 'done' => true, 'actual_weight' => 60, 'actual_reps' => 5,
+ ]);
+ WorkoutSessionItem::factory()->create([
+ 'session_id' => $laterSession->id, 'workout_exercise_id' => $workoutExercise->id,
+ 'done' => true, 'actual_weight' => 70, 'actual_reps' => 3,
+ ]);
+
+ $response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
+
+ $response->assertOk();
+ $response->assertSee('70 kg', false);
+ $response->assertDontSee('60 kg', false);
+ }
+
+ public function test_show_ignores_incomplete_sets_for_personal_best(): void
+ {
+ $user = User::factory()->create();
+ $exercise = Exercise::factory()->create();
+ $workout = Workout::factory()->create(['user_id' => $user->id]);
+ $workoutExercise = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'exercise_id' => $exercise->id]);
+ $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+
+ WorkoutSessionItem::factory()->create([
+ 'session_id' => $session->id, 'workout_exercise_id' => $workoutExercise->id,
+ 'done' => false, 'actual_weight' => 90, 'actual_reps' => 5,
+ ]);
+
+ $response = $this->actingAs($user)->get(route('exercises.show', $exercise->slug));
+
+ $response->assertOk();
+ $response->assertDontSee('90 kg', false);
+ }
}
diff --git a/tests/Feature/WorkoutSessionTest.php b/tests/Feature/WorkoutSessionTest.php
index 370246f..bca6a1b 100644
--- a/tests/Feature/WorkoutSessionTest.php
+++ b/tests/Feature/WorkoutSessionTest.php
@@ -93,6 +93,48 @@ class WorkoutSessionTest extends TestCase
]);
}
+ public function test_toggling_done_records_actual_weight_and_reps(): void
+ {
+ $user = User::factory()->create();
+ $workout = Workout::factory()->create(['user_id' => $user->id]);
+ $item = WorkoutExercise::factory()->create(['workout_id' => $workout->id]);
+ $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+
+ $this->actingAs($user)->patch(route('sessions.items.toggle', [$session, $item]), [
+ 'actual_weight' => 62.5,
+ 'actual_reps' => 8,
+ ]);
+
+ $this->assertDatabaseHas('workout_session_items', [
+ 'session_id' => $session->id,
+ 'workout_exercise_id' => $item->id,
+ 'done' => true,
+ 'actual_weight' => 62.5,
+ 'actual_reps' => 8,
+ ]);
+ }
+
+ public function test_untoggling_does_not_send_actual_weight(): void
+ {
+ $user = User::factory()->create();
+ $workout = Workout::factory()->create(['user_id' => $user->id]);
+ $item = WorkoutExercise::factory()->create(['workout_id' => $workout->id]);
+ $session = WorkoutSession::factory()->create(['user_id' => $user->id, 'workout_id' => $workout->id]);
+
+ $this->actingAs($user)->patch(route('sessions.items.toggle', [$session, $item]), [
+ 'actual_weight' => 62.5,
+ 'actual_reps' => 8,
+ ]);
+ $this->actingAs($user)->patch(route('sessions.items.toggle', [$session, $item]));
+
+ $this->assertDatabaseHas('workout_session_items', [
+ 'session_id' => $session->id,
+ 'workout_exercise_id' => $item->id,
+ 'done' => false,
+ 'actual_weight' => 62.5,
+ ]);
+ }
+
public function test_owner_can_complete_a_session(): void
{
$user = User::factory()->create();