diff --git a/functional/exercises/src/Http/Controllers/ExercisesController.php b/functional/exercises/src/Http/Controllers/ExercisesController.php index e26017d..50a88a2 100644 --- a/functional/exercises/src/Http/Controllers/ExercisesController.php +++ b/functional/exercises/src/Http/Controllers/ExercisesController.php @@ -24,7 +24,12 @@ class ExercisesController $exercises = Exercise::query() ->search($request->query('search')) ->when($request->query('body_part'), fn ($query, $bodyPart) => $query->where('body_part', $bodyPart)) - ->when($request->query('equipment'), fn ($query, $equipment) => $query->where('equipment', $equipment)) + ->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') diff --git a/functional/tracking/src/Http/Controllers/SessionsController.php b/functional/tracking/src/Http/Controllers/SessionsController.php index c199b0e..ff1742d 100644 --- a/functional/tracking/src/Http/Controllers/SessionsController.php +++ b/functional/tracking/src/Http/Controllers/SessionsController.php @@ -34,10 +34,12 @@ class SessionsController ->setStatusCode(Response::HTTP_CREATED); } - /** GET /api/sessions/{session} — séance + setLogs. */ + /** GET /api/sessions/{session} — séance + setLogs + exercices planifiés de la routine. */ public function show(int $session): WorkoutSessionResource { - return new WorkoutSessionResource($this->findForUser($session)->load('setLogs')); + return new WorkoutSessionResource( + $this->findForUser($session)->load(['setLogs', 'routine.routineExercises.exercise']) + ); } /** POST /api/sessions/{session}/sets — log un set. */ diff --git a/functional/tracking/src/Http/Resources/WorkoutSessionResource.php b/functional/tracking/src/Http/Resources/WorkoutSessionResource.php index 1fb6628..72587fb 100644 --- a/functional/tracking/src/Http/Resources/WorkoutSessionResource.php +++ b/functional/tracking/src/Http/Resources/WorkoutSessionResource.php @@ -2,11 +2,14 @@ namespace Functional\Tracking\Http\Resources; +use Functional\Exercises\Models\Exercise; +use Functional\Routines\Models\RoutineExercise; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; /** - * Une séance d'entraînement, avec ses séries (setLogs) quand elles sont chargées. + * Une séance d'entraînement, avec ses séries (setLogs) quand elles sont chargées, et — pour une + * séance issue d'une routine — les exercices planifiés (prêts-à-logger, avec séries/reps cibles). */ class WorkoutSessionResource extends JsonResource { @@ -23,6 +26,42 @@ class WorkoutSessionResource extends JsonResource 'notes' => $this->notes, 'set_count' => $this->when($this->set_logs_count !== null, $this->set_logs_count), 'set_logs' => SetLogResource::collection($this->whenLoaded('setLogs')), + 'planned_exercises' => $this->plannedExercises($request), ]; } + + /** + * Exercices prévus par la routine (dans l'ordre), pour pré-remplir la séance dès son démarrage. + * Vide pour une séance ad hoc (sans routine). + */ + private function plannedExercises(Request $request): array + { + if (! $this->relationLoaded('routine') || $this->routine === null) { + return []; + } + + $locale = (string) ($request->query('locale') ?: 'fr'); + + return $this->routine->routineExercises + ->map(function (RoutineExercise $routineExercise) use ($locale) { + $exercise = $routineExercise->exercise; + + return [ + 'exercise_id' => $routineExercise->exercise_id, + 'position' => $routineExercise->position, + 'sets' => $routineExercise->sets, + 'target_reps' => $routineExercise->target_reps, + 'target_weight' => $routineExercise->target_weight, + 'rest_seconds' => $routineExercise->rest_seconds, + 'exercise' => $exercise ? [ + 'id' => $exercise->id, + 'name' => $exercise->localizedName($locale), + 'image_url' => $exercise->image_url, + 'target' => Exercise::localizeTerm($exercise->target, $locale), + ] : null, + ]; + }) + ->values() + ->all(); + } } diff --git a/functional/users/database/migrations/2026_07_21_000001_add_available_equipment_to_users.php b/functional/users/database/migrations/2026_07_21_000001_add_available_equipment_to_users.php new file mode 100644 index 0000000..44ff77f --- /dev/null +++ b/functional/users/database/migrations/2026_07_21_000001_add_available_equipment_to_users.php @@ -0,0 +1,26 @@ +json('available_equipment')->nullable()->after('timezone'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('available_equipment'); + }); + } +}; diff --git a/functional/users/routes/api.php b/functional/users/routes/api.php index 0ff944e..300683f 100644 --- a/functional/users/routes/api.php +++ b/functional/users/routes/api.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Route; // Le préfixe api/ est ajouté par UsersServiceProvider::withRouting(). Route::middleware('auth:sanctum')->group(function () { Route::get('me', [AuthController::class, 'me']); + Route::patch('me', [AuthController::class, 'update']); // Web Push (rappels de série pilotés par le back) Route::post('push/subscribe', [PushController::class, 'subscribe']); diff --git a/functional/users/src/Http/Controllers/AuthController.php b/functional/users/src/Http/Controllers/AuthController.php index 189009f..60ded3c 100644 --- a/functional/users/src/Http/Controllers/AuthController.php +++ b/functional/users/src/Http/Controllers/AuthController.php @@ -5,6 +5,7 @@ namespace Functional\Users\Http\Controllers; use Functional\Streaks\Models\Streak; use Functional\Users\Http\Requests\LoginRequest; use Functional\Users\Http\Requests\RegisterRequest; +use Functional\Users\Http\Requests\UpdateProfileRequest; use Functional\Users\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -79,6 +80,7 @@ class AuthController 'units' => $user->units, 'timezone' => $user->timezone, 'avatar_url' => $this->avatarUrl($user->avatar_path), + 'available_equipment' => $user->available_equipment ?? [], ], 'streak' => [ 'current' => $streak->current_count, @@ -92,6 +94,17 @@ class AuthController ]); } + /** + * PATCH /api/me — met à jour les préférences du profil courant (matériel possédé, locale, unités…). + * Renvoie la vue /api/me rafraîchie. + */ + public function update(UpdateProfileRequest $request): JsonResponse + { + $request->user()->update($request->validated()); + + return $this->me($request); + } + /** * Résout l'URL publique d'un média stocké sur RustFS/S3 à partir de son chemin relatif. */ diff --git a/functional/users/src/Http/Requests/UpdateProfileRequest.php b/functional/users/src/Http/Requests/UpdateProfileRequest.php new file mode 100644 index 0000000..a2f6c7e --- /dev/null +++ b/functional/users/src/Http/Requests/UpdateProfileRequest.php @@ -0,0 +1,34 @@ +user() !== null; + } + + /** + * @return array|string> + */ + public function rules(): array + { + return [ + 'locale' => ['sometimes', 'string', Rule::in(['fr', 'en', 'pt-BR'])], + 'units' => ['sometimes', Rule::enum(Units::class)], + 'timezone' => ['sometimes', 'nullable', 'string', 'max:64'], + 'available_equipment' => ['sometimes', 'array'], + 'available_equipment.*' => ['string', 'max:64'], + ]; + } +} diff --git a/functional/users/src/Models/User.php b/functional/users/src/Models/User.php index c62f699..de46666 100644 --- a/functional/users/src/Models/User.php +++ b/functional/users/src/Models/User.php @@ -23,8 +23,9 @@ use Spatie\Permission\Traits\HasRoles; * @property string|null $timezone * @property string|null $avatar_path * @property string|null $oidc_sub + * @property array|null $available_equipment */ -#[Fillable(['name', 'email', 'password', 'locale', 'units', 'timezone', 'avatar_path', 'oidc_sub'])] +#[Fillable(['name', 'email', 'password', 'locale', 'units', 'timezone', 'avatar_path', 'oidc_sub', 'available_equipment'])] #[Hidden(['password', 'remember_token'])] #[UseFactory(UserFactory::class)] class User extends Authenticatable implements FilamentUser @@ -48,6 +49,7 @@ class User extends Authenticatable implements FilamentUser 'email_verified_at' => 'datetime', 'password' => 'hashed', 'units' => Units::class, + 'available_equipment' => 'array', ]; } }