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:
@@ -6,6 +6,7 @@ use App\Models\BodyPart;
|
|||||||
use App\Models\Equipment;
|
use App\Models\Equipment;
|
||||||
use App\Models\Exercise;
|
use App\Models\Exercise;
|
||||||
use App\Models\Muscle;
|
use App\Models\Muscle;
|
||||||
|
use App\Models\WorkoutSessionItem;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
@@ -104,9 +105,23 @@ class ExerciseController extends Controller
|
|||||||
|
|
||||||
$similarExercises = $similarQuery->get();
|
$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', [
|
return view('exercises.show', [
|
||||||
'exercise' => $exercise,
|
'exercise' => $exercise,
|
||||||
'similarExercises' => $similarExercises,
|
'similarExercises' => $similarExercises,
|
||||||
|
'personalBest' => $personalBest,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ class WorkoutSessionController extends Controller
|
|||||||
abort(404);
|
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([
|
$si = WorkoutSessionItem::firstOrCreate([
|
||||||
'session_id' => $session->id,
|
'session_id' => $session->id,
|
||||||
'workout_exercise_id' => $item->id,
|
'workout_exercise_id' => $item->id,
|
||||||
@@ -66,6 +71,14 @@ class WorkoutSessionController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$si->done = !$si->done;
|
$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();
|
$si->save();
|
||||||
|
|
||||||
return back();
|
return back();
|
||||||
|
|||||||
@@ -16,12 +16,16 @@ class WorkoutSessionItem extends Model
|
|||||||
'done',
|
'done',
|
||||||
'exercise_ms',
|
'exercise_ms',
|
||||||
'rest_ms',
|
'rest_ms',
|
||||||
|
'actual_weight',
|
||||||
|
'actual_reps',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'done' => 'boolean',
|
'done' => 'boolean',
|
||||||
'exercise_ms' => 'integer',
|
'exercise_ms' => 'integer',
|
||||||
'rest_ms' => 'integer',
|
'rest_ms' => 'integer',
|
||||||
|
'actual_weight' => 'decimal:2',
|
||||||
|
'actual_reps' => 'integer',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function session(): BelongsTo
|
public function session(): BelongsTo
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\WorkoutExercise;
|
||||||
|
use App\Models\WorkoutSession;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WorkoutSessionItem>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('workout_session_items', function (Blueprint $table) {
|
||||||
|
$table->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+5
-1
@@ -192,6 +192,8 @@ return [
|
|||||||
'rename_reset' => 'Reset',
|
'rename_reset' => 'Reset',
|
||||||
'renamed_success' => 'Your custom name has been saved.',
|
'renamed_success' => 'Your custom name has been saved.',
|
||||||
'rename_cleared' => 'Custom name cleared.',
|
'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' => [
|
'sessions' => [
|
||||||
@@ -211,7 +213,9 @@ return [
|
|||||||
'rest_timer' => 'Rest timer',
|
'rest_timer' => 'Rest timer',
|
||||||
'start_rest' => 'Start',
|
'start_rest' => 'Start',
|
||||||
'pause_rest' => 'Pause',
|
'pause_rest' => 'Pause',
|
||||||
'reset_rest' => 'Reset'
|
'reset_rest' => 'Reset',
|
||||||
|
'weight_lifted' => 'Weight',
|
||||||
|
'reps_done' => 'Reps',
|
||||||
],
|
],
|
||||||
|
|
||||||
'dashboard' => [
|
'dashboard' => [
|
||||||
|
|||||||
+5
-1
@@ -192,6 +192,8 @@ return [
|
|||||||
'rename_reset' => 'Resetează',
|
'rename_reset' => 'Resetează',
|
||||||
'renamed_success' => 'Numele personalizat a fost salvat.',
|
'renamed_success' => 'Numele personalizat a fost salvat.',
|
||||||
'rename_cleared' => 'Numele personalizat a fost eliminat.',
|
'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' => [
|
'sessions' => [
|
||||||
@@ -211,7 +213,9 @@ return [
|
|||||||
'rest_timer' => 'Cronometru pauză',
|
'rest_timer' => 'Cronometru pauză',
|
||||||
'start_rest' => 'Start',
|
'start_rest' => 'Start',
|
||||||
'pause_rest' => 'Pauză',
|
'pause_rest' => 'Pauză',
|
||||||
'reset_rest' => 'Reset'
|
'reset_rest' => 'Reset',
|
||||||
|
'weight_lifted' => 'Greutate',
|
||||||
|
'reps_done' => 'Repetări',
|
||||||
],
|
],
|
||||||
|
|
||||||
'dashboard' => [
|
'dashboard' => [
|
||||||
|
|||||||
@@ -79,6 +79,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lg:col-span-4">
|
<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">× {{ $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">
|
<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>
|
<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">
|
<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' }}">
|
<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
|
@csrf
|
||||||
@method('PATCH')
|
@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"
|
<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' }}"
|
aria-pressed="{{ $isDone ? 'true' : 'false' }}"
|
||||||
title="{{ $isDone ? __('app.sessions.mark_complete') : __('app.sessions.progress') }}">
|
title="{{ $isDone ? __('app.sessions.mark_complete') : __('app.sessions.progress') }}">
|
||||||
@@ -40,6 +42,17 @@
|
|||||||
</form>
|
</form>
|
||||||
<div>
|
<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="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">
|
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||||
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
|
@if($item->sets) {{ $item->sets }} {{ __('app.sessions.sets') }} @endif
|
||||||
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
|
@if($item->reps) · {{ $item->reps }} {{ __('app.sessions.reps') }} @endif
|
||||||
@@ -130,6 +143,17 @@
|
|||||||
const next = !wasDone;
|
const next = !wasDone;
|
||||||
updateBtnState(btn, next);
|
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, {
|
fetch(form.action, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ namespace Tests\Feature;
|
|||||||
|
|
||||||
use App\Models\BodyPart;
|
use App\Models\BodyPart;
|
||||||
use App\Models\Exercise;
|
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 Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -73,4 +78,70 @@ class ExerciseTest extends TestCase
|
|||||||
|
|
||||||
$response->assertNotFound();
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
public function test_owner_can_complete_a_session(): void
|
||||||
{
|
{
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|||||||
Reference in New Issue
Block a user