diff --git a/app/Http/Controllers/TrainerController.php b/app/Http/Controllers/TrainerController.php
new file mode 100644
index 0000000..6322bce
--- /dev/null
+++ b/app/Http/Controllers/TrainerController.php
@@ -0,0 +1,92 @@
+clients()
+ ->with(['workouts' => function ($q) use ($trainer) {
+ $q->where('assigned_by_trainer_id', $trainer->id)->latest();
+ }])
+ ->orderBy('name')
+ ->get();
+
+ return view('trainer.clients', compact('clients'));
+ }
+
+ public function addClient(Request $request): RedirectResponse
+ {
+ $trainer = Auth::user();
+ $data = $request->validate([
+ 'email' => ['required', 'email', 'exists:users,email'],
+ ]);
+
+ $client = User::where('email', $data['email'])->firstOrFail();
+
+ if ($client->id === $trainer->id) {
+ return back()->withErrors(['email' => 'You cannot add yourself as a client.']);
+ }
+
+ if ($trainer->clients()->where('client_id', $client->id)->exists()) {
+ return back()->withErrors(['email' => 'This person is already one of your clients.']);
+ }
+
+ $trainer->clients()->attach($client->id);
+
+ return back()->with('status', 'Client added');
+ }
+
+ public function removeClient(User $client): RedirectResponse
+ {
+ Auth::user()->clients()->detach($client->id);
+
+ return back()->with('status', 'Client removed');
+ }
+
+ public function createWorkout(User $client): View
+ {
+ $this->authorizeClient($client);
+
+ return view('trainer.assign-workout', compact('client'));
+ }
+
+ public function storeWorkout(Request $request, User $client): RedirectResponse
+ {
+ $this->authorizeClient($client);
+
+ $data = $request->validate([
+ 'title' => ['required', 'string', 'max:255'],
+ 'notes' => ['nullable', 'string'],
+ 'estimated_minutes' => ['nullable', 'integer', 'min:1', 'max:600'],
+ ]);
+
+ $workout = Workout::create([
+ 'user_id' => $client->id,
+ 'assigned_by_trainer_id' => Auth::id(),
+ 'title' => $data['title'],
+ 'notes' => $data['notes'] ?? null,
+ 'estimated_minutes' => $data['estimated_minutes'] ?? null,
+ ]);
+
+ return redirect()->route('trainer.clients')->with('status', "Workout assigned to {$client->name}");
+ }
+
+ protected function authorizeClient(User $client): void
+ {
+ if (!Auth::user()->clients()->where('client_id', $client->id)->exists()) {
+ abort(403);
+ }
+ }
+}
diff --git a/app/Http/Controllers/WorkoutController.php b/app/Http/Controllers/WorkoutController.php
index 9718847..cb6a8c5 100644
--- a/app/Http/Controllers/WorkoutController.php
+++ b/app/Http/Controllers/WorkoutController.php
@@ -17,7 +17,7 @@ class WorkoutController extends Controller
public function index()
{
- $workouts = Auth::user()->workouts()->latest()->paginate(10);
+ $workouts = Auth::user()->workouts()->with('assignedByTrainer')->latest()->paginate(10);
return view('workouts.index', compact('workouts'));
}
@@ -41,7 +41,7 @@ class WorkoutController extends Controller
public function show(Workout $workout)
{
$this->authorizeWorkout($workout);
- $workout->load(['items.exercise']);
+ $workout->load(['items.exercise', 'assignedByTrainer']);
$exercises = Exercise::orderBy('name')->limit(20)->get(['id','name','slug']);
return view('workouts.show', compact('workout', 'exercises'));
}
@@ -112,15 +112,25 @@ class WorkoutController extends Controller
public function destroy(Workout $workout)
{
- $this->authorizeWorkout($workout);
+ // Deletion stays strictly owner-only, even for a trainer-assigned workout.
+ if ($workout->user_id !== Auth::id()) {
+ abort(403);
+ }
$workout->delete();
return redirect()->route('workouts.index')->with('status', 'Workout deleted');
}
+ // Allows the owner, or the trainer who assigned this workout, to view and edit it.
+ // Session/completion data is intentionally not exposed to the trainer here - only
+ // the workout structure they authored.
protected function authorizeWorkout(Workout $workout): void
{
- if ($workout->user_id !== Auth::id()) {
- abort(403);
+ if ($workout->user_id === Auth::id()) {
+ return;
}
+ if ($workout->assigned_by_trainer_id && $workout->assigned_by_trainer_id === Auth::id()) {
+ return;
+ }
+ abort(403);
}
}
diff --git a/app/Http/Middleware/Trainer.php b/app/Http/Middleware/Trainer.php
new file mode 100644
index 0000000..25f48ab
--- /dev/null
+++ b/app/Http/Middleware/Trainer.php
@@ -0,0 +1,25 @@
+is_trainer) {
+ abort(403);
+ }
+ return $next($request);
+ }
+}
diff --git a/app/Models/User.php b/app/Models/User.php
index 9b3f514..f317748 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -74,6 +74,22 @@ class User extends Authenticatable
return $this->hasMany(Program::class);
}
+ // Trainer/client relationships
+ public function clients()
+ {
+ return $this->belongsToMany(User::class, 'trainer_clients', 'trainer_id', 'client_id')->withTimestamps();
+ }
+
+ public function trainers()
+ {
+ return $this->belongsToMany(User::class, 'trainer_clients', 'client_id', 'trainer_id')->withTimestamps();
+ }
+
+ public function assignedWorkouts()
+ {
+ return $this->hasMany(Workout::class, 'assigned_by_trainer_id');
+ }
+
public function favoriteExercises()
{
return $this->belongsToMany(Exercise::class, 'exercise_favorites')->withTimestamps();
diff --git a/app/Models/Workout.php b/app/Models/Workout.php
index ec79d2a..ba82263 100644
--- a/app/Models/Workout.php
+++ b/app/Models/Workout.php
@@ -20,6 +20,7 @@ class Workout extends Model
'program_id',
'week_number',
'day_number',
+ 'assigned_by_trainer_id',
];
protected $casts = [
@@ -38,6 +39,11 @@ class Workout extends Model
return $this->belongsTo(Program::class);
}
+ public function assignedByTrainer(): BelongsTo
+ {
+ return $this->belongsTo(User::class, 'assigned_by_trainer_id');
+ }
+
public function items(): HasMany
{
return $this->hasMany(WorkoutExercise::class)->orderBy('position');
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 8e56a9d..226602a 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -17,6 +17,7 @@ return Application::configure(basePath: dirname(__DIR__))
]);
$middleware->alias([
'admin' => \App\Http\Middleware\Admin::class,
+ 'trainer' => \App\Http\Middleware\Trainer::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
diff --git a/database/migrations/2026_08_10_092220_add_assigned_by_trainer_to_workouts_table.php b/database/migrations/2026_08_10_092220_add_assigned_by_trainer_to_workouts_table.php
new file mode 100644
index 0000000..8375d1b
--- /dev/null
+++ b/database/migrations/2026_08_10_092220_add_assigned_by_trainer_to_workouts_table.php
@@ -0,0 +1,28 @@
+foreignId('assigned_by_trainer_id')->nullable()->after('day_number')->constrained('users')->nullOnDelete();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('workouts', function (Blueprint $table) {
+ $table->dropConstrainedForeignId('assigned_by_trainer_id');
+ });
+ }
+};
diff --git a/database/migrations/2026_08_10_092220_create_trainer_clients_table.php b/database/migrations/2026_08_10_092220_create_trainer_clients_table.php
new file mode 100644
index 0000000..0ce5d35
--- /dev/null
+++ b/database/migrations/2026_08_10_092220_create_trainer_clients_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->foreignId('trainer_id')->constrained('users')->cascadeOnDelete();
+ $table->foreignId('client_id')->constrained('users')->cascadeOnDelete();
+ $table->timestamps();
+
+ $table->unique(['trainer_id', 'client_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('trainer_clients');
+ }
+};
diff --git a/lang/en/app.php b/lang/en/app.php
index 1c69435..dcdb373 100644
--- a/lang/en/app.php
+++ b/lang/en/app.php
@@ -9,6 +9,7 @@ return [
'build_workout' => 'Build a Workout',
'dashboard' => 'Dashboard',
'programs' => 'Programs',
+ 'clients' => 'Clients',
'admin' => 'Admin',
'profile' => 'Profile',
'logout' => 'Log Out',
diff --git a/lang/ro/app.php b/lang/ro/app.php
index 4e8de8a..5084f64 100644
--- a/lang/ro/app.php
+++ b/lang/ro/app.php
@@ -9,6 +9,7 @@ return [
'build_workout' => 'Creează un antrenament',
'dashboard' => 'Tabloul de bord',
'programs' => 'Programe',
+ 'clients' => 'Clienți',
'admin' => 'Admin',
'profile' => 'Profil',
'logout' => 'Deconectare',
diff --git a/resources/views/layouts/navigation.blade.php b/resources/views/layouts/navigation.blade.php
index 2b8b039..56021d5 100644
--- a/resources/views/layouts/navigation.blade.php
+++ b/resources/views/layouts/navigation.blade.php
@@ -20,6 +20,9 @@
@auth
{{ __('app.nav.dashboard') }}
{{ __('app.nav.programs') }}
+ @if(Auth::user()->is_trainer)
+ {{ __('app.nav.clients') }}
+ @endif
@if(Auth::user()->is_admin)
{{ __('app.nav.admin') }}
@endif
@@ -100,6 +103,9 @@
@auth
You'll be able to add exercises to it right after creating it. Add clients by the email they registered with, and assign them workouts. {{ $client->email }} No workouts assigned yet.Assign a Workout to {{ $client->name }}
+
+ @foreach ($errors->all() as $error)
+
+ My Clients
+
+ @foreach ($errors->all() as $error)
+
+ {{ $client->name }}
+
+ @foreach($client->workouts as $workout)
+
+ @endif
+
~ {{ $workout->estimated_minutes }} min
@endif diff --git a/resources/views/workouts/show.blade.php b/resources/views/workouts/show.blade.php index 325221a..f6b92cd 100644 --- a/resources/views/workouts/show.blade.php +++ b/resources/views/workouts/show.blade.php @@ -23,6 +23,9 @@ @endauth@if($workout->estimated_minutes) ~ {{ $workout->estimated_minutes }} min · @endif {{ $workout->items->count() }} exercises