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>
128 lines
5.4 KiB
PHP
128 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
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;
|
|
|
|
class ExerciseController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$q = trim((string) $request->query('q', ''));
|
|
$bodypart = (string) $request->query('bodypart', '');
|
|
$muscle = (string) $request->query('muscle', '');
|
|
$equipment = (string) $request->query('equipment', '');
|
|
$perPage = (int) ($request->query('per_page', 12));
|
|
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
|
|
|
$query = Exercise::query()
|
|
->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']
|
|
+ (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : []))
|
|
->when($q !== '', function ($qBuilder) use ($q) {
|
|
$qBuilder->where(function ($sub) use ($q) {
|
|
$sub->where('name', 'like', "%{$q}%")
|
|
->orWhere('instructions', 'like', "%{$q}%");
|
|
});
|
|
})
|
|
->when($bodypart !== '', function ($qBuilder) use ($bodypart) {
|
|
$qBuilder->whereHas('bodyParts', function ($rel) use ($bodypart) {
|
|
$rel->where('slug', $bodypart);
|
|
});
|
|
})
|
|
->when($muscle !== '', function ($qBuilder) use ($muscle) {
|
|
$qBuilder->where(function ($sub) use ($muscle) {
|
|
$sub->whereHas('targetMuscles', function ($rel) use ($muscle) { $rel->where('slug', $muscle); })
|
|
->orWhereHas('secondaryMuscles', function ($rel) use ($muscle) { $rel->where('slug', $muscle); });
|
|
});
|
|
})
|
|
->when($equipment !== '', function ($qBuilder) use ($equipment) {
|
|
$qBuilder->whereHas('equipments', function ($rel) use ($equipment) {
|
|
$rel->where('slug', $equipment);
|
|
});
|
|
})
|
|
->orderBy('name');
|
|
|
|
$exercises = $query->paginate($perPage)->withQueryString();
|
|
|
|
$bodyParts = BodyPart::query()->orderBy('name')->get(['id','name','slug']);
|
|
$muscles = Muscle::query()->orderBy('name')->get(['id','name','slug']);
|
|
$equipments = Equipment::query()->orderBy('name')->get(['id','name','slug']);
|
|
|
|
return view('exercises.index', [
|
|
'exercises' => $exercises,
|
|
'filters' => [
|
|
'q' => $q,
|
|
'bodypart' => $bodypart,
|
|
'muscle' => $muscle,
|
|
'equipment' => $equipment,
|
|
'per_page' => $perPage,
|
|
],
|
|
'bodyParts' => $bodyParts,
|
|
'muscles' => $muscles,
|
|
'equipments' => $equipments,
|
|
]);
|
|
}
|
|
|
|
public function show(Exercise $exercise)
|
|
{
|
|
$exercise->load(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'secondaryMuscles:id,name,slug', 'equipments:id,name,slug']
|
|
+ (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : []));
|
|
|
|
// Find similar exercises by shared target muscles, body parts, or equipment
|
|
$targetIds = $exercise->targetMuscles->pluck('id')->all();
|
|
$bodyPartIds = $exercise->bodyParts->pluck('id')->all();
|
|
$equipmentIds = $exercise->equipments->pluck('id')->all();
|
|
|
|
$similarQuery = Exercise::query()
|
|
->with(['bodyParts:id,name,slug', 'targetMuscles:id,name,slug', 'equipments:id,name,slug']
|
|
+ (Auth::check() ? ['aliases' => function($rel){ $rel->where('user_id', Auth::id()); }] : []))
|
|
->where('id', '!=', $exercise->id)
|
|
->where(function ($q) use ($targetIds, $bodyPartIds, $equipmentIds) {
|
|
if (!empty($targetIds)) {
|
|
$q->orWhereHas('targetMuscles', function ($rel) use ($targetIds) {
|
|
$rel->whereIn('id', $targetIds);
|
|
});
|
|
}
|
|
if (!empty($bodyPartIds)) {
|
|
$q->orWhereHas('bodyParts', function ($rel) use ($bodyPartIds) {
|
|
$rel->whereIn('id', $bodyPartIds);
|
|
});
|
|
}
|
|
if (!empty($equipmentIds)) {
|
|
$q->orWhereHas('equipments', function ($rel) use ($equipmentIds) {
|
|
$rel->whereIn('id', $equipmentIds);
|
|
});
|
|
}
|
|
})
|
|
->orderBy('name')
|
|
->limit(9);
|
|
|
|
$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,
|
|
]);
|
|
}
|
|
}
|