WeekPlannerController.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. using Microsoft.AspNetCore.Mvc;
  2. using Microsoft.EntityFrameworkCore;
  3. using Recepie.Data;
  4. using Recepie.Models;
  5. using Recepie.ViewModels;
  6. namespace Recepie.Controllers
  7. {
  8. public class WeekPlannerController : Controller
  9. {
  10. private readonly RecipeContext _context;
  11. public WeekPlannerController(RecipeContext context)
  12. {
  13. _context = context;
  14. }
  15. public async Task<IActionResult> Index(DateTime? startDate = null)
  16. {
  17. var weekStart = startDate ?? DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + 1); // Monday
  18. var currentWeekPlan = await _context.WeekPlans
  19. .Include(wp => wp.DayPlans.OrderBy(dp => dp.Date))
  20. .ThenInclude(dp => dp.Recipe)
  21. .FirstOrDefaultAsync(wp => wp.StartDate.Date == weekStart.Date);
  22. var mainIngredients = await GetMainIngredientsAsync();
  23. var viewModel = new WeekPlannerViewModel
  24. {
  25. CurrentWeekPlan = currentWeekPlan,
  26. AvailableMainIngredients = mainIngredients,
  27. StartDate = weekStart
  28. };
  29. return View(viewModel);
  30. }
  31. [HttpPost]
  32. public async Task<IActionResult> CreateWeekPlan(DateTime startDate, string planName = "")
  33. {
  34. if (string.IsNullOrEmpty(planName))
  35. planName = $"Week Plan {startDate:MMM dd, yyyy}";
  36. var weekPlan = new WeekPlan
  37. {
  38. Name = planName,
  39. StartDate = startDate,
  40. CreatedAt = DateTime.Now
  41. };
  42. _context.WeekPlans.Add(weekPlan);
  43. await _context.SaveChangesAsync();
  44. // Create day plans for the week
  45. for (int i = 0; i < 7; i++)
  46. {
  47. var dayPlan = new DayPlan
  48. {
  49. WeekPlanId = weekPlan.Id,
  50. DayOfWeek = (DayOfWeek)((int)(startDate.DayOfWeek + i) % 7),
  51. Date = startDate.AddDays(i)
  52. };
  53. _context.DayPlans.Add(dayPlan);
  54. }
  55. await _context.SaveChangesAsync();
  56. return RedirectToAction(nameof(Index));
  57. }
  58. public async Task<IActionResult> ConfigureDay(int weekPlanId, DayOfWeek dayOfWeek)
  59. {
  60. var weekPlan = await _context.WeekPlans
  61. .Include(wp => wp.DayPlans)
  62. .FirstOrDefaultAsync(wp => wp.Id == weekPlanId);
  63. if (weekPlan == null)
  64. return NotFound();
  65. var dayPlan = weekPlan.DayPlans.FirstOrDefault(dp => dp.DayOfWeek == dayOfWeek);
  66. if (dayPlan == null)
  67. return NotFound();
  68. var mainIngredients = await GetMainIngredientsAsync();
  69. var suggestedRecipes = await GetSuggestedRecipesAsync(dayPlan.MainIngredient, dayPlan.BannedIngredients, dayPlan.RequiredIngredients);
  70. var viewModel = new DayPlanConfigViewModel
  71. {
  72. DayPlanId = dayPlan.Id,
  73. DayOfWeek = dayPlan.DayOfWeek,
  74. Date = dayPlan.Date,
  75. SelectedMainIngredient = dayPlan.MainIngredient,
  76. BannedIngredients = dayPlan.BannedIngredients,
  77. RequiredIngredients = dayPlan.RequiredIngredients,
  78. AvailableMainIngredients = mainIngredients,
  79. SuggestedRecipes = suggestedRecipes,
  80. SelectedRecipeId = dayPlan.RecipeId
  81. };
  82. return View(viewModel);
  83. }
  84. [HttpPost]
  85. public async Task<IActionResult> UpdateDayPlan(DayPlanConfigViewModel model)
  86. {
  87. var dayPlan = await _context.DayPlans.FindAsync(model.DayPlanId);
  88. if (dayPlan == null)
  89. return NotFound();
  90. dayPlan.MainIngredient = model.SelectedMainIngredient;
  91. dayPlan.BannedIngredients = model.BannedIngredients;
  92. dayPlan.RequiredIngredients = model.RequiredIngredients;
  93. dayPlan.RecipeId = model.SelectedRecipeId;
  94. await _context.SaveChangesAsync();
  95. return RedirectToAction(nameof(Index));
  96. }
  97. [HttpPost]
  98. public async Task<IActionResult> RandomizeRecipe(int dayPlanId)
  99. {
  100. var dayPlan = await _context.DayPlans.FindAsync(dayPlanId);
  101. if (dayPlan == null)
  102. return NotFound();
  103. // Get all recipes for this week to avoid repeats
  104. var weekPlan = await _context.WeekPlans
  105. .Include(wp => wp.DayPlans)
  106. .FirstOrDefaultAsync(wp => wp.DayPlans.Any(dp => dp.Id == dayPlanId));
  107. var usedRecipeIds = weekPlan?.DayPlans
  108. .Where(dp => dp.RecipeId.HasValue && dp.Id != dayPlanId)
  109. .Select(dp => dp.RecipeId!.Value)
  110. .ToList() ?? new List<int>();
  111. var recipes = await GetSuggestedRecipesAsync(dayPlan.MainIngredient, dayPlan.BannedIngredients, dayPlan.RequiredIngredients);
  112. // Filter out already used recipes this week
  113. var availableRecipes = recipes.Where(r => !usedRecipeIds.Contains(r.Id)).ToList();
  114. // If no unused recipes, fall back to all suggested recipes
  115. if (!availableRecipes.Any())
  116. availableRecipes = recipes;
  117. if (availableRecipes.Any())
  118. {
  119. // Use cryptographically secure random for better distribution
  120. using var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
  121. var randomBytes = new byte[4];
  122. rng.GetBytes(randomBytes);
  123. var randomIndex = Math.Abs(BitConverter.ToInt32(randomBytes, 0)) % availableRecipes.Count;
  124. var randomRecipe = availableRecipes[randomIndex];
  125. dayPlan.RecipeId = randomRecipe.Id;
  126. await _context.SaveChangesAsync();
  127. }
  128. return RedirectToAction(nameof(Index));
  129. }
  130. private async Task<List<string>> GetMainIngredientsAsync()
  131. {
  132. // Define clean, consolidated main ingredient categories
  133. var mainIngredientCategories = new List<string>
  134. {
  135. "Pasta",
  136. "Potatis",
  137. "Ris",
  138. "Kött",
  139. "Kyckling",
  140. "Fisk",
  141. "Skaldjur",
  142. "Ägg",
  143. "Bönor & Linser",
  144. "Vegetariskt",
  145. "Soppa",
  146. "Sallad"
  147. };
  148. return mainIngredientCategories;
  149. }
  150. private async Task<List<Recipe>> GetSuggestedRecipesAsync(string? mainIngredient, string? bannedIngredients, string? requiredIngredients)
  151. {
  152. var query = _context.Recipes
  153. .Include(r => r.RecipeIngredients)
  154. .ThenInclude(ri => ri.Ingredient)
  155. .AsQueryable();
  156. // Filter by main ingredient with smart categorization
  157. if (!string.IsNullOrEmpty(mainIngredient))
  158. {
  159. var searchTerms = GetIngredientSearchTerms(mainIngredient);
  160. query = query.Where(r => r.RecipeIngredients
  161. .Any(ri => ri.Ingredient != null && ri.Ingredient.Name != null &&
  162. searchTerms.Any(term => ri.Ingredient.Name.ToLower().Contains(term))));
  163. }
  164. // Filter out banned ingredients
  165. if (!string.IsNullOrEmpty(bannedIngredients))
  166. {
  167. var banned = bannedIngredients.Split(',', StringSplitOptions.RemoveEmptyEntries)
  168. .Select(b => b.Trim().ToLower()).ToList();
  169. query = query.Where(r => !r.RecipeIngredients
  170. .Any(ri => ri.Ingredient != null && ri.Ingredient.Name != null && banned.Any(b => ri.Ingredient.Name!.ToLower().Contains(b))));
  171. }
  172. // Filter by required ingredients
  173. if (!string.IsNullOrEmpty(requiredIngredients))
  174. {
  175. var required = requiredIngredients.Split(',', StringSplitOptions.RemoveEmptyEntries)
  176. .Select(req => req.Trim().ToLower()).ToList();
  177. foreach (var req in required)
  178. {
  179. query = query.Where(r => r.RecipeIngredients
  180. .Any(ri => ri.Ingredient != null && ri.Ingredient.Name != null && ri.Ingredient.Name.ToLower().Contains(req)));
  181. }
  182. }
  183. // Increase the pool significantly and add randomization at DB level
  184. return await query.OrderBy(r => Guid.NewGuid()).Take(50).ToListAsync();
  185. }
  186. private List<string> GetIngredientSearchTerms(string mainIngredient)
  187. {
  188. return mainIngredient.ToLower() switch
  189. {
  190. "pasta" => new List<string> { "pasta", "spaghetti", "penne", "fusilli", "linguine", "tagliatelle", "fettuccine", "makaroner", "lasagne", "ravioli", "gnocchi" },
  191. "potatis" => new List<string> { "potatis", "potato", "potatismos", "klyftpotatis", "gratäng" },
  192. "ris" => new List<string> { "ris", "rice", "risotto", "basmati", "jasmin" },
  193. "kött" => new List<string> { "kött", "nötkött", "fläsk", "lamm", "köttfärs", "bacon", "skinka", "korv" },
  194. "kyckling" => new List<string> { "kyckling", "chicken", "kycklingfilé", "kycklingklubb", "kycklingbröst" },
  195. "fisk" => new List<string> { "fisk", "lax", "torsk", "abborre", "tonfisk", "salmon", "cod" },
  196. "skaldjur" => new List<string> { "räkor", "krabba", "hummer", "musslor", "shrimp", "crab", "lobster" },
  197. "ägg" => new List<string> { "ägg", "egg", "omelett", "scrambled" },
  198. "bönor & linser" => new List<string> { "bönor", "linser", "kikärtor", "beans", "lentils", "chickpeas" },
  199. "vegetariskt" => new List<string> { "tofu", "halloumi", "quinoa", "bulgur", "tempeh", "vegetarisk" },
  200. "soppa" => new List<string> { "soppa", "soup", "buljong", "bisque" },
  201. "sallad" => new List<string> { "sallad", "salad", "coleslaw", "caesar" },
  202. _ => new List<string> { mainIngredient.ToLower() }
  203. };
  204. }
  205. }
  206. }