progress
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function register(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
'is_trainer' => ['nullable', 'boolean'],
|
||||
'age' => ['nullable', 'integer', 'min:5', 'max:120'],
|
||||
'weight_kg' => ['nullable', 'numeric', 'min:20', 'max:400'],
|
||||
'height_cm' => ['nullable', 'numeric', 'min:80', 'max:250'],
|
||||
'fitness_level' => ['nullable', 'in:beginner,intermediate,advanced'],
|
||||
'goal' => ['nullable', 'string', 'max:50'],
|
||||
'health_notes' => ['nullable', 'string', 'max:65535'],
|
||||
'available_equipment' => ['nullable', 'string', 'max:65535'],
|
||||
'days_per_week' => ['nullable', 'integer', 'min:1', 'max:7'],
|
||||
'minutes_per_session' => ['nullable', 'integer', 'min:5', 'max:600'],
|
||||
'device_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'is_trainer' => (bool) ($data['is_trainer'] ?? false),
|
||||
'age' => $data['age'] ?? null,
|
||||
'weight_kg' => $data['weight_kg'] ?? null,
|
||||
'height_cm' => $data['height_cm'] ?? null,
|
||||
'fitness_level' => $data['fitness_level'] ?? null,
|
||||
'goal' => $data['goal'] ?? null,
|
||||
'health_notes' => $data['health_notes'] ?? null,
|
||||
'available_equipment' => $data['available_equipment'] ?? null,
|
||||
'days_per_week' => $data['days_per_week'] ?? null,
|
||||
'minutes_per_session' => $data['minutes_per_session'] ?? null,
|
||||
]);
|
||||
|
||||
event(new Registered($user));
|
||||
|
||||
$token = $user->createToken($data['device_name'])->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'user' => new UserResource($user),
|
||||
'token' => $token,
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
'device_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$user = User::where('email', $data['email'])->first();
|
||||
|
||||
if (!$user || !Hash::check($data['password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'email' => ['The provided credentials are incorrect.'],
|
||||
]);
|
||||
}
|
||||
|
||||
$token = $user->createToken($data['device_name'])->plainTextToken;
|
||||
|
||||
return response()->json([
|
||||
'user' => new UserResource($user),
|
||||
'token' => $token,
|
||||
]);
|
||||
}
|
||||
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$request->user()->currentAccessToken()->delete();
|
||||
|
||||
return response()->json(['message' => 'Logged out']);
|
||||
}
|
||||
|
||||
public function me(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WeightLogResource;
|
||||
use App\Models\WorkoutSession;
|
||||
use App\Support\Gamification;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
$workoutsCount = $user->workouts()->count();
|
||||
$sessionsQuery = WorkoutSession::where('user_id', $user->id);
|
||||
$sessionsCount = (clone $sessionsQuery)->count();
|
||||
$completedSessions = (clone $sessionsQuery)->whereNotNull('completed_at')->count();
|
||||
$completionRate = $sessionsCount > 0 ? round(($completedSessions / $sessionsCount) * 100) : 0;
|
||||
|
||||
$recentSessions = (clone $sessionsQuery)
|
||||
->latest('started_at')
|
||||
->latest('id')
|
||||
->with(['workout:id,title', 'items:id,session_id,done'])
|
||||
->limit(5)
|
||||
->get()
|
||||
->map(function ($s) {
|
||||
$total = $s->items->count();
|
||||
$done = $s->items->where('done', true)->count();
|
||||
$pct = $total > 0 ? (int) round(($done / $total) * 100) : 0;
|
||||
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'workout_title' => optional($s->workout)->title ?? 'Workout',
|
||||
'started_at' => $s->started_at,
|
||||
'completed_at' => $s->completed_at,
|
||||
'done' => $done,
|
||||
'total' => $total,
|
||||
'pct' => $pct,
|
||||
];
|
||||
});
|
||||
|
||||
$since = now()->subDays(29)->startOfDay();
|
||||
$dailyActivity = (clone $sessionsQuery)
|
||||
->where('created_at', '>=', $since)
|
||||
->get(['id', 'created_at'])
|
||||
->groupBy(fn ($s) => $s->created_at->format('Y-m-d'))
|
||||
->map->count();
|
||||
|
||||
$weightLogs = $user->weightLogs()->orderBy('logged_at')->get(['id', 'weight_kg', 'logged_at']);
|
||||
$weightChange = $weightLogs->count() >= 2
|
||||
? round($weightLogs->last()->weight_kg - $weightLogs->first()->weight_kg, 2)
|
||||
: null;
|
||||
|
||||
$gamification = Gamification::forUser($user);
|
||||
|
||||
return response()->json([
|
||||
'workouts_count' => $workoutsCount,
|
||||
'sessions_count' => $sessionsCount,
|
||||
'completed_sessions' => $completedSessions,
|
||||
'completion_rate' => $completionRate,
|
||||
'recent_sessions' => $recentSessions,
|
||||
'daily_activity' => $dailyActivity,
|
||||
'weight_logs' => WeightLogResource::collection($weightLogs),
|
||||
'weight_change' => $weightChange,
|
||||
'gamification' => $gamification,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$currentTokenId = $request->user()->currentAccessToken()?->id;
|
||||
|
||||
$tokens = $request->user()->tokens()->orderByDesc('last_used_at')->get()->map(fn ($token) => [
|
||||
'id' => $token->id,
|
||||
'name' => $token->name,
|
||||
'is_current_device' => $token->id === $currentTokenId,
|
||||
'last_used_at' => $token->last_used_at,
|
||||
'created_at' => $token->created_at,
|
||||
]);
|
||||
|
||||
return response()->json(['data' => $tokens]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $tokenId): JsonResponse
|
||||
{
|
||||
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
|
||||
|
||||
if (!$deleted) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Device signed out']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class EmailVerificationController extends Controller
|
||||
{
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return response()->json(['message' => 'Email already verified.']);
|
||||
}
|
||||
|
||||
$request->user()->sendEmailVerificationNotification();
|
||||
|
||||
return response()->json(['message' => 'Verification link sent.']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\ExerciseAlias;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExerciseAliasController extends Controller
|
||||
{
|
||||
public function store(Request $request, Exercise $exercise): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$name = trim((string) ($data['name'] ?? ''));
|
||||
$userId = Auth::id();
|
||||
|
||||
if ($name === '') {
|
||||
ExerciseAlias::where('user_id', $userId)->where('exercise_id', $exercise->id)->delete();
|
||||
|
||||
return response()->json(['message' => __('app.exercises.rename_cleared'), 'name' => null]);
|
||||
}
|
||||
|
||||
ExerciseAlias::updateOrCreate(
|
||||
['user_id' => $userId, 'exercise_id' => $exercise->id],
|
||||
['name' => $name]
|
||||
);
|
||||
|
||||
return response()->json(['message' => __('app.exercises.renamed_success'), 'name' => $name]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ExerciseResource;
|
||||
use App\Models\BodyPart;
|
||||
use App\Models\Equipment;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Muscle;
|
||||
use App\Models\WorkoutSessionItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ExerciseController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$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();
|
||||
|
||||
return response()->json([
|
||||
'data' => ExerciseResource::collection($exercises->items()),
|
||||
'meta' => [
|
||||
'current_page' => $exercises->currentPage(),
|
||||
'last_page' => $exercises->lastPage(),
|
||||
'per_page' => $exercises->perPage(),
|
||||
'total' => $exercises->total(),
|
||||
],
|
||||
'filters' => [
|
||||
'body_parts' => BodyPart::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'muscles' => Muscle::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'equipments' => Equipment::orderBy('name')->get(['id', 'name', 'slug']),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Exercise $exercise): JsonResponse
|
||||
{
|
||||
$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()); }] : []));
|
||||
|
||||
$targetIds = $exercise->targetMuscles->pluck('id')->all();
|
||||
$bodyPartIds = $exercise->bodyParts->pluck('id')->all();
|
||||
$equipmentIds = $exercise->equipments->pluck('id')->all();
|
||||
|
||||
$similarExercises = 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', fn ($rel) => $rel->whereIn('id', $targetIds));
|
||||
}
|
||||
if (!empty($bodyPartIds)) {
|
||||
$q->orWhereHas('bodyParts', fn ($rel) => $rel->whereIn('id', $bodyPartIds));
|
||||
}
|
||||
if (!empty($equipmentIds)) {
|
||||
$q->orWhereHas('equipments', fn ($rel) => $rel->whereIn('id', $equipmentIds));
|
||||
}
|
||||
})
|
||||
->orderBy('name')
|
||||
->limit(9)
|
||||
->get();
|
||||
|
||||
$personalBest = null;
|
||||
if (Auth::check()) {
|
||||
$personalBest = WorkoutSessionItem::query()
|
||||
->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 response()->json([
|
||||
'exercise' => new ExerciseResource($exercise),
|
||||
'similar_exercises' => ExerciseResource::collection($similarExercises),
|
||||
'personal_best' => $personalBest ? [
|
||||
'actual_weight' => $personalBest->actual_weight !== null ? (float) $personalBest->actual_weight : null,
|
||||
'actual_reps' => $personalBest->actual_reps,
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Workout;
|
||||
use App\Models\YogaPose;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class FavoriteController extends Controller
|
||||
{
|
||||
public function toggleExercise(Exercise $exercise): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteExercises()->where('exercise_id', $exercise->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteExercises()->detach($exercise->id);
|
||||
} else {
|
||||
$user->favoriteExercises()->attach($exercise->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
|
||||
public function toggleWorkout(Workout $workout): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteWorkouts()->where('workout_id', $workout->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteWorkouts()->detach($workout->id);
|
||||
} else {
|
||||
$user->favoriteWorkouts()->attach($workout->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
|
||||
public function toggleYogaPose(YogaPose $pose): JsonResponse
|
||||
{
|
||||
$user = Auth::user();
|
||||
$exists = $user->favoriteYogaPoses()->where('yoga_pose_id', $pose->id)->exists();
|
||||
if ($exists) {
|
||||
$user->favoriteYogaPoses()->detach($pose->id);
|
||||
} else {
|
||||
$user->favoriteYogaPoses()->attach($pose->id);
|
||||
}
|
||||
|
||||
return response()->json(['favorited' => !$exists]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\PasswordReset;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules;
|
||||
|
||||
class PasswordResetController extends Controller
|
||||
{
|
||||
public function forgotPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
]);
|
||||
|
||||
$status = Password::sendResetLink($request->only('email'));
|
||||
|
||||
return $status === Password::RESET_LINK_SENT
|
||||
? response()->json(['message' => __($status)])
|
||||
: response()->json(['message' => __($status)], 422);
|
||||
}
|
||||
|
||||
public function resetPassword(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'token' => ['required'],
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'confirmed', Rules\Password::defaults()],
|
||||
]);
|
||||
|
||||
$status = Password::reset(
|
||||
$request->only('email', 'password', 'password_confirmation', 'token'),
|
||||
function (User $user) use ($request) {
|
||||
$user->forceFill([
|
||||
'password' => Hash::make($request->password),
|
||||
'remember_token' => Str::random(60),
|
||||
])->save();
|
||||
|
||||
// A password reset often means "I lost my phone" - revoke every
|
||||
// existing API token rather than leaving old devices signed in.
|
||||
$user->tokens()->delete();
|
||||
|
||||
event(new PasswordReset($user));
|
||||
}
|
||||
);
|
||||
|
||||
return $status === Password::PASSWORD_RESET
|
||||
? response()->json(['message' => __($status)])
|
||||
: response()->json(['message' => __($status)], 422);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\ProfileUpdateRequest;
|
||||
use App\Http\Resources\UserResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
|
||||
public function update(ProfileUpdateRequest $request): JsonResponse
|
||||
{
|
||||
$request->user()->fill($request->validated());
|
||||
|
||||
if ($request->user()->isDirty('email')) {
|
||||
$request->user()->email_verified_at = null;
|
||||
}
|
||||
|
||||
$request->user()->save();
|
||||
|
||||
return response()->json(['user' => new UserResource($request->user())]);
|
||||
}
|
||||
|
||||
public function destroy(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'password' => ['required', 'current_password'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$user->tokens()->delete();
|
||||
$user->delete();
|
||||
|
||||
return response()->json(['message' => 'Account deleted']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ProgramResource;
|
||||
use App\Models\Program;
|
||||
use App\Models\WorkoutSession;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class ProgramController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$programs = Auth::user()->programs()->withCount('workouts')->latest()->paginate(10);
|
||||
|
||||
return response()->json([
|
||||
'data' => ProgramResource::collection($programs->items()),
|
||||
'meta' => [
|
||||
'current_page' => $programs->currentPage(),
|
||||
'last_page' => $programs->lastPage(),
|
||||
'per_page' => $programs->perPage(),
|
||||
'total' => $programs->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'weeks' => ['required', 'integer', 'min:1', 'max:52'],
|
||||
]);
|
||||
|
||||
$program = Auth::user()->programs()->create($data);
|
||||
|
||||
return response()->json(['program' => new ProgramResource($program)], 201);
|
||||
}
|
||||
|
||||
public function show(Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$program->load('workouts');
|
||||
|
||||
$completedWorkoutIds = WorkoutSession::query()
|
||||
->where('user_id', Auth::id())
|
||||
->whereIn('workout_id', $program->workouts->pluck('id'))
|
||||
->whereNotNull('completed_at')
|
||||
->pluck('workout_id')
|
||||
->unique();
|
||||
|
||||
$totalWorkouts = $program->workouts->count();
|
||||
$completedCount = $completedWorkoutIds->count();
|
||||
$progressPct = $totalWorkouts > 0 ? (int) round(($completedCount / $totalWorkouts) * 100) : 0;
|
||||
|
||||
return response()->json([
|
||||
'program' => new ProgramResource($program),
|
||||
'completed_workout_ids' => $completedWorkoutIds->values(),
|
||||
'progress_pct' => $progressPct,
|
||||
'completed_count' => $completedCount,
|
||||
'total_workouts' => $totalWorkouts,
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$program->delete();
|
||||
|
||||
return response()->json(['message' => 'Program deleted']);
|
||||
}
|
||||
|
||||
public function addWorkout(Request $request, Program $program): JsonResponse
|
||||
{
|
||||
$this->authorizeProgram($program);
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'week_number' => ['required', 'integer', 'min:1', 'max:'.$program->weeks],
|
||||
'day_number' => ['required', 'integer', 'min:1', 'max:14'],
|
||||
'estimated_minutes' => ['nullable', 'integer', 'min:1', 'max:600'],
|
||||
]);
|
||||
|
||||
$workout = $program->workouts()->create([
|
||||
'user_id' => Auth::id(),
|
||||
'title' => $data['title'],
|
||||
'estimated_minutes' => $data['estimated_minutes'] ?? null,
|
||||
'week_number' => $data['week_number'],
|
||||
'day_number' => $data['day_number'],
|
||||
]);
|
||||
|
||||
return response()->json(['workout_id' => $workout->id, 'message' => 'Workout added to program'], 201);
|
||||
}
|
||||
|
||||
protected function authorizeProgram(Program $program): void
|
||||
{
|
||||
if ($program->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Http\Resources\WorkoutResource;
|
||||
use App\Models\User;
|
||||
use App\Models\Workout;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class TrainerController extends Controller
|
||||
{
|
||||
public function clients(): JsonResponse
|
||||
{
|
||||
$trainer = Auth::user();
|
||||
$clients = $trainer->clients()
|
||||
->with(['workouts' => function ($q) use ($trainer) {
|
||||
$q->where('assigned_by_trainer_id', $trainer->id)->latest();
|
||||
}])
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'data' => $clients->map(fn ($client) => [
|
||||
'client' => new UserResource($client),
|
||||
'assigned_workouts' => WorkoutResource::collection($client->workouts),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function addClient(Request $request): JsonResponse
|
||||
{
|
||||
$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 response()->json(['message' => 'You cannot add yourself as a client.'], 422);
|
||||
}
|
||||
|
||||
if ($trainer->clients()->where('client_id', $client->id)->exists()) {
|
||||
return response()->json(['message' => 'This person is already one of your clients.'], 422);
|
||||
}
|
||||
|
||||
$trainer->clients()->attach($client->id);
|
||||
|
||||
return response()->json(['message' => 'Client added', 'client' => new UserResource($client)], 201);
|
||||
}
|
||||
|
||||
public function removeClient(User $client): JsonResponse
|
||||
{
|
||||
Auth::user()->clients()->detach($client->id);
|
||||
|
||||
return response()->json(['message' => 'Client removed']);
|
||||
}
|
||||
|
||||
public function storeWorkout(Request $request, User $client): JsonResponse
|
||||
{
|
||||
$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 response()->json(['workout' => new WorkoutResource($workout)], 201);
|
||||
}
|
||||
|
||||
protected function authorizeClient(User $client): void
|
||||
{
|
||||
if (!Auth::user()->clients()->where('client_id', $client->id)->exists()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WeightLogResource;
|
||||
use App\Models\WeightLog;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WeightLogController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$logs = Auth::user()->weightLogs()->orderBy('logged_at')->get();
|
||||
|
||||
return response()->json(['data' => WeightLogResource::collection($logs)]);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'weight_kg' => ['required', 'numeric', 'min:20', 'max:400'],
|
||||
'logged_at' => ['nullable', 'date', 'before_or_equal:today'],
|
||||
]);
|
||||
|
||||
$loggedAt = $data['logged_at'] ?? now()->toDateString();
|
||||
|
||||
$entry = WeightLog::where('user_id', Auth::id())->whereDate('logged_at', $loggedAt)->first()
|
||||
?? new WeightLog(['user_id' => Auth::id(), 'logged_at' => $loggedAt]);
|
||||
$entry->weight_kg = $data['weight_kg'];
|
||||
$entry->save();
|
||||
|
||||
return response()->json(['weight_log' => new WeightLogResource($entry)], 201);
|
||||
}
|
||||
|
||||
public function destroy(WeightLog $weightLog): JsonResponse
|
||||
{
|
||||
if ($weightLog->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$weightLog->delete();
|
||||
|
||||
return response()->json(['message' => __('app.weight_log.deleted')]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\ExerciseResource;
|
||||
use App\Http\Resources\WorkoutExerciseResource;
|
||||
use App\Http\Resources\WorkoutResource;
|
||||
use App\Models\Exercise;
|
||||
use App\Models\Workout;
|
||||
use App\Models\WorkoutExercise;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkoutController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$workouts = Auth::user()->workouts()->with('assignedByTrainer')->latest()->paginate(10);
|
||||
|
||||
return $this->paginated($workouts);
|
||||
}
|
||||
|
||||
public function community(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$workouts = Workout::query()
|
||||
->where('is_public', true)
|
||||
->with(['user:id,name', 'items'])
|
||||
->when($q !== '', fn ($qb) => $qb->where('title', 'like', "%{$q}%"))
|
||||
->latest()
|
||||
->paginate(12)
|
||||
->withQueryString();
|
||||
|
||||
return $this->paginated($workouts);
|
||||
}
|
||||
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'title' => 'required|string|max:255',
|
||||
'notes' => 'nullable|string',
|
||||
'estimated_minutes' => 'nullable|integer|min:1|max:600',
|
||||
]);
|
||||
$workout = Auth::user()->workouts()->create($data);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)], 201);
|
||||
}
|
||||
|
||||
public function show(Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeView($workout);
|
||||
$workout->load(['items.exercise', 'assignedByTrainer', 'user:id,name']);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)]);
|
||||
}
|
||||
|
||||
public function searchExercises(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
|
||||
$exercises = Exercise::query()
|
||||
->with(['aliases' => fn ($rel) => $rel->where('user_id', Auth::id())])
|
||||
->when($q !== '', fn ($qb) => $qb->where('name', 'like', "%{$q}%"))
|
||||
->orderBy('name')
|
||||
->limit(20)
|
||||
->get(['id', 'name']);
|
||||
|
||||
return response()->json(ExerciseResource::collection($exercises));
|
||||
}
|
||||
|
||||
public function togglePublic(Workout $workout): JsonResponse
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
$workout->is_public = !$workout->is_public;
|
||||
$workout->save();
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($workout)]);
|
||||
}
|
||||
|
||||
public function copy(Workout $workout): JsonResponse
|
||||
{
|
||||
if (!$workout->is_public && $workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$workout->loadMissing('items');
|
||||
|
||||
$copy = Auth::user()->workouts()->create([
|
||||
'title' => $workout->title.' (copy)',
|
||||
'notes' => $workout->notes,
|
||||
'estimated_minutes' => $workout->estimated_minutes,
|
||||
]);
|
||||
|
||||
foreach ($workout->items as $item) {
|
||||
$copy->items()->create([
|
||||
'exercise_id' => $item->exercise_id,
|
||||
'position' => $item->position,
|
||||
'sets' => $item->sets,
|
||||
'reps' => $item->reps,
|
||||
'weight' => $item->weight,
|
||||
'rest_seconds' => $item->rest_seconds,
|
||||
'notes' => $item->notes,
|
||||
]);
|
||||
}
|
||||
|
||||
$copy->load(['items.exercise']);
|
||||
|
||||
return response()->json(['workout' => new WorkoutResource($copy)], 201);
|
||||
}
|
||||
|
||||
public function addExercise(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
$data = $request->validate([
|
||||
'exercise_id' => 'required|exists:exercises,id',
|
||||
'position' => 'nullable|integer|min:1',
|
||||
'sets' => 'nullable|integer|min:1|max:20',
|
||||
'reps' => 'nullable|integer|min:1|max:200',
|
||||
'weight' => 'nullable|numeric|min:0|max:999.99',
|
||||
'rest_seconds' => 'nullable|integer|min:0|max:600',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
$nextPos = ($workout->items()->max('position') ?? 0) + 1;
|
||||
$data['position'] = $data['position'] ?? $nextPos;
|
||||
$item = $workout->items()->create($data);
|
||||
$item->load('exercise');
|
||||
|
||||
return response()->json(['item' => new WorkoutExerciseResource($item)], 201);
|
||||
}
|
||||
|
||||
public function updateExercise(Request $request, Workout $workout, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
if ($item->workout_id !== $workout->id) {
|
||||
abort(404);
|
||||
}
|
||||
$data = $request->validate([
|
||||
'position' => 'nullable|integer|min:1',
|
||||
'sets' => 'nullable|integer|min:1|max:20',
|
||||
'reps' => 'nullable|integer|min:1|max:200',
|
||||
'weight' => 'nullable|numeric|min:0|max:999.99',
|
||||
'rest_seconds' => 'nullable|integer|min:0|max:600',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
$item->update($data);
|
||||
$item->load('exercise');
|
||||
|
||||
return response()->json(['item' => new WorkoutExerciseResource($item)]);
|
||||
}
|
||||
|
||||
public function removeExercise(Workout $workout, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
if ($item->workout_id !== $workout->id) {
|
||||
abort(404);
|
||||
}
|
||||
$item->delete();
|
||||
|
||||
return response()->json(['message' => 'Exercise removed']);
|
||||
}
|
||||
|
||||
public function destroy(Workout $workout): JsonResponse
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
$workout->delete();
|
||||
|
||||
return response()->json(['message' => 'Workout deleted']);
|
||||
}
|
||||
|
||||
protected function authorizeWorkout(Workout $workout): void
|
||||
{
|
||||
if ($workout->user_id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
if ($workout->assigned_by_trainer_id && $workout->assigned_by_trainer_id === Auth::id()) {
|
||||
return;
|
||||
}
|
||||
abort(403);
|
||||
}
|
||||
|
||||
protected function authorizeView(Workout $workout): void
|
||||
{
|
||||
if ($workout->is_public) {
|
||||
return;
|
||||
}
|
||||
$this->authorizeWorkout($workout);
|
||||
}
|
||||
|
||||
protected function paginated($paginator): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'data' => WorkoutResource::collection($paginator->items()),
|
||||
'meta' => [
|
||||
'current_page' => $paginator->currentPage(),
|
||||
'last_page' => $paginator->lastPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\WorkoutSessionResource;
|
||||
use App\Models\Workout;
|
||||
use App\Models\WorkoutExercise;
|
||||
use App\Models\WorkoutSession;
|
||||
use App\Models\WorkoutSessionItem;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class WorkoutSessionController extends Controller
|
||||
{
|
||||
public function store(Request $request, Workout $workout): JsonResponse
|
||||
{
|
||||
$this->authorizeWorkout($workout);
|
||||
|
||||
$session = WorkoutSession::create([
|
||||
'user_id' => Auth::id(),
|
||||
'workout_id' => $workout->id,
|
||||
'started_at' => now(),
|
||||
]);
|
||||
|
||||
$workout->loadMissing('items');
|
||||
foreach ($workout->items as $item) {
|
||||
WorkoutSessionItem::create([
|
||||
'session_id' => $session->id,
|
||||
'workout_exercise_id' => $item->id,
|
||||
'done' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)], 201);
|
||||
}
|
||||
|
||||
public function show(WorkoutSession $session): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)]);
|
||||
}
|
||||
|
||||
public function toggleItem(Request $request, WorkoutSession $session, WorkoutExercise $item): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
if ($item->workout_id !== $session->workout_id) {
|
||||
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([
|
||||
'session_id' => $session->id,
|
||||
'workout_exercise_id' => $item->id,
|
||||
], [
|
||||
'done' => false,
|
||||
]);
|
||||
|
||||
$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();
|
||||
|
||||
return response()->json(['item' => $si]);
|
||||
}
|
||||
|
||||
public function complete(WorkoutSession $session): JsonResponse
|
||||
{
|
||||
$this->authorizeSession($session);
|
||||
$session->completed_at = now();
|
||||
$session->save();
|
||||
$session->load(['workout.items.exercise', 'items']);
|
||||
|
||||
return response()->json(['session' => new WorkoutSessionResource($session)]);
|
||||
}
|
||||
|
||||
protected function authorizeWorkout(Workout $workout): void
|
||||
{
|
||||
if ($workout->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
protected function authorizeSession(WorkoutSession $session): void
|
||||
{
|
||||
if ($session->user_id !== Auth::id()) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\YogaPoseResource;
|
||||
use App\Models\YogaCategory;
|
||||
use App\Models\YogaPose;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class YogaPoseController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$q = trim((string) $request->query('q', ''));
|
||||
$category = (int) $request->query('category', 0);
|
||||
$benefit = trim((string) $request->query('benefit', ''));
|
||||
$perPage = (int) $request->query('per_page', 12);
|
||||
if ($perPage < 6 || $perPage > 60) { $perPage = 12; }
|
||||
|
||||
$query = YogaPose::query()->with('category')
|
||||
->when($q !== '', function ($qb) use ($q) {
|
||||
$qb->where(function ($sub) use ($q) {
|
||||
$sub->where('english_name', 'like', "%{$q}%")
|
||||
->orWhere('sanskrit_name', 'like', "%{$q}%")
|
||||
->orWhere('sanskrit_name_adapted', 'like', "%{$q}%");
|
||||
});
|
||||
})
|
||||
->when($category > 0, fn ($qb) => $qb->where('category_id', $category))
|
||||
->when($benefit !== '', fn ($qb) => $qb->where('benefits', 'like', "%{$benefit}%"))
|
||||
->orderBy('english_name');
|
||||
|
||||
$poses = $query->paginate($perPage)->withQueryString();
|
||||
|
||||
return response()->json([
|
||||
'data' => YogaPoseResource::collection($poses->items()),
|
||||
'meta' => [
|
||||
'current_page' => $poses->currentPage(),
|
||||
'last_page' => $poses->lastPage(),
|
||||
'per_page' => $poses->perPage(),
|
||||
'total' => $poses->total(),
|
||||
],
|
||||
'categories' => YogaCategory::orderBy('name')->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(YogaPose $pose): JsonResponse
|
||||
{
|
||||
$pose->load('category');
|
||||
|
||||
$benefits = (string) $pose->benefits;
|
||||
$tokens = collect(preg_split('/[^\p{L}]+/u', mb_strtolower($benefits)))
|
||||
->filter(fn ($t) => $t !== null && $t !== '' && mb_strlen($t) >= 4)
|
||||
->unique()
|
||||
->take(8)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$similarPoses = YogaPose::query()
|
||||
->with('category')
|
||||
->where('id', '!=', $pose->id)
|
||||
->where(function ($q) use ($pose, $tokens) {
|
||||
if ($pose->category_id) {
|
||||
$q->orWhere('category_id', $pose->category_id);
|
||||
}
|
||||
foreach ($tokens as $tk) {
|
||||
$q->orWhere('benefits', 'like', "%{$tk}%");
|
||||
}
|
||||
})
|
||||
->orderBy('english_name')
|
||||
->limit(9)
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'pose' => new YogaPoseResource($pose),
|
||||
'similar_poses' => YogaPoseResource::collection($similarPoses),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user