diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index d24d710..78a6780 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -61,11 +61,10 @@ class AuthController extends Controller $request->user()->currentAccessToken()->delete(); return response()->json([ - 'message' => 'Déconnecté avec succès' + 'message' => 'Déconnecté avec succès', ])->withoutCookie('token'); } - public function me(): JsonResponse { $user = auth()->user(); @@ -99,5 +98,4 @@ class AuthController extends Controller return response()->json(new UserResource($user->fresh())); } - } diff --git a/app/Http/Controllers/DeviceTokenController.php b/app/Http/Controllers/DeviceTokenController.php index f1f9155..854ab40 100644 --- a/app/Http/Controllers/DeviceTokenController.php +++ b/app/Http/Controllers/DeviceTokenController.php @@ -34,11 +34,13 @@ class DeviceTokenController extends Controller ], ], $deviceToken->wasRecentlyCreated ? 201 : 200); } + public function notifs(): JsonResponse { - auth()->user()->notify(new TestNotification()); + auth()->user()->notify(new TestNotification); + return response()->json([ - 'message' => 'Notification envoyée' + 'message' => 'Notification envoyée', ]); } diff --git a/app/Http/Controllers/MealPostController.php b/app/Http/Controllers/MealPostController.php index 9e31dbd..124fa96 100644 --- a/app/Http/Controllers/MealPostController.php +++ b/app/Http/Controllers/MealPostController.php @@ -3,12 +3,16 @@ namespace App\Http\Controllers; use App\Http\Requests\MealPostsRequest; +use App\Http\Requests\MealPostStatsRequest; use App\Http\Resources\MealPostsResource; use App\Models\MealPosts; +use Carbon\CarbonImmutable; +use Carbon\CarbonInterface; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Http\Response; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; class MealPostController extends Controller @@ -27,6 +31,40 @@ class MealPostController extends Controller return MealPostsResource::collection($mealPosts); } + public function stats(MealPostStatsRequest $request): JsonResponse + { + $validated = $request->validated(); + $period = $validated['period'] ?? 'day'; + $referenceDate = CarbonImmutable::createFromFormat('!Y-m-d', $validated['date'] ?? now()->toDateString()); + + [$startDate, $endDate] = match ($period) { + 'week' => [ + $referenceDate->startOfWeek(CarbonInterface::MONDAY), + $referenceDate->endOfWeek(CarbonInterface::SUNDAY), + ], + default => [ + $referenceDate->startOfDay(), + $referenceDate->endOfDay(), + ], + }; + + $mealPosts = MealPosts::query() + ->where('user_id', $request->user()->getKey()) + ->whereBetween('eaten_at', [$startDate, $endDate]) + ->get(['calories', 'proteins', 'carbs', 'fats', 'eaten_at']); + + return response()->json([ + 'data' => [ + 'period' => $period, + 'date' => $referenceDate->toDateString(), + 'startDate' => $startDate->toDateString(), + 'endDate' => $endDate->toDateString(), + 'totals' => $this->getNutritionTotals($mealPosts), + 'days' => $this->getDailyNutritionTotals($mealPosts, $startDate, $endDate), + ], + ]); + } + public function store(MealPostsRequest $request): JsonResponse { $mealPost = MealPosts::create([ @@ -85,4 +123,34 @@ class MealPostController extends Controller return $data; } + + private function getDailyNutritionTotals(Collection $mealPosts, CarbonImmutable $startDate, CarbonImmutable $endDate): array + { + $days = []; + + for ($date = $startDate; $date->lte($endDate); $date = $date->addDay()) { + $day = $date->toDateString(); + $dayMealPosts = $mealPosts->filter( + fn (MealPosts $mealPost): bool => $mealPost->eaten_at->toDateString() === $day + ); + + $days[] = [ + 'date' => $day, + 'totals' => $this->getNutritionTotals($dayMealPosts), + ]; + } + + return $days; + } + + private function getNutritionTotals(Collection $mealPosts): array + { + return [ + 'calories' => (int) $mealPosts->sum(fn (MealPosts $mealPost): int => (int) ($mealPost->calories ?? 0)), + 'proteins' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->proteins ?? 0)), 2), + 'carbs' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->carbs ?? 0)), 2), + 'fats' => round((float) $mealPosts->sum(fn (MealPosts $mealPost): float => (float) ($mealPost->fats ?? 0)), 2), + 'mealsCount' => $mealPosts->count(), + ]; + } } diff --git a/app/Http/Middleware/AuthenticateWithCookie.php b/app/Http/Middleware/AuthenticateWithCookie.php index 97109ff..5ea416e 100644 --- a/app/Http/Middleware/AuthenticateWithCookie.php +++ b/app/Http/Middleware/AuthenticateWithCookie.php @@ -16,7 +16,7 @@ class AuthenticateWithCookie public function handle(Request $request, Closure $next): Response { if ($request->hasCookie('token')) { - $request->headers->set('Authorization', 'Bearer ' . $request->cookie('token')); + $request->headers->set('Authorization', 'Bearer '.$request->cookie('token')); } return $next($request); diff --git a/app/Http/Requests/MealPostStatsRequest.php b/app/Http/Requests/MealPostStatsRequest.php new file mode 100644 index 0000000..4a41a9b --- /dev/null +++ b/app/Http/Requests/MealPostStatsRequest.php @@ -0,0 +1,22 @@ + ['sometimes', 'string', Rule::in(['day', 'week'])], + 'date' => ['sometimes', 'date_format:Y-m-d'], + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/app/Http/Requests/MealPostsRequest.php b/app/Http/Requests/MealPostsRequest.php index 56023a0..aa9defc 100644 --- a/app/Http/Requests/MealPostsRequest.php +++ b/app/Http/Requests/MealPostsRequest.php @@ -30,7 +30,7 @@ class MealPostsRequest extends FormRequest { return [ 'image.max' => "L'image ne doit pas dépasser 4 Mo.", - 'image_url.required_without' => "Ajoute une image au repas.", + 'image_url.required_without' => 'Ajoute une image au repas.', ]; } } diff --git a/app/Http/Requests/RegisterRequest.php b/app/Http/Requests/RegisterRequest.php index fe97b86..52859e4 100644 --- a/app/Http/Requests/RegisterRequest.php +++ b/app/Http/Requests/RegisterRequest.php @@ -27,7 +27,7 @@ class RegisterRequest extends FormRequest 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 'password' => ['required', 'string', 'min:8'], 'avatar' => ['sometimes', 'nullable', 'image', 'max:2048'], - 'bio' => [ 'nullable', 'string'] + 'bio' => ['nullable', 'string'], ]; } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index a31ae94..00118e7 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -19,7 +19,7 @@ class UserResource extends JsonResource 'name' => $this->name, 'email' => $this->email, 'avatarUrl' => $this->avatar_url ? asset(Storage::url($this->avatar_url)) : null, - 'bio' => $this->bio + 'bio' => $this->bio, ]; } } diff --git a/app/Notifications/TestNotification.php b/app/Notifications/TestNotification.php index 7d5e674..36a95f6 100644 --- a/app/Notifications/TestNotification.php +++ b/app/Notifications/TestNotification.php @@ -3,7 +3,6 @@ namespace App\Notifications; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; @@ -39,11 +38,12 @@ class TestNotification extends Notification ->action('Notification Action', url('/')) ->line('Thank you for using our application!'); } + public function toExpoPush($notifiable) { return [ 'title' => 'Test', - 'body' => 'Notification depuis Laravel API', + 'body' => 'Notification test depuis Laravel API', 'data' => ['test' => true], ]; } diff --git a/app/Policies/MealPostsPolicy.php b/app/Policies/MealPostsPolicy.php index 5d523aa..c288edf 100644 --- a/app/Policies/MealPostsPolicy.php +++ b/app/Policies/MealPostsPolicy.php @@ -10,32 +10,17 @@ class MealPostsPolicy { use HandlesAuthorization; - public function viewAny(User $user): bool - { + public function viewAny(User $user): bool {} - } + public function view(User $user, MealPosts $mealPosts): bool {} - public function view(User $user, MealPosts $mealPosts): bool - { - } + public function create(User $user): bool {} - public function create(User $user): bool - { - } + public function update(User $user, MealPosts $mealPosts): bool {} - public function update(User $user, MealPosts $mealPosts): bool - { - } + public function delete(User $user, MealPosts $mealPosts): bool {} - public function delete(User $user, MealPosts $mealPosts): bool - { - } + public function restore(User $user, MealPosts $mealPosts): bool {} - public function restore(User $user, MealPosts $mealPosts): bool - { - } - - public function forceDelete(User $user, MealPosts $mealPosts): bool - { - } + public function forceDelete(User $user, MealPosts $mealPosts): bool {} } diff --git a/app/Providers/Filament/DashboardPanelProvider.php b/app/Providers/Filament/DashboardPanelProvider.php index fd2baad..8ab8814 100644 --- a/app/Providers/Filament/DashboardPanelProvider.php +++ b/app/Providers/Filament/DashboardPanelProvider.php @@ -14,14 +14,14 @@ use Filament\PanelProvider; use Filament\Support\Colors\Color; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; -use ShuvroRoy\FilamentSpatieLaravelBackup\FilamentSpatieLaravelBackupPlugin; -use ShuvroRoy\FilamentSpatieLaravelHealth\FilamentSpatieLaravelHealthPlugin; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Session\Middleware\StartSession; use Illuminate\View\Middleware\ShareErrorsFromSession; +use ShuvroRoy\FilamentSpatieLaravelBackup\FilamentSpatieLaravelBackupPlugin; +use ShuvroRoy\FilamentSpatieLaravelHealth\FilamentSpatieLaravelHealthPlugin; class DashboardPanelProvider extends PanelProvider { @@ -44,7 +44,7 @@ class DashboardPanelProvider extends PanelProvider ->sort(3), NavigationItem::make('Meilisearch') - ->url("https://meili.leonmorival.com", shouldOpenInNewTab: true) + ->url('https://meili.leonmorival.com', shouldOpenInNewTab: true) ->icon('heroicon-o-magnifying-glass') // Changé ici ->group('Settings') ->sort(4), // Changé ici pour qu'il soit après Horizon @@ -76,7 +76,7 @@ class DashboardPanelProvider extends PanelProvider ->plugins([ FilamentSpatieLaravelHealthPlugin::make(), FilamentExceptionsPlugin::make(), - FilamentSpatieLaravelBackupPlugin::make() + FilamentSpatieLaravelBackupPlugin::make(), ]); } } diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php index a7535be..1bffa3c 100644 --- a/app/Providers/HorizonServiceProvider.php +++ b/app/Providers/HorizonServiceProvider.php @@ -29,7 +29,6 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider { Gate::define('viewHorizon', function ($user = null) { - return false; }); } diff --git a/config/scramble.php b/config/scramble.php index 67b796e..cc46802 100644 --- a/config/scramble.php +++ b/config/scramble.php @@ -32,7 +32,6 @@ return [ 'description' => '', ], - /* * Customize Stoplight Elements UI */ @@ -90,10 +89,10 @@ return [ * ], * ``` */ -// 'servers' => [ -// 'Prod' => 'https://starter-api.leonmorival.xyz/api', -// 'Local' => 'http://localhost/api' -// ], + // 'servers' => [ + // 'Prod' => 'https://starter-api.leonmorival.xyz/api', + // 'Local' => 'http://localhost/api' + // ], /** * Determines how Scramble stores the descriptions of enum cases. diff --git a/database/migrations/2026_03_18_110246_create_health_tables.php b/database/migrations/2026_03_18_110246_create_health_tables.php index 5d8ebff..a064ed8 100644 --- a/database/migrations/2026_03_18_110246_create_health_tables.php +++ b/database/migrations/2026_03_18_110246_create_health_tables.php @@ -12,7 +12,7 @@ return new class extends Migration { $connection = (new HealthCheckResultHistoryItem)->getConnectionName(); $tableName = EloquentHealthResultStore::getHistoryItemInstance()->getTable(); - + Schema::connection($connection)->create($tableName, function (Blueprint $table) { $table->id(); @@ -27,7 +27,7 @@ return new class extends Migration $table->timestamps(); }); - + Schema::connection($connection)->table($tableName, function (Blueprint $table) { $table->index('created_at'); $table->index('batch'); diff --git a/database/migrations/2026_05_15_111236_create_meal_posts_table.php b/database/migrations/2026_05_15_111236_create_meal_posts_table.php index a7cc196..089b35d 100644 --- a/database/migrations/2026_05_15_111236_create_meal_posts_table.php +++ b/database/migrations/2026_05_15_111236_create_meal_posts_table.php @@ -4,18 +4,19 @@ use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::create('meal_posts', function (Blueprint $table) { - $table->ulid('id')->primary(); + $table->ulid('id')->primary(); $table->foreignUlid('user_id')->constrained('users')->cascadeOnDelete(); $table->string('image_url', 2048); $table->text('caption')->nullable(); $table->integer('calories')->nullable(); - $table->decimal('proteins',8,2)->nullable(); - $table->decimal('carbs',8,2)->nullable(); - $table->decimal('fats',8,2)->nullable(); + $table->decimal('proteins', 8, 2)->nullable(); + $table->decimal('carbs', 8, 2)->nullable(); + $table->decimal('fats', 8, 2)->nullable(); $table->string('title'); $table->timestamp('eaten_at'); $table->timestamps(); diff --git a/routes/api.php b/routes/api.php index c1f3214..f568bff 100644 --- a/routes/api.php +++ b/routes/api.php @@ -22,4 +22,7 @@ Route::middleware('auth:sanctum')->group(function (): void { Route::get('test-notifs', [DeviceTokenController::class, 'notifs']); }); -Route::middleware('auth:sanctum')->apiResource('meal-posts', MealPostController::class); +Route::middleware('auth:sanctum')->group(function (): void { + Route::get('meal-posts/stats', [MealPostController::class, 'stats']); + Route::apiResource('meal-posts', MealPostController::class); +}); diff --git a/routes/web.php b/routes/web.php index 7c4d255..b3d9bbc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,3 +1 @@ create(); + $otherUser = User::factory()->create(); + + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 500, + 'proteins' => 30.50, + 'carbs' => 60.25, + 'fats' => 12.75, + 'eaten_at' => '2026-05-16 08:30:00', + ]); + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 250, + 'proteins' => 10.25, + 'carbs' => 20, + 'fats' => 5.50, + 'eaten_at' => '2026-05-16 20:00:00', + ]); + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 900, + 'proteins' => 80, + 'carbs' => 90, + 'fats' => 40, + 'eaten_at' => '2026-05-15 12:00:00', + ]); + MealPosts::factory()->for($otherUser, 'user')->create([ + 'calories' => 700, + 'proteins' => 60, + 'carbs' => 70, + 'fats' => 30, + 'eaten_at' => '2026-05-16 12:00:00', + ]); + + Sanctum::actingAs($user); + + $response = $this->getJson('/api/meal-posts/stats?period=day&date=2026-05-16'); + + $response + ->assertOk() + ->assertJsonPath('data.period', 'day') + ->assertJsonPath('data.date', '2026-05-16') + ->assertJsonPath('data.startDate', '2026-05-16') + ->assertJsonPath('data.endDate', '2026-05-16') + ->assertJsonPath('data.totals.calories', 750) + ->assertJsonPath('data.totals.proteins', 40.75) + ->assertJsonPath('data.totals.carbs', 80.25) + ->assertJsonPath('data.totals.fats', 18.25) + ->assertJsonPath('data.totals.mealsCount', 2) + ->assertJsonCount(1, 'data.days') + ->assertJsonPath('data.days.0.date', '2026-05-16') + ->assertJsonPath('data.days.0.totals.calories', 750); +}); + +it('returns nutrition totals for a week with a daily breakdown', function () { + $user = User::factory()->create(); + + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 400, + 'proteins' => 20, + 'carbs' => 45, + 'fats' => 8, + 'eaten_at' => '2026-05-11 10:00:00', + ]); + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 600, + 'proteins' => 35.50, + 'carbs' => 80.25, + 'fats' => 14.75, + 'eaten_at' => '2026-05-16 12:00:00', + ]); + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 300, + 'proteins' => 18, + 'carbs' => 25, + 'fats' => 11, + 'eaten_at' => '2026-05-17 21:30:00', + ]); + MealPosts::factory()->for($user, 'user')->create([ + 'calories' => 999, + 'proteins' => 99, + 'carbs' => 99, + 'fats' => 99, + 'eaten_at' => '2026-05-18 08:00:00', + ]); + + Sanctum::actingAs($user); + + $response = $this->getJson('/api/meal-posts/stats?period=week&date=2026-05-16'); + + $response + ->assertOk() + ->assertJsonPath('data.period', 'week') + ->assertJsonPath('data.startDate', '2026-05-11') + ->assertJsonPath('data.endDate', '2026-05-17') + ->assertJsonPath('data.totals.calories', 1300) + ->assertJsonPath('data.totals.proteins', 73.5) + ->assertJsonPath('data.totals.carbs', 150.25) + ->assertJsonPath('data.totals.fats', 33.75) + ->assertJsonPath('data.totals.mealsCount', 3) + ->assertJsonCount(7, 'data.days') + ->assertJsonPath('data.days.0.date', '2026-05-11') + ->assertJsonPath('data.days.0.totals.calories', 400) + ->assertJsonPath('data.days.5.date', '2026-05-16') + ->assertJsonPath('data.days.5.totals.calories', 600) + ->assertJsonPath('data.days.6.date', '2026-05-17') + ->assertJsonPath('data.days.6.totals.calories', 300); +}); + +it('validates stats filters', function () { + Sanctum::actingAs(User::factory()->create()); + + $response = $this->getJson('/api/meal-posts/stats?period=month&date=16-05-2026'); + + $response + ->assertUnprocessable() + ->assertJsonValidationErrors(['period', 'date']); +});