integer('per_page', 15), 100)); $mealPosts = $this->mealPostListQuery($request) ->latest('eaten_at') ->paginate($perPage) ->withQueryString(); return MealPostsResource::collection($mealPosts); } public function mine(Request $request): AnonymousResourceCollection { $perPage = max(1, min($request->integer('per_page', 15), 100)); $mealPosts = $this->mealPostListQuery($request) ->where('user_id', $request->user()->getKey()) ->latest('eaten_at') ->paginate($perPage) ->withQueryString(); 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 calendar(MealPostCalendarRequest $request): JsonResponse { $month = $request->validated('month') ?? now()->format('Y-m'); $referenceDate = CarbonImmutable::createFromFormat('!Y-m', $month); $startDate = $referenceDate->startOfMonth(); $endDate = $referenceDate->endOfMonth(); $days = MealPosts::query() ->where('user_id', $request->user()->getKey()) ->whereBetween('eaten_at', [$startDate->startOfDay(), $endDate->endOfDay()]) ->selectRaw('DATE(eaten_at) as date, COUNT(*) as meals_count') ->groupByRaw('DATE(eaten_at)') ->orderBy('date') ->get() ->map(fn (MealPosts $mealPost): array => [ 'date' => CarbonImmutable::parse((string) $mealPost->date)->toDateString(), 'mealsCount' => (int) $mealPost->meals_count, ]) ->values(); return response()->json([ 'data' => [ 'month' => $referenceDate->format('Y-m'), 'startDate' => $startDate->toDateString(), 'endDate' => $endDate->toDateString(), 'days' => $days, ], ]); } public function store(MealPostsRequest $request): JsonResponse { $data = $this->getValidatedDataWithImageUrl($request); $ingredients = $data['ingredients'] ?? []; unset($data['ingredients']); $mealPost = DB::transaction(function () use ($data, $ingredients, $request): MealPosts { $mealPost = MealPosts::create([ ...$data, 'user_id' => $request->user()->getKey(), ]); if ($ingredients !== []) { $mealPost->ingredients()->createMany($ingredients); } return $mealPost; }); $mealPost->setRelation('user', $request->user()); return (new MealPostsResource($this->loadMealPostForResponse($mealPost, $request))) ->response() ->setStatusCode(201); } public function show(Request $request, MealPosts $mealPost): MealPostsResource { $this->abortIfUnavailable($mealPost); return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)); } public function update(MealPostsRequest $request, MealPosts $mealPost): MealPostsResource { $this->abortIfNotOwner($request, $mealPost); $data = $this->getValidatedDataWithImageUrl($request); $shouldSyncIngredients = array_key_exists('ingredients', $data); $ingredients = $data['ingredients'] ?? []; unset($data['ingredients']); $mealPost = DB::transaction(function () use ($data, $ingredients, $mealPost, $shouldSyncIngredients): MealPosts { if ($data !== []) { $mealPost->update($data); } if ($shouldSyncIngredients) { $mealPost->ingredients()->delete(); if ($ingredients !== []) { $mealPost->ingredients()->createMany($ingredients); } } return $mealPost->refresh(); }); return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)); } public function like(Request $request, MealPosts $mealPost): MealPostsResource { $this->abortIfUnavailable($mealPost); $mealPost->likedByUsers()->syncWithoutDetaching([$request->user()->getKey()]); return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)); } public function unlike(Request $request, MealPosts $mealPost): MealPostsResource { $this->abortIfUnavailable($mealPost); $mealPost->likedByUsers()->detach($request->user()->getKey()); return new MealPostsResource($this->loadMealPostForResponse($mealPost, $request)); } public function destroy(Request $request, MealPosts $mealPost): Response { $this->abortIfNotOwner($request, $mealPost); $mealPost->delete(); return response()->noContent(); } private function abortIfNotOwner(Request $request, MealPosts $mealPost): void { abort_unless($request->user(), 401); abort_unless($mealPost->user_id === $request->user()->getKey(), 404); } private function abortIfUnavailable(MealPosts $mealPost): void { abort_if( $mealPost->isHidden() || $mealPost->user()->whereNotNull('suspended_at')->exists(), 404, ); } private function loadMealPostForResponse(MealPosts $mealPost, Request $request): MealPosts { return $mealPost ->loadMissing('user:id,name,avatar_url,account_verified_at', 'ingredients'); } private function mealPostListQuery(Request $request): Builder { return MealPosts::query() ->whereNull('hidden_at') ->whereHas('user', fn (Builder $query): Builder => $query->whereNull('suspended_at')) ->with('user:id,name,avatar_url,account_verified_at'); } private function getValidatedDataWithImageUrl(MealPostsRequest $request): array { $data = $request->validated(); if ($request->hasFile('image')) { $path = $request->file('image')->store('meal-posts', ); $data['image_url'] = $path; } unset($data['image']); 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(), ]; } }