285 lines
13 KiB
C#
285 lines
13 KiB
C#
using MealPlan.Api.Extensions;
|
|
using MealPlan.Api.DTOs.Diet;
|
|
using MealPlan.Api.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace MealPlan.Api.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/diet")]
|
|
[Authorize]
|
|
public class DietController : ControllerBase
|
|
{
|
|
private readonly IDietTrackingService _dietService;
|
|
|
|
public DietController(IDietTrackingService dietService) => _dietService = dietService;
|
|
|
|
/// <summary>Daily intake summary with consumed totals, goals, and remaining amounts.</summary>
|
|
[HttpGet("summary")]
|
|
[ProducesResponseType(typeof(DailySummaryDto), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetSummary([FromQuery] DateOnly? date, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var summary = await _dietService.GetDailySummaryAsync(userId, targetDate, ct);
|
|
return Ok(summary);
|
|
}
|
|
|
|
[HttpGet("goals")]
|
|
[ProducesResponseType(typeof(UserDailyGoalsDto), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetGoals(CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var goals = await _dietService.GetGoalsAsync(userId, ct);
|
|
return Ok(goals ?? new UserDailyGoalsDto());
|
|
}
|
|
|
|
[HttpPut("goals")]
|
|
[ProducesResponseType(typeof(UserDailyGoalsDto), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> UpsertGoals([FromBody] UserDailyGoalsDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var goals = await _dietService.UpsertGoalsAsync(userId, dto, ct);
|
|
return Ok(goals);
|
|
}
|
|
|
|
[HttpGet("meals")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<MealConsumptionDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetMeals([FromQuery] DateOnly? date, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var meals = await _dietService.GetMealsAsync(userId, targetDate, ct);
|
|
return Ok(meals);
|
|
}
|
|
|
|
[HttpPost("meals")]
|
|
[ProducesResponseType(typeof(MealConsumptionDto), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> LogMeal([FromBody] LogMealDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var meal = await _dietService.LogMealAsync(userId, dto, ct);
|
|
if (meal is null)
|
|
{
|
|
return dto.RecipeId.HasValue
|
|
? NotFound(new { error = $"Recipe {dto.RecipeId} was not found." })
|
|
: BadRequest(new { error = "Invalid meal log payload." });
|
|
}
|
|
|
|
return CreatedAtAction(nameof(GetMeals), new { date = meal.LogDate }, meal);
|
|
}
|
|
|
|
[HttpPost("meals/preview")]
|
|
[ProducesResponseType(typeof(MealPreviewDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> PreviewMeal([FromQuery] DateOnly? date, [FromBody] LogMealDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var preview = await _dietService.PreviewMealAsync(userId, targetDate, dto, ct);
|
|
return preview is null
|
|
? BadRequest(new { error = "Could not preview meal macros." })
|
|
: Ok(preview);
|
|
}
|
|
|
|
[HttpGet("suggestions")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<DietSuggestionDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetSuggestions([FromQuery] DateOnly? date, [FromQuery] int limit = 5, CancellationToken ct = default)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var suggestions = await _dietService.GetMealSuggestionsAsync(userId, targetDate, limit, ct);
|
|
return Ok(suggestions);
|
|
}
|
|
|
|
[HttpDelete("meals/{id:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> DeleteMeal(int id, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var deleted = await _dietService.DeleteMealAsync(userId, id, ct);
|
|
return deleted ? NoContent() : NotFound(new { error = $"Meal log {id} was not found." });
|
|
}
|
|
|
|
[HttpGet("drinks/catalog")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<DrinkCatalogItemDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetDrinkCatalog(CancellationToken ct)
|
|
{
|
|
var catalog = await _dietService.GetDrinkCatalogAsync(ct);
|
|
return Ok(catalog);
|
|
}
|
|
|
|
[HttpGet("drinks")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<DrinkConsumptionDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetDrinks([FromQuery] DateOnly? date, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var drinks = await _dietService.GetDrinksAsync(userId, targetDate, ct);
|
|
return Ok(drinks);
|
|
}
|
|
|
|
[HttpPost("drinks")]
|
|
[ProducesResponseType(typeof(DrinkConsumptionDto), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> LogDrink([FromBody] LogDrinkDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var drink = await _dietService.LogDrinkAsync(userId, dto, ct);
|
|
if (drink is null)
|
|
{
|
|
return NotFound(new { error = "Drink catalog item was not found." });
|
|
}
|
|
|
|
return CreatedAtAction(nameof(GetDrinks), new { date = drink.LogDate }, drink);
|
|
}
|
|
|
|
[HttpDelete("drinks/{id:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> DeleteDrink(int id, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var deleted = await _dietService.DeleteDrinkAsync(userId, id, ct);
|
|
return deleted ? NoContent() : NotFound(new { error = $"Drink log {id} was not found." });
|
|
}
|
|
|
|
[HttpGet("reminders")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<DietReminderDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetReminders(CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var reminders = await _dietService.GetRemindersAsync(userId, ct);
|
|
return Ok(reminders);
|
|
}
|
|
|
|
[HttpGet("reminders/due")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<DueReminderDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetDueReminders(
|
|
[FromQuery] DateOnly? date,
|
|
[FromQuery] TimeOnly? time,
|
|
CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var due = await _dietService.GetDueRemindersAsync(userId, date, time, ct);
|
|
return Ok(due);
|
|
}
|
|
|
|
[HttpPost("reminders")]
|
|
[ProducesResponseType(typeof(DietReminderDto), StatusCodes.Status201Created)]
|
|
public async Task<IActionResult> CreateReminder([FromBody] CreateDietReminderDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var reminder = await _dietService.CreateReminderAsync(userId, dto, ct);
|
|
return CreatedAtAction(nameof(GetReminders), reminder);
|
|
}
|
|
|
|
[HttpPut("reminders/{id:int}")]
|
|
[ProducesResponseType(typeof(DietReminderDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> UpdateReminder(int id, [FromBody] UpdateDietReminderDto dto, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var reminder = await _dietService.UpdateReminderAsync(userId, id, dto, ct);
|
|
return reminder is null
|
|
? NotFound(new { error = $"Reminder {id} was not found." })
|
|
: Ok(reminder);
|
|
}
|
|
|
|
[HttpDelete("reminders/{id:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> DeleteReminder(int id, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var deleted = await _dietService.DeleteReminderAsync(userId, id, ct);
|
|
return deleted ? NoContent() : NotFound(new { error = $"Reminder {id} was not found." });
|
|
}
|
|
|
|
[HttpGet("history")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<HistoryDayDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetHistory([FromQuery] DateOnly from, [FromQuery] DateOnly to, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var history = await _dietService.GetHistoryAsync(userId, from, to, ct);
|
|
return Ok(history);
|
|
}
|
|
|
|
[HttpGet("meals/recent")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<RecentMealDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetRecentMeals([FromQuery] int limit = 10, CancellationToken ct = default)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var meals = await _dietService.GetRecentMealsAsync(userId, limit, ct);
|
|
return Ok(meals);
|
|
}
|
|
|
|
[HttpPost("meals/copy-yesterday")]
|
|
[ProducesResponseType(typeof(CopyMealsResultDto), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> CopyYesterdayMeals([FromQuery] DateOnly? date, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var targetDate = date ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
|
var result = await _dietService.CopyYesterdayMealsAsync(userId, targetDate, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
[HttpGet("favorites/recipes")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<FavoriteRecipeDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetFavoriteRecipes(CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
return Ok(await _dietService.GetFavoriteRecipesAsync(userId, ct));
|
|
}
|
|
|
|
[HttpPost("favorites/recipes/{recipeId:int}")]
|
|
[ProducesResponseType(typeof(FavoriteRecipeDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> AddFavoriteRecipe(int recipeId, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var fav = await _dietService.AddFavoriteRecipeAsync(userId, recipeId, ct);
|
|
return fav is null ? NotFound() : Ok(fav);
|
|
}
|
|
|
|
[HttpDelete("favorites/recipes/{recipeId:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
public async Task<IActionResult> RemoveFavoriteRecipe(int recipeId, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var deleted = await _dietService.RemoveFavoriteRecipeAsync(userId, recipeId, ct);
|
|
return deleted ? NoContent() : NotFound();
|
|
}
|
|
|
|
[HttpGet("favorites/catalog")]
|
|
[ProducesResponseType(typeof(IReadOnlyList<FavoriteCatalogItemDto>), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> GetFavoriteCatalogItems(CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
return Ok(await _dietService.GetFavoriteCatalogItemsAsync(userId, ct));
|
|
}
|
|
|
|
[HttpPost("favorites/catalog/{catalogItemId:int}")]
|
|
[ProducesResponseType(typeof(FavoriteCatalogItemDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> AddFavoriteCatalogItem(int catalogItemId, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var fav = await _dietService.AddFavoriteCatalogItemAsync(userId, catalogItemId, ct);
|
|
return fav is null ? NotFound() : Ok(fav);
|
|
}
|
|
|
|
[HttpDelete("favorites/catalog/{catalogItemId:int}")]
|
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
public async Task<IActionResult> RemoveFavoriteCatalogItem(int catalogItemId, CancellationToken ct)
|
|
{
|
|
if (User.RequireUserId(out var userId) is { } unauthorized) return unauthorized;
|
|
var deleted = await _dietService.RemoveFavoriteCatalogItemAsync(userId, catalogItemId, ct);
|
|
return deleted ? NoContent() : NotFound();
|
|
}
|
|
}
|