119 lines
4.6 KiB
C#
119 lines
4.6 KiB
C#
using MealPlan.Api.DTOs.Recipe;
|
|
using MealPlan.Api.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace MealPlan.Api.Controllers;
|
|
|
|
/// <summary>Endpoints for creating recipes manually and importing from Excel.</summary>
|
|
[ApiController]
|
|
[Route("api/recipes")]
|
|
[Authorize]
|
|
public class RecipeManagementController : ControllerBase
|
|
{
|
|
private readonly IRecipeManagementService _managementService;
|
|
private readonly IRecipeExcelService _excelService;
|
|
private readonly IRecipeCalorieRecalculationService _calorieRecalculation;
|
|
|
|
public RecipeManagementController(
|
|
IRecipeManagementService managementService,
|
|
IRecipeExcelService excelService,
|
|
IRecipeCalorieRecalculationService calorieRecalculation)
|
|
{
|
|
_managementService = managementService;
|
|
_excelService = excelService;
|
|
_calorieRecalculation = calorieRecalculation;
|
|
}
|
|
|
|
/// <summary>Creates a new recipe with ingredients and steps.</summary>
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status201Created)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> Create([FromBody] CreateRecipeDto dto, CancellationToken ct)
|
|
{
|
|
var recipe = await _managementService.CreateRecipeAsync(dto, ct);
|
|
return CreatedAtAction(
|
|
nameof(RecipesController.GetRecipe),
|
|
"Recipes",
|
|
new { id = recipe.Id },
|
|
recipe);
|
|
}
|
|
|
|
/// <summary>Updates an existing recipe (replaces ingredients and steps).</summary>
|
|
[HttpPut("{id:int}")]
|
|
[ProducesResponseType(typeof(RecipeDetailDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> Update(int id, [FromBody] CreateRecipeDto dto, CancellationToken ct)
|
|
{
|
|
var recipe = await _managementService.UpdateRecipeAsync(id, dto, ct);
|
|
return recipe is null
|
|
? NotFound(new { error = $"Recipe {id} was not found." })
|
|
: Ok(recipe);
|
|
}
|
|
|
|
/// <summary>Downloads an Excel template for bulk recipe import.</summary>
|
|
[HttpGet("import/template")]
|
|
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
|
public IActionResult DownloadTemplate()
|
|
{
|
|
var bytes = _excelService.GenerateTemplate();
|
|
return File(
|
|
bytes,
|
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"DailyMeals-recipe-import-template.xlsx");
|
|
}
|
|
|
|
/// <summary>Imports recipes from an uploaded .xlsx file (Recipes + Ingredients + Steps sheets).</summary>
|
|
[HttpPost("import/excel")]
|
|
[RequestSizeLimit(10_000_000)]
|
|
[ProducesResponseType(typeof(RecipeImportResultDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
|
public async Task<IActionResult> ImportExcel(IFormFile? file, CancellationToken ct)
|
|
{
|
|
if (file is null || file.Length == 0)
|
|
{
|
|
return BadRequest(new { error = "No file uploaded." });
|
|
}
|
|
|
|
if (!file.FileName.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return BadRequest(new { error = "Only .xlsx files are supported." });
|
|
}
|
|
|
|
await using var stream = file.OpenReadStream();
|
|
var result = await _excelService.ImportAsync(stream, ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>Recalculates recipe macros from linked catalog ingredients (all active recipes).</summary>
|
|
[HttpPost("recalculate-macros")]
|
|
[ProducesResponseType(typeof(RecipeMacroRecalcResultDto), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> RecalculateMacros(CancellationToken ct)
|
|
{
|
|
var result = await _managementService.RecalculateAllMacrosFromCatalogAsync(ct);
|
|
return Ok(result);
|
|
}
|
|
|
|
/// <summary>Scales or AI-adjusts ingredient amounts to match a calorie target.</summary>
|
|
[HttpPost("{id:int}/recalculate-calories")]
|
|
[ProducesResponseType(typeof(RecalculateRecipeCaloriesResultDto), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> RecalculateCalories(
|
|
int id,
|
|
[FromBody] RecalculateRecipeCaloriesRequestDto request,
|
|
CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
var result = await _calorieRecalculation.RecalculateAsync(id, request, ct);
|
|
return result is null
|
|
? NotFound(new { error = $"Recipe {id} was not found." })
|
|
: Ok(result);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
return BadRequest(new { error = ex.Message });
|
|
}
|
|
}
|
|
}
|