Build & Deploy / build (push) Successful in 25s
- BUG: une séance issue d'une routine était vide -> WorkoutSessionResource expose planned_exercises (eager-load routine.routineExercises.exercise), pre-rempli cote front - profil: champ users.available_equipment (json), PATCH /api/me (UpdateProfileRequest), expose dans /api/me - exercises: filtre equipment accepte une liste CSV (whereIn) -> filtre 'mon matos' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
3.6 KiB
PHP
92 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace Functional\Exercises\Http\Controllers;
|
|
|
|
use Functional\Exercises\Http\Resources\ExerciseListResource;
|
|
use Functional\Exercises\Http\Resources\ExerciseResource;
|
|
use Functional\Exercises\Models\Exercise;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
|
|
|
/**
|
|
* Catalogue d'exercices : lecture seule et public, aucun scoping par utilisateur.
|
|
*/
|
|
class ExercisesController
|
|
{
|
|
private const DEFAULT_LOCALE = 'fr';
|
|
|
|
private const DEFAULT_PER_PAGE = 20;
|
|
|
|
/** GET /api/exercises — liste filtrable (search/body_part/equipment/target/category) et paginée. */
|
|
public function index(Request $request): AnonymousResourceCollection
|
|
{
|
|
$exercises = Exercise::query()
|
|
->search($request->query('search'))
|
|
->when($request->query('body_part'), fn ($query, $bodyPart) => $query->where('body_part', $bodyPart))
|
|
->when($request->query('equipment'), function ($query, $equipment) {
|
|
// Accepte une valeur unique ou une liste séparée par des virgules ("dumbbell,barbell")
|
|
// → filtre "mon matériel" (plusieurs équipements possédés).
|
|
$values = array_values(array_filter(is_array($equipment) ? $equipment : explode(',', (string) $equipment)));
|
|
$query->whereIn('equipment', $values);
|
|
})
|
|
->when($request->query('target'), fn ($query, $target) => $query->where('target', $target))
|
|
->when($request->query('category'), fn ($query, $category) => $query->where('category', $category))
|
|
->orderBy('name')
|
|
->paginate((int) $request->query('per_page', self::DEFAULT_PER_PAGE));
|
|
|
|
ExerciseListResource::$locale = $this->resolveLocale($request);
|
|
|
|
return ExerciseListResource::collection($exercises);
|
|
}
|
|
|
|
/** GET /api/exercises/{exercise} — détail avec instructions dans la locale demandée. */
|
|
public function show(Request $request, Exercise $exercise): ExerciseResource
|
|
{
|
|
return new ExerciseResource($exercise, $this->resolveLocale($request));
|
|
}
|
|
|
|
/**
|
|
* GET /api/exercises/facets — valeurs distinctes pour les filtres.
|
|
* Chaque valeur = { value: anglais (canonique, envoyé au filtre), label: traduit }.
|
|
*/
|
|
public function facets(Request $request): JsonResponse
|
|
{
|
|
$locale = $this->resolveLocale($request);
|
|
|
|
$facet = fn (string $column) => Exercise::query()
|
|
->whereNotNull($column)->distinct()->orderBy($column)->pluck($column)
|
|
->map(fn ($v) => ['value' => $v, 'label' => Exercise::localizeTerm($v, $locale)])
|
|
->values();
|
|
|
|
return response()->json([
|
|
'body_parts' => $facet('body_part'),
|
|
'equipments' => $facet('equipment'),
|
|
'targets' => $facet('target'),
|
|
'categories' => $facet('category'),
|
|
]);
|
|
}
|
|
|
|
/** Résout la locale demandée : paramètre `locale`, sinon header Accept-Language, sinon 'fr'. */
|
|
private function resolveLocale(Request $request): string
|
|
{
|
|
$locale = $request->query('locale');
|
|
|
|
if ($locale) {
|
|
return (string) $locale;
|
|
}
|
|
|
|
$acceptLanguage = $request->header('Accept-Language');
|
|
|
|
if (! $acceptLanguage) {
|
|
return self::DEFAULT_LOCALE;
|
|
}
|
|
|
|
// Le header peut lister plusieurs langues pondérées ("fr-FR,fr;q=0.9,en;q=0.8") : on ne garde que la première.
|
|
$firstLanguage = strtok($acceptLanguage, ',');
|
|
$languageTag = strtok($firstLanguage, ';');
|
|
|
|
return $languageTag !== false ? $languageTag : self::DEFAULT_LOCALE;
|
|
}
|
|
}
|