Menu - Pre 9 .net

This commit is contained in:
Elias Jansson
2025-05-06 13:19:13 +02:00
parent 3cdf630af2
commit a140a59f66
19 changed files with 849 additions and 378 deletions

View File

@@ -31,7 +31,7 @@
<ItemGroup> <ItemGroup>
<Folder Include="Views\NewFolder\" /> <Folder Include="Views\NewFolder\" />
<Folder Include="wwwroot\uploads\" /> <Folder Include="wwwroot\images\meals\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,7 +1,14 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Aberwyn.Models; using Aberwyn.Models;
using Aberwyn.Data; using Aberwyn.Data;
using System;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Aberwyn.Controllers namespace Aberwyn.Controllers
{ {
@@ -16,35 +23,89 @@ namespace Aberwyn.Controllers
_env = env; _env = env;
} }
public IActionResult Veckomeny(int week = -1) public IActionResult Veckomeny(int? week, int? year)
{ {
var menuService = new MenuService(_configuration, _env); var menuService = new MenuService(_configuration, _env);
int currentWeek = week == -1 ? ISOWeek.GetWeekOfYear(DateTime.Now) : week;
int currentYear = DateTime.Now.Year;
// Hämta meny från DB var today = DateTime.Today;
var menus = menuService.GetWeeklyMenu(currentWeek, currentYear); int resolvedWeek = week ?? ISOWeek.GetWeekOfYear(today);
int resolvedYear = year ?? today.Year;
var model = new WeeklyMenuViewModel var menus = menuService.GetWeeklyMenu(resolvedWeek, resolvedYear);
var vm = new WeeklyMenuViewModel
{ {
WeekNumber = currentWeek, WeekNumber = resolvedWeek,
Year = currentYear, Year = resolvedYear,
WeeklyMenus = menus, WeeklyMenus = menus,
AvailableCooks = menuService.GetUsers() AvailableCooks = menuService.GetUsers()
}; };
return View(model); var recent = menuService
.GetMenuEntriesByDateRange(DateTime.Now.AddDays(-28), DateTime.Now)
.Select(x => new WeeklyMenuViewModel.RecentMenuEntry
{
Date = x.Date,
BreakfastMealName = x.BreakfastMealName,
LunchMealName = x.LunchMealName,
DinnerMealName = x.DinnerMealName
})
.ToList();
vm.RecentEntries = recent;
ViewBag.AvailableMeals = menuService.GetMeals();
return View(vm);
}
[HttpPost]
public IActionResult AddMealAjax([FromBody] Meal meal)
{
if (meal == null || string.IsNullOrWhiteSpace(meal.Name))
return BadRequest("Ogiltigt måltidsobjekt.");
var service = new MenuService(_configuration, _env);
// Kontrollera om en måltid med samma namn redan finns
var existing = service.GetMeals()
.FirstOrDefault(m => m.Name.Equals(meal.Name, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
// Fyll i CreatedAt om det inte sätts automatiskt i databasen
meal.CreatedAt = DateTime.Now;
service.SaveOrUpdateMeal(meal);
existing = meal;
}
else
{
// Om måltiden finns men saknar data (t.ex. är bara ett namn) kan vi uppdatera den
existing.Description = meal.Description;
existing.ProteinType = meal.ProteinType;
existing.CarbType = meal.CarbType;
existing.RecipeUrl = meal.RecipeUrl;
existing.ImageUrl = meal.ImageUrl;
service.SaveOrUpdateMeal(existing);
}
return Json(new
{
Id = existing.Id,
Name = existing.Name,
Description = existing.Description,
ProteinType = existing.ProteinType,
CarbType = existing.CarbType,
RecipeUrl = existing.RecipeUrl
});
} }
public IActionResult MealAdmin() public IActionResult MealAdmin()
{ {
var service = new MenuService(_configuration, _env); var service = new MenuService(_configuration, _env);
var meals = service.GetMealsDetailed(); // Se till att denna metod finns och returnerar fullständiga Meal-objekt var meals = service.GetMealsDetailed();
return View("MealAdmin", meals); return View("MealAdmin", meals);
} }
[HttpGet] [HttpGet]
public JsonResult SearchMeals(string term) public JsonResult SearchMeals(string term)
{ {
@@ -64,17 +125,16 @@ namespace Aberwyn.Controllers
return Json(result); return Json(result);
} }
[HttpPost] [HttpPost]
public IActionResult SaveVeckomeny(IFormCollection form) public IActionResult SaveVeckomeny(IFormCollection form, int week, int year)
{ {
var menuService = new MenuService(_configuration, _env); var menuService = new MenuService(_configuration, _env);
var allMeals = menuService.GetMeals(); var allMeals = menuService.GetMeals();
var model = new MenuViewModel var model = new MenuViewModel
{ {
WeekNumber = ISOWeek.GetWeekOfYear(DateTime.Now), WeekNumber = week,
Year = DateTime.Now.Year, Year = year,
WeeklyMenus = new List<WeeklyMenu>() WeeklyMenus = new List<WeeklyMenu>()
}; };
@@ -82,62 +142,76 @@ namespace Aberwyn.Controllers
foreach (var key in form.Keys.Where(k => k.StartsWith("Meal[", StringComparison.OrdinalIgnoreCase))) foreach (var key in form.Keys.Where(k => k.StartsWith("Meal[", StringComparison.OrdinalIgnoreCase)))
{ {
var match = System.Text.RegularExpressions.Regex.Match(key, @"Meal\[(\d+)\]\[(\w+)\]"); var match = Regex.Match(key, @"Meal\[(\d+)\]\[(\w+)\]");
if (match.Success) if (!match.Success) continue;
int day = int.Parse(match.Groups[1].Value);
string mealType = match.Groups[2].Value;
string mealName = form[key];
string cook = form[$"Cook[{day}]"];
if (!entriesByDay.TryGetValue(day, out var entry))
{ {
int day = int.Parse(match.Groups[1].Value); entry = new WeeklyMenu
string mealType = match.Groups[2].Value;
string mealName = form[key];
string cook = form[$"Cook[{day}][{mealType}]"];
if (string.IsNullOrWhiteSpace(mealName)) continue;
var meal = allMeals.FirstOrDefault(m => m.Name.Equals(mealName, StringComparison.OrdinalIgnoreCase));
if (meal == null)
{ {
meal = new Meal { Name = mealName }; WeekNumber = week,
menuService.SaveMeal(meal); Year = year,
DayOfWeek = day + 1
};
entriesByDay[day] = entry;
}
entry.Cook = cook;
if (string.IsNullOrWhiteSpace(mealName))
{
switch (mealType.ToLower())
{
case "lunch":
entry.LunchMealId = null;
entry.LunchMealName = null;
break;
case "middag":
entry.DinnerMealId = null;
entry.DinnerMealName = null;
break;
case "frukost":
entry.BreakfastMealId = null;
entry.BreakfastMealName = null;
break;
} }
continue;
}
if (!entriesByDay.TryGetValue(day, out var entry)) var meal = allMeals.FirstOrDefault(m => m.Name.Equals(mealName, StringComparison.OrdinalIgnoreCase))
{ ?? new Meal { Name = mealName };
entry = new WeeklyMenu
{
WeekNumber = model.WeekNumber,
Year = model.Year
};
entriesByDay[day] = entry;
}
entry.DayOfWeek = day + 1; // se till att alltid sätta rätt dag if (meal.Id == 0)
entry.Cook = cook; menuService.SaveOrUpdateMeal(meal);
if (mealType.Equals("Lunch", StringComparison.OrdinalIgnoreCase)) switch (mealType.ToLower())
{ {
case "lunch":
entry.LunchMealId = meal.Id; entry.LunchMealId = meal.Id;
entry.LunchMealName = meal.Name; entry.LunchMealName = meal.Name;
} break;
else if (mealType.Equals("Middag", StringComparison.OrdinalIgnoreCase)) case "middag":
{
entry.DinnerMealId = meal.Id; entry.DinnerMealId = meal.Id;
entry.DinnerMealName = meal.Name; entry.DinnerMealName = meal.Name;
} break;
else if (mealType.Equals("Frukost", StringComparison.OrdinalIgnoreCase)) case "frukost":
{
entry.BreakfastMealId = meal.Id; entry.BreakfastMealId = meal.Id;
entry.BreakfastMealName = meal.Name; entry.BreakfastMealName = meal.Name;
} break;
} }
} }
model.WeeklyMenus = entriesByDay.Values.ToList(); model.WeeklyMenus = entriesByDay.Values.ToList();
menuService.UpdateWeeklyMenu(model); menuService.UpdateWeeklyMenu(model);
return RedirectToAction("Veckomeny"); return RedirectToAction("Veckomeny", new { week, year });
} }
} }
}
}

View File

@@ -15,16 +15,62 @@ namespace Aberwyn.Controllers
_env = env; _env = env;
} }
[HttpGet] [HttpGet]
public IActionResult View(int id) public IActionResult View(int id, bool edit = false)
{ {
var service = new MenuService(_configuration, _env); var service = new MenuService(_configuration, _env);
var meal = service.GetMealById(id); var meal = service.GetMealById(id);
ViewData["IsEditing"] = edit; // → här
if (meal == null) if (meal == null)
{
return NotFound(); return NotFound();
}
return View("View", meal); return View("View", meal);
} }
[HttpGet]
[Route("Meal/Tooltip/{id}")]
public IActionResult Tooltip(int id)
{
try
{
var service = new MenuService(_configuration, _env);
var meal = service.GetMealById(id);
if (meal == null)
return Content("<div>Hittade ingen rätt.</div>", "text/html");
// Se till att Description aldrig är null
var desc = meal.Description ?? "";
// Gör en kort beskrivning
var shortDesc = desc.Length > 100
? desc.Substring(0, 100) + "…"
: desc;
// Bygg upp en enkel HTML-snutt
var html =
$"<div class='tooltip-content'>" +
$" <strong>{meal.Name}</strong><br/>" +
(string.IsNullOrWhiteSpace(shortDesc)
? "<em>Ingen beskrivning tillgänglig.</em>"
: $"<em>{shortDesc}</em>") +
"</div>";
return Content(html, "text/html");
}
catch (Exception ex)
{
// För felsökning kan du logga ex.Message, men för tillfället returnera det så vi ser det i devtools
return StatusCode(500, $"<pre>{ex.Message}</pre>");
}
}
[HttpGet] [HttpGet]
public IActionResult Edit(int? id) public IActionResult Edit(int? id)
{ {
@@ -34,30 +80,33 @@ namespace Aberwyn.Controllers
} }
[HttpPost] [HttpPost]
public async Task<IActionResult> SaveMeal(Meal meal, IFormFile ImageFile) public IActionResult SaveMeal(Meal meal, IFormFile ImageFile)
{ {
var service = new MenuService(_configuration, _env); var service = new MenuService(_configuration, _env);
if (ImageFile != null && ImageFile.Length > 0) if (ImageFile != null && ImageFile.Length > 0)
{ {
var uploadsFolder = Path.Combine(_env.ContentRootPath, "wwwroot/uploads"); var fileName = Path.GetFileNameWithoutExtension(ImageFile.FileName);
Directory.CreateDirectory(uploadsFolder); // skapa om saknas var extension = Path.GetExtension(ImageFile.FileName);
var uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(ImageFile.FileName); var uniqueName = $"{fileName}_{Guid.NewGuid()}{extension}";
var filePath = Path.Combine(uploadsFolder, uniqueFileName); var imagePath = Path.Combine("wwwroot/images/meals", uniqueName);
using (var fileStream = new FileStream(filePath, FileMode.Create)) using (var stream = new FileStream(imagePath, FileMode.Create))
{ {
await ImageFile.CopyToAsync(fileStream); ImageFile.CopyTo(stream);
} }
meal.ImageUrl = "/uploads/" + uniqueFileName; // Spara relativ sökväg för visning
meal.ImageUrl = $"/images/meals/{uniqueName}";
} }
service.SaveOrUpdateMeal(meal); service.SaveOrUpdateMeal(meal);
return RedirectToAction("Edit", new { id = meal.Id });
return RedirectToAction("View", new { id = meal.Id });
} }
[HttpPost] [HttpPost]
public IActionResult DeleteMeal(int id) public IActionResult DeleteMeal(int id)
{ {

View File

@@ -2,6 +2,8 @@
using Aberwyn.Models; using Aberwyn.Models;
using Aberwyn.Data; using Aberwyn.Data;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
namespace Aberwyn.Controllers namespace Aberwyn.Controllers
{ {
@@ -11,71 +13,45 @@ namespace Aberwyn.Controllers
{ {
private readonly MenuService _menuService; private readonly MenuService _menuService;
public MealMenuApiController(MenuService menuService) public MealMenuApiController(IConfiguration configuration, IHostEnvironment env)
{ {
_menuService = menuService; _menuService = new MenuService(configuration, env);
} }
// API endpoint to fetch the weekly menu
[HttpGet("menu")] [HttpGet("menu")]
public IActionResult GetMenu(int weekNumber, int year) public IActionResult GetMenu(int weekNumber, int year)
{ {
var menu = _menuService.GetWeeklyMenu(weekNumber, year); var menu = _menuService.GetWeeklyMenu(weekNumber, year);
return Ok(menu ?? new List<WeeklyMenu>());
if (menu == null || !menu.Any())
{
// Return an empty object or array instead of a 404 error
return Ok(new List<WeeklyMenu>()); // Return empty list if no menu is found
}
return Ok(menu); // Return the menu as a JSON response
} }
// API endpoint to fetch the weekly menu
[HttpGet("getMeals")] [HttpGet("getMeals")]
public IActionResult GetMeals() public IActionResult GetMeals()
{ {
var meals = _menuService.GetMeals(); var meals = _menuService.GetMeals();
return Ok(meals ?? new List<Meal>());
if (meals == null || !meals.Any())
{
// Return an empty object or array instead of a 404 error
return Ok(new List<Meal>()); // Return empty list if no menu is found
}
return Ok(meals); // Return the menu as a JSON response
} }
// API endpoint to save the updated menu (if you need this)
[HttpPut("menu")] [HttpPut("menu")]
public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu) public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu)
{ {
// Process and save the menu (you may need to write this logic)
_menuService.UpdateWeeklyMenu(weeklyMenu); _menuService.UpdateWeeklyMenu(weeklyMenu);
return Ok("Menu saved successfully"); return Ok("Menu saved successfully");
} }
[HttpPost("addMeal")] [HttpPost("addMeal")]
public IActionResult AddMeal([FromBody] Meal meal) public IActionResult AddMeal([FromBody] Meal meal)
{ {
if (meal == null || string.IsNullOrWhiteSpace(meal.Name)) if (meal == null || string.IsNullOrWhiteSpace(meal.Name))
{
return BadRequest("Meal Name is required."); return BadRequest("Meal Name is required.");
}
// Använd AddMeal som returnerar det nya ID:t
var mealId = _menuService.AddMeal(meal); var mealId = _menuService.AddMeal(meal);
if (mealId > 0) if (mealId > 0)
{
return Ok(new { Message = "Meal added successfully", MealId = mealId }); return Ok(new { Message = "Meal added successfully", MealId = mealId });
}
return StatusCode(500, "Failed to add meal."); return StatusCode(500, "Failed to add meal.");
} }
} }
} }

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Aberwyn.Models; using Aberwyn.Models;
using System.Globalization;
namespace Aberwyn.Data namespace Aberwyn.Data
{ {
@@ -197,7 +198,7 @@ namespace Aberwyn.Data
using var connection = GetConnection(); using var connection = GetConnection();
connection.Open(); connection.Open();
string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt string query = @"SELECT Id, Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl
FROM Meals WHERE Id = @id"; FROM Meals WHERE Id = @id";
using var cmd = new MySqlCommand(query, connection); using var cmd = new MySqlCommand(query, connection);
@@ -214,6 +215,7 @@ namespace Aberwyn.Data
ProteinType = reader.IsDBNull(reader.GetOrdinal("ProteinType")) ? null : reader.GetString(reader.GetOrdinal("ProteinType")), ProteinType = reader.IsDBNull(reader.GetOrdinal("ProteinType")) ? null : reader.GetString(reader.GetOrdinal("ProteinType")),
CarbType = reader.IsDBNull(reader.GetOrdinal("CarbType")) ? null : reader.GetString(reader.GetOrdinal("CarbType")), CarbType = reader.IsDBNull(reader.GetOrdinal("CarbType")) ? null : reader.GetString(reader.GetOrdinal("CarbType")),
RecipeUrl = reader.IsDBNull(reader.GetOrdinal("RecipeUrl")) ? null : reader.GetString(reader.GetOrdinal("RecipeUrl")), RecipeUrl = reader.IsDBNull(reader.GetOrdinal("RecipeUrl")) ? null : reader.GetString(reader.GetOrdinal("RecipeUrl")),
ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")),
CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt")) CreatedAt = reader.GetDateTime(reader.GetOrdinal("CreatedAt"))
}; };
@@ -234,44 +236,48 @@ namespace Aberwyn.Data
public void SaveOrUpdateMeal(Meal meal) public void SaveOrUpdateMeal(Meal meal)
{ {
using var connection = GetConnection(); using var conn = new MySqlConnection(_configuration.GetConnectionString("DefaultConnection"));
connection.Open(); conn.Open();
string query; using var cmd = conn.CreateCommand();
bool isNew = meal.Id == 0; if (meal.Id == 0)
if (isNew)
{ {
query = @"INSERT INTO Meals (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt) cmd.CommandText = @"INSERT INTO Meals
VALUES (@name, @desc, @protein, @carb, @url, @created)"; (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl)
VALUES (@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl);
SELECT LAST_INSERT_ID();";
} }
else else
{ {
query = @"UPDATE Meals cmd.CommandText = @"UPDATE Meals
SET Name = @name, Description = @desc, ProteinType = @protein, CarbType = @carb, RecipeUrl = @url SET Name = @Name, Description = @Description, ProteinType = @ProteinType,
WHERE Id = @id"; CarbType = @CarbType, RecipeUrl = @RecipeUrl, ImageUrl = @ImageUrl
WHERE Id = @Id";
cmd.Parameters.AddWithValue("@Id", meal.Id);
} }
using var cmd = new MySqlCommand(query, connection); cmd.Parameters.AddWithValue("@Name", meal.Name ?? "");
cmd.Parameters.AddWithValue("@name", meal.Name); cmd.Parameters.AddWithValue("@Description", meal.Description ?? "");
cmd.Parameters.AddWithValue("@desc", meal.Description ?? ""); cmd.Parameters.AddWithValue("@ProteinType", meal.ProteinType ?? "");
cmd.Parameters.AddWithValue("@protein", meal.ProteinType ?? ""); cmd.Parameters.AddWithValue("@CarbType", meal.CarbType ?? "");
cmd.Parameters.AddWithValue("@carb", meal.CarbType ?? ""); cmd.Parameters.AddWithValue("@RecipeUrl", meal.RecipeUrl ?? "");
cmd.Parameters.AddWithValue("@url", meal.RecipeUrl ?? ""); cmd.Parameters.AddWithValue("@CreatedAt", meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt);
cmd.Parameters.AddWithValue("@ImageUrl", meal.ImageUrl ?? "");
if (isNew) if (meal.Id == 0)
{ {
cmd.Parameters.AddWithValue("@created", meal.CreatedAt == default ? DateTime.Now : meal.CreatedAt); var insertedId = Convert.ToInt32(cmd.ExecuteScalar());
meal.Id = insertedId; // ← Sätt ID direkt på objektet
} }
else else
{ {
cmd.Parameters.AddWithValue("@id", meal.Id); cmd.ExecuteNonQuery();
} }
cmd.ExecuteNonQuery();
} }
public void UpdateWeeklyMenu(MenuViewModel menuData) public void UpdateWeeklyMenu(MenuViewModel menuData)
{ {
if (menuData == null || menuData.WeeklyMenus == null) if (menuData == null || menuData.WeeklyMenus == null)
@@ -384,6 +390,54 @@ namespace Aberwyn.Data
_ => throw new System.ArgumentException("Invalid day name") _ => throw new System.ArgumentException("Invalid day name")
}; };
} }
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
var results = new List<WeeklyMenu>();
// Hitta start- och slut-år/veckonummer
int startWeek = ISOWeek.GetWeekOfYear(startDate);
int startYear = startDate.Year;
int endWeek = ISOWeek.GetWeekOfYear(endDate);
int endYear = endDate.Year;
// Loopar över alla veckor från start → slut
var allMenus = new List<WeeklyMenu>();
int w = startWeek, y = startYear;
while (y < endYear || (y == endYear && w <= endWeek))
{
allMenus.AddRange(GetWeeklyMenu(w, y));
// öka vecka, hantera årsskifte
int weeksInYear = ISOWeek.GetWeeksInYear(y);
if (w == weeksInYear)
{
w = 1;
y++;
}
else
{
w++;
}
}
// Filtrera på datumintervall
foreach (var menu in allMenus)
{
// Konvertera DayOfWeek (1=Mon…7=Sun) till System.DayOfWeek
var dow = (DayOfWeek)(menu.DayOfWeek % 7);
var date = ISOWeek.ToDateTime(menu.Year, menu.WeekNumber, dow);
if (date.Date >= startDate.Date && date.Date <= endDate.Date)
{
menu.Date = date; // Se till att WeeklyMenu har en Date-property
results.Add(menu);
}
}
return results;
}
} }
} }

View File

@@ -24,9 +24,16 @@ namespace Aberwyn.Models
public string BreakfastMealName { get; set; } // 👈 Och denna public string BreakfastMealName { get; set; } // 👈 Och denna
public string LunchMealName { get; set; } public string LunchMealName { get; set; }
public string DinnerMealName { get; set; } public string DinnerMealName { get; set; }
public DateTime Date { get; set; }
} }
public class RecentMenuEntry
{
public DateTime Date { get; set; }
public string BreakfastMealName { get; set; }
public string LunchMealName { get; set; }
public string DinnerMealName { get; set; }
}
public class Meal public class Meal
{ {
public int Id { get; set; } public int Id { get; set; }

View File

@@ -5,18 +5,30 @@ using System.Linq;
namespace Aberwyn.Models namespace Aberwyn.Models
{ {
public class WeeklyMenuViewModel public class WeeklyMenuViewModel
{ {
public int WeekNumber { get; set; } public int WeekNumber { get; set; }
public int Year { get; set; } public int Year { get; set; }
public List<RecentMenuEntry> RecentEntries { get; set; } = new();
public List<WeeklyMenu> WeeklyMenus { get; set; } = new(); public List<WeeklyMenu> WeeklyMenus { get; set; } = new();
public List<User> AvailableCooks { get; set; } = new(); public List<User> AvailableCooks { get; set; } = new();
// Ny lista för översikt
public List<WeeklyMenuViewModel> PreviousWeeks { get; set; } = new();
public class RecentMenuEntry
{
public DateTime Date { get; set; }
public string BreakfastMealName { get; set; }
public string LunchMealName { get; set; }
public string DinnerMealName { get; set; }
}
public WeeklyMenu GetMealEntry(int day, string type) public WeeklyMenu GetMealEntry(int day, string type)
{ {
int dayOfWeek = day + 1; int dayOfWeek = day + 1;
return WeeklyMenus.FirstOrDefault(m => return WeeklyMenus.FirstOrDefault(m =>
m.DayOfWeek == dayOfWeek && m.DayOfWeek == dayOfWeek &&
( (
@@ -27,4 +39,6 @@ namespace Aberwyn.Models
); );
} }
} }
} }

View File

@@ -1,28 +1,36 @@
using Microsoft.EntityFrameworkCore; // Add this for DbContext using Microsoft.EntityFrameworkCore;
using Aberwyn.Data; // Include your data namespace using Aberwyn.Data;
using MySql.EntityFrameworkCore.Extensions; // For MySQL support using MySql.EntityFrameworkCore.Extensions;
using System.Text; using System.Text;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Add services to the container. // Add services to the container.
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews()
builder.Services.AddRazorPages(); // Add this line to enable Razor Pages .AddJsonOptions(opts =>
builder.Services.AddHttpClient(); // Register HttpClient {
// Behåll propertynamn som i C#-klassen (PascalCase)
opts.JsonSerializerOptions.PropertyNamingPolicy = null;
// Ignorera null-värden vid serialisering
opts.JsonSerializerOptions.IgnoreNullValues = true;
});
builder.Services.AddRazorPages();
builder.Services.AddHttpClient();
// Configure your DbContext with MySQL // Configure your DbContext with MySQL
builder.Services.AddDbContext<BudgetContext>(options => builder.Services.AddDbContext<BudgetContext>(options =>
options.UseMySQL(builder.Configuration.GetConnectionString("BudgetDb"))); options.UseMySQL(builder.Configuration.GetConnectionString("BudgetDb")));
// Register your services
// Register your BudgetService with a scoped lifetime
builder.Services.AddScoped<BudgetService>(); builder.Services.AddScoped<BudgetService>();
builder.Services.AddScoped<MenuService>(); builder.Services.AddScoped<MenuService>();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
builder.Services.Configure<RequestLocalizationOptions>(options => builder.Services.Configure<RequestLocalizationOptions>(options =>
{ {
@@ -31,6 +39,7 @@ builder.Services.Configure<RequestLocalizationOptions>(options =>
options.SupportedCultures = supportedCultures; options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures; options.SupportedUICultures = supportedCultures;
}); });
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -40,20 +49,19 @@ if (!app.Environment.IsDevelopment())
app.UseHsts(); app.UseHsts();
} }
//app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseRouting(); app.UseRouting();
app.UseAuthorization(); app.UseAuthorization();
// Map controller endpoints (including AJAX JSON actions)
app.MapControllers();
// Map default controller route // Conventional MVC route
app.MapControllerRoute( app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
// Map Razor Pages // Map Razor Pages
app.MapRazorPages(); // Add this line to map Razor Pages app.MapRazorPages();
app.Run(); app.Run();

View File

@@ -1,93 +1,229 @@
@model WeeklyMenuViewModel @using System.Globalization
@model Aberwyn.Models.WeeklyMenuViewModel
@{ @{
ViewData["Title"] = "Veckomeny"; ViewData["Title"] = "Veckomeny";
var days = new[] { "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag" }; var days = new[] { "Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag" };
var mealTypes = new[] { "Frukost", "Lunch", "Middag" }; var allMeals = ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>;
} }
<h1>Veckomeny - Vecka @Model.WeekNumber</h1> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<div class="week-nav">
<a asp-action="Veckomeny" asp-route-week="@(@Model.WeekNumber - 1)">Föregående vecka</a> <style>
<a asp-action="Veckomeny" asp-route-week="@(@Model.WeekNumber + 1)">Nästa vecka</a> [x-cloak] { display: none !important; }
[x-show] {
transition: all 0.3s ease-in-out;
overflow: hidden;
}
.rotate-chevron {
transition: transform 0.3s ease;
}
.rotate-chevron.open {
transform: rotate(90deg);
}
.meal-input {
padding: 4px 6px;
font-size: 0.85rem;
width: 100%;
border-radius: 8px;
border: 1px solid #546E7A;
background: #445A6E;
color: #ECEFF1;
}
.meal-input.new-entry {
border-color: #FFC107; /* Orange */
background: inherit; /* Lägg till detta om nödvändigt */
}
.meal-input.existing-entry {
border-color: #4CAF50; /* Grön */
background: inherit; /* Lägg till detta också */
}
.add-meal-wrapper {
display: flex;
align-items: flex-start;
gap: 12px;
margin-top: 12px;
}
.add-meal-wrapper input[type=text] {
width: 160px;
padding: 4px 6px;
font-size: 0.85rem;
}
.delete-btn {
background: none;
border: none;
cursor: pointer;
vertical-align: middle;
padding: 0;
}
.delete-btn i {
margin-left: 6px;
color: #FF8A65;
}
.week-nav a {
color: #FFEB3B;
text-decoration: none;
font-weight: bold;
}
.week-nav a:hover {
text-decoration: underline;
}
</style>
<script>
window.knownMeals = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
(ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>)?.Select(m => m.Name.Trim()).ToList() ?? new List<string>()));
</script>
<div class="weekly-menu-wrapper"
style="display:grid; grid-template-columns:1fr; gap:16px; padding:16px; background:#263238;"
x-data="{
highlightNew(event) {
const input = event.target;
const val = input.value?.trim().toLowerCase();
input.classList.remove('new-entry', 'existing-entry');
if (!val) return;
const isKnown = window.knownMeals.some(name => name.toLowerCase() === val);
input.classList.add(isKnown ? 'existing-entry' : 'new-entry');
}
}"
<!-- Rest of the content remains unchanged -->
<section class="weekly-editor">
<h1 style="color:#ECEFF1; margin-bottom:12px;">Veckomeny - Vecka @Model.WeekNumber</h1>
<div class="week-nav" style="margin-bottom:12px; display:flex; align-items:center; color:#B0BEC5; font-size:0.9rem;">
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber - 1)" asp-route-year="@Model.Year" style="margin-right:8px;">&larr; Föregående vecka</a>
<span style="font-weight:bold; color:#ECEFF1;">Vecka @Model.WeekNumber</span>
<a asp-action="Veckomeny" asp-route-week="@(Model.WeekNumber + 1)" asp-route-year="@Model.Year" style="margin-left:8px;">Nästa vecka &rarr;</a>
</div>
<form method="post" asp-action="SaveVeckomeny">
<input type="hidden" name="week" value="@Model.WeekNumber" />
<input type="hidden" name="year" value="@Model.Year" />
<table style="width:100%; border-collapse:separate; border-spacing:0 4px; font-size:0.9rem;">
<thead>
<tr style="background:#37474F; color:#ECEFF1;">
<th style="padding:8px 12px; text-align:left; border-radius:4px 0 0 4px;">Dag</th>
<th style="padding:8px 12px; text-align:left;">Middag</th>
<th style="padding:8px 12px; text-align:left;">Kock</th>
</tr>
</thead>
<tbody>
@for (int i = 0; i < 7; i++) {
var dinnerEntry = Model.GetMealEntry(i, "Middag");
var lunchEntry = Model.GetMealEntry(i, "Lunch");
var breakfastEntry = Model.GetMealEntry(i, "Frukost");
<tbody x-data="{ showExtra: false }">
<tr style="background:#2A3A48; color:#ECEFF1;">
<td style="padding:8px 12px; font-weight:bold; vertical-align:top; width:12%;">
<button type="button" x-on:click="showExtra = !showExtra" style="margin-right:6px; background:none; border:none; color:#ECEFF1; font-size:1rem; cursor:pointer;">
<i :class="showExtra ? 'fa-solid fa-chevron-right rotate-chevron open' : 'fa-solid fa-chevron-right rotate-chevron'"></i>
</button>
@days[i]
</td>
<td style="padding:8px 12px; vertical-align:top;">
<div style="display:flex; align-items:center; gap:4px;">
<input type="text" name="Meal[@i][Middag]" value="@dinnerEntry?.DinnerMealName" placeholder="Lägg till middag..." list="meals-list" class="meal-input" :tabindex="showExtra ? 0 : -1" x-on:input="highlightNew($event)" />
<button type="button" class="delete-btn" title="Rensa middag" onclick="this.previousElementSibling.value='';">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</td>
<td style="padding:8px 12px; vertical-align:top; width:14%;">
<select name="Cook[@i]" :tabindex="showExtra ? 0 : -1" style="width:100%; padding:6px; border-radius:8px; border:1px solid #546E7A; background:#445A6E; color:#ECEFF1; font-size:0.9rem;">
<option value="">Välj kock</option>
@foreach(var user in Model.AvailableCooks) {
var selected = Model.WeeklyMenus.FirstOrDefault(m => m.DayOfWeek == i+1)?.Cook == user.Username;
<option value="@user.Username" selected="@selected">@user.Name</option>
}
</select>
</td>
</tr>
<tr x-show="showExtra" x-transition x-cloak style="background:#2A3A48; color:#ECEFF1;">
<td></td>
<td colspan="2">
<div style="display:flex; flex-direction:column; gap:8px;">
<div style="display:flex; align-items:center; gap:4px;">
<label style="font-size:0.85rem; color:#B0BEC5; min-width:60px;">Frukost:</label>
<input type="text" name="Meal[@i][Frukost]" value="@breakfastEntry?.BreakfastMealName" placeholder="Lägg till frukost..." list="meals-list" class="meal-input" :tabindex="showExtra ? 0 : -1" x-on:input="highlightNew($event)" />
<button type="button" class="delete-btn" title="Rensa frukost" onclick="this.previousElementSibling.value='';">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div style="display:flex; align-items:center; gap:4px;">
<label style="font-size:0.85rem; color:#B0BEC5; min-width:60px;">Lunch:</label>
<input type="text" name="Meal[@i][Lunch]" value="@lunchEntry?.LunchMealName" placeholder="Lägg till lunch..." list="meals-list" class="meal-input" :tabindex="showExtra ? 0 : -1" x-on:input="highlightNew($event)" />
<button type="button" class="delete-btn" title="Rensa lunch" onclick="this.previousElementSibling.value='';">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
</td>
</tr>
</tbody>
}
</tbody>
</table>
<!-- Save button och övrigt -->
<div class="add-meal-wrapper">
<button type="submit" style="padding:8px 16px; border:none; border-radius:8px; background:#FFEB3B; font-weight:bold; font-size:0.9rem;">Spara veckomeny</button>
</div>
<aside class="recent-history" style="background:#37474F; border-radius:8px; color:#ECEFF1; padding:16px; margin-top:24px;">
<h2 style="margin:0 0 8px; font-size:1rem;">Översikt senaste 4 veckor</h2>
@{
var lastWeeks = Enumerable.Range(1, 4)
.Select(i => DateTime.Now.AddDays(-7 * i))
.Select(dt => new { Year = dt.Year, Week = ISOWeek.GetWeekOfYear(dt) })
.Distinct().ToList();
}
<table style="width:100%; border-collapse:collapse; font-size:0.85rem;">
<thead>
<tr style="background:#546E7A; color:#ECEFF1;">
<th style="padding:4px 8px;">Vecka</th>
@foreach (var d in days)
{
<th style="padding:4px 8px;">@d</th>
}
</tr>
</thead>
<tbody>
@foreach (var w in lastWeeks)
{
<tr style="background:#2A3A48; color:#ECEFF1;">
<td style="padding:4px 8px; font-weight:bold;">@w.Week</td>
@for (int idx = 1; idx <= 7; idx++)
{
var entry = Model.RecentEntries?.FirstOrDefault(e =>
e.Date.Year == w.Year &&
ISOWeek.GetWeekOfYear(e.Date) == w.Week &&
((int)e.Date.DayOfWeek == (idx % 7))
);
<td style="padding:4px 8px; white-space:nowrap;">@(entry?.DinnerMealName ?? "—")</td>
}
</tr>
}
</tbody>
</table>
</aside>
<datalist id="meals-list">
@foreach(var m in ViewBag.AvailableMeals as List<Aberwyn.Models.Meal>) {
<option value="@m.Name" id="@m.Name"></option>
}
</datalist>
</form>
</div> </div>
<form method="post" asp-action="SaveVeckomeny">
<table class="menu-table">
<thead>
<tr>
<th>Dag</th>
@foreach (var mealType in mealTypes)
{
<th>@mealType</th>
}
</tr>
</thead>
<tbody>
@for (int i = 0; i < 7; i++)
{
<tr>
<td>@days[i]</td>
@foreach (var mealType in mealTypes)
{
var entry = Model.GetMealEntry(i, mealType);
var mealName = mealType switch
{
"Frukost" => entry?.BreakfastMealName,
"Lunch" => entry?.LunchMealName,
"Middag" => entry?.DinnerMealName,
_ => null
};
<td>
<input type="text" name="Meal[@i][@mealType]"
value="@mealName"
placeholder="Lägg till måltid..."
class="meal-input" data-day="@i" data-type="@mealType" list="meals-list" />
<select name="Cook[@i][@mealType]">
<option value="">Vem lagar?</option>
@foreach (var user in Model.AvailableCooks)
{
if (entry?.Cook == user.Username)
{
<option value="@user.Username" selected>@user.Name</option>
}
else
{
<option value="@user.Username">@user.Name</option>
}
}
</select>
</td>
}
</tr>
}
</tbody>
</table>
<button type="submit">Spara veckomeny</button>
</form>
<datalist id="meals-list"></datalist>
@section Scripts {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(function() {
$('.meal-input').on('focus input', function() {
const input = $(this);
const term = input.val();
input.attr('list', 'meals-list'); // tvinga list-id
$.getJSON('/FoodMenu/SearchMeals', { term: term }, function(data) {
const list = $('#meals-list');
list.empty();
data.forEach(name => {
list.append(`<option value="${name}">`);
});
});
});
});
function showMealInfo(name, cook) {
alert(`Måltid: ${name}\nLagas av: ${cook}`);
}
</script>
}

View File

@@ -1,54 +1,97 @@
@model Aberwyn.Models.MenuViewModel @model Aberwyn.Models.MenuViewModel
@{ @{
// Define a flag to hide the sidebar if the URL path matches '/nosidebar' (relative part after the controller) var hideSidebar = Context.Request.Path.Value.EndsWith("/nosidebar", StringComparison.OrdinalIgnoreCase);
bool hideSidebar = Context.Request.Path.Value.EndsWith("/nosidebar", StringComparison.OrdinalIgnoreCase); Layout = hideSidebar ? null : "_Layout";
Layout = hideSidebar ? null : "_Layout"; // No layout if the path ends with '/nosidebar'
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" ng-app="mealMenuApp"> <html lang="sv" ng-app="mealMenuApp">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<title>Meal Menu Overview</title> <title>Veckomeny</title>
<!-- AngularJS -->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular-sanitize.js"></script>
<script src="~/js/menu.js"></script> <script src="~/js/menu.js"></script>
<link rel="stylesheet" type="text/css" href="~/css/meal-menu.css"> <!-- CSS -->
<link rel="stylesheet" href="~/css/meal-menu.css" />
<style>
.view-selector { position: absolute; top: 24px; right: 24px; }
.view-selector select { padding: 6px 10px; border-radius: 4px; border: none; background: #546E7A; color: #ECEFF1; font-size: 0.95rem; cursor: pointer; }
.list-view .day-item { background: #37474F; margin-bottom: 16px; padding: 16px; border-radius: 8px; }
.list-view .day-header { font-size: 1.4rem; color: #FFEB3B; margin-bottom: 8px; position: relative; }
.list-view .day-header::after { content: ''; position: absolute; bottom: -4px; left: 0; width: 40px; height: 3px; background: #FFEB3B; border-radius: 2px; }
.meal-selection { margin: 6px 0; font-size: 1rem; }
.meal-selection a { color: #ECEFF1; text-decoration: none; }
.meal-selection a:hover { text-decoration: underline; }
.not-assigned { color: #B0BEC5; font-style: italic; }
.card-view .card-container { display: grid; grid-template-columns: repeat(auto-fill,minmax(180px,1fr)); gap: 16px; }
.meal-card { position: relative; height: 180px; color: #fff; background-size: cover; background-position: center; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 8px rgba(0,0,0,0.3); display: flex; align-items: flex-end; cursor: pointer; }
.meal-card::before { content: ''; position: absolute; inset: 0; background: linear-gradient(to top,rgba(0,0,0,0.7),transparent); }
.card-content { position: relative; padding: 12px; z-index: 1; width: 100%; }
.card-content .day { font-size: 1.2rem; font-weight: bold; margin-bottom: 6px; }
.card-content .meal { font-size: 0.9rem; line-height: 1.2; }
</style>
</head> </head>
<body ng-controller="MealMenuController"> <body ng-controller="MealMenuController" ng-init="viewMode='list'">
<div class="meal-menu-page" style="position:relative">
<h1>Veckomeny</h1>
<div class="meal-menu-page"> <!-- Vecko-navigering -->
<h1 class="page-title">Meal Menu Overview</h1> <div class="date-picker">
<button ng-click="goToPreviousWeek()">← Föregående vecka</button>
<div class="date-picker"> <span>Vecka {{selectedWeek}} {{selectedYear}}</span>
<button type="button" class="date-btn" ng-click="goToPreviousWeek()">Föregående vecka</button> <button ng-click="goToNextWeek()">Nästa vecka</button>
<span class="week-info">Vecka {{ selectedWeek }} - {{ selectedYear }}</span>
<button type="button" class="date-btn" ng-click="goToNextWeek()">Nästa vecka</button>
</div>
<div class="meal-menu-container">
<div ng-repeat="day in daysOfWeek" class="day-item">
<div class="day-header">{{ day }}</div>
<div class="meal-info" ng-if="!isEditing">
<div ng-if="menu[day]">
<div ng-if="menu[day].breakfastMealName">
<span><strong>Frukost:</strong> {{ menu[day].breakfastMealName }}</span>
</div>
<div ng-if="menu[day].lunchMealName">
<span><strong>Lunch:</strong> {{ menu[day].lunchMealName }}</span>
</div>
<div ng-if="menu[day].dinnerMealName">
<span><strong>Middag:</strong> {{ menu[day].dinnerMealName }}</span>
</div>
</div>
<div ng-if="!menu[day]">
<span class="not-assigned">Inte bestämd</span>
</div>
</div>
</div>
</div>
</div> </div>
<!-- Vy-väljare -->
<div class="view-selector">
<label for="viewModeSelect">Visa:</label>
<select id="viewModeSelect" ng-model="viewMode">
<option value="list">Lista</option>
<option value="card">Kort</option>
</select>
</div>
<!-- Vy: List eller Kort -->
<div ng-switch="viewMode">
<div ng-switch-when="list" class="list-view">
<div ng-repeat="day in daysOfWeek" class="day-item">
<div class="day-header">{{day}}</div>
<div class="meal-info" ng-if="menu[day]">
<div ng-if="menu[day].breakfastMealId" class="meal-selection">
<a href="/Meal/View/{{menu[day].breakfastMealId}}" target="_blank"><strong>Frukost:</strong> {{menu[day].breakfastMealName}}</a>
</div>
<div ng-if="menu[day].lunchMealId" class="meal-selection">
<a href="/Meal/View/{{menu[day].lunchMealId}}" target="_blank"><strong>Lunch:</strong> {{menu[day].lunchMealName}}</a>
</div>
<div ng-if="menu[day].dinnerMealId" class="meal-selection">
<a href="/Meal/View/{{menu[day].dinnerMealId}}" target="_blank"><strong>Middag:</strong> {{menu[day].dinnerMealName}}</a>
</div>
</div>
<div ng-if="!menu[day]"><span class="not-assigned">Inte bestämd</span></div>
</div>
</div>
<div ng-switch-when="card" class="card-view">
<div class="card-container">
<div ng-repeat="day in daysOfWeek"
class="meal-card"
ng-style="{'background-image':'url('+getDayImage(day)+')'}"
ng-click="openMeal(getMealIdByDay(day))">
<div class="card-content">
<div class="day">{{day}}</div>
<div class="meal" ng-if="menu[day].breakfastMealName">Frukost: {{menu[day].breakfastMealName}}</div>
<div class="meal" ng-if="menu[day].lunchMealName">Lunch: {{menu[day].lunchMealName}}</div>
<div class="meal" ng-if="menu[day].dinnerMealName">Middag: {{menu[day].dinnerMealName}}</div>
<div class="meal" ng-if="!menu[day]">Inte bestämd</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body> </body>
</html> </html>

View File

@@ -40,7 +40,7 @@
<div> <div>
<label for="ImageFile">Bild</label> <label for="ImageFile">Bild</label>
<input type="file" name="ImageFile" /> <input type="file" name="ImageFile" value="@Model.ImageUrl" />
</div> </div>
<button type="submit">Spara</button> <button type="submit">Spara</button>

View File

@@ -1,90 +1,192 @@
@model Aberwyn.Models.Meal @model Aberwyn.Models.Meal
@{ @{
ViewData["Title"] = Model.Name; ViewData["Title"] = Model.Name;
bool isEditing = (bool)(ViewData["IsEditing"] ?? false);
var imageUrl = string.IsNullOrEmpty(Model.ImageUrl) ? "/images/placeholder-meal.jpg" : Model.ImageUrl;
} }
<div class="meal-container"> <div class="meal-container">
<h1>@Model.Name</h1> <div class="meal-header">
<img src="@imageUrl" alt="@Model.Name" class="meal-image" />
@if (!string.IsNullOrEmpty(Model.ImageUrl)) <div class="meal-meta">
{ <h1 class="meal-title">@Model.Name</h1>
<img src="@Model.ImageUrl" alt="@Model.Name" class="meal-image" /> <p class="description">@Model.Description</p>
} </div>
@if (!string.IsNullOrEmpty(Model.Description))
{
<p class="description">@Model.Description</p>
}
<div class="buttons">
<button onclick="toggleRecipe()">Visa Recept</button>
<a class="edit-button" asp-controller="Meal" asp-action="Edit" asp-route-id="@Model.Id">Redigera</a>
</div> </div>
<div id="recipe-section" style="display:none;"> @if (isEditing)
<h2>Så här gör du</h2> {
<p>(Tillagningsinstruktioner kommer snart...)</p> <form asp-action="SaveMeal" method="post" enctype="multipart/form-data">
<input type="hidden" name="Id" value="@Model.Id" />
<div class="form-group">
<label for="ImageFile">Bild</label>
<input type="file" name="ImageFile" accept="image/*" value="@Model.ImageUrl" class="form-control" />
</div>
<div class="form-group">
<label for="Name">Namn</label>
<input type="text" name="Name" value="@Model.Name" class="form-control" required />
</div>
<div class="form-group">
<label for="Description">Beskrivning</label>
<textarea name="Description" class="form-control">@Model.Description</textarea>
</div>
<div class="form-group">
<label for="ProteinType">Protein</label>
<input type="text" name="ProteinType" value="@Model.ProteinType" class="form-control" />
</div>
<div class="form-group">
<label for="CarbType">Kolhydrat</label>
<input type="text" name="CarbType" value="@Model.CarbType" class="form-control" />
</div>
<div class="form-group">
<label for="RecipeUrl">Receptlänk</label>
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" class="form-control" />
</div>
<div class="buttons">
<button type="submit" class="btn">Spara</button>
<button type="submit" formaction="@Url.Action("DeleteMeal", new { id = Model.Id })" formmethod="post" onclick="return confirm('Vill du verkligen ta bort denna måltid?');" class="btn-outline">Ta bort</button>
<a href="@Url.Action("View", new { id = Model.Id, edit = false })" class="btn-outline">Avbryt</a>
</div>
</form>
}
else
{
<div class="meal-details">
@if (!string.IsNullOrEmpty(Model.ProteinType))
{
<p><span class="label">Protein:</span> @Model.ProteinType</p>
}
@if (!string.IsNullOrEmpty(Model.CarbType))
{
<p><span class="label">Kolhydrat:</span> @Model.CarbType</p>
}
</div>
@if (!string.IsNullOrEmpty(Model.RecipeUrl)) @if (!string.IsNullOrEmpty(Model.RecipeUrl))
{ {
<p><a href="@Model.RecipeUrl" target="_blank">Öppna fullständigt recept</a></p> <p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Visa Recept</a></p>
}
<div class="buttons">
<button type="button" class="btn-outline" onclick="toggleRecipe()">Visa Tillagning</button>
<a class="btn-outline" href="@Url.Action("View", new { id = Model.Id, edit = true })">Redigera</a>
</div>
}
<div id="recipe-section" style="display:none; margin-top:1.5rem;">
<h2>Så här gör du</h2>
<p class="placeholder">(Tillagningsinstruktioner kommer snart...)</p>
@if (!string.IsNullOrEmpty(Model.RecipeUrl))
{
<p><a href="@Model.RecipeUrl" class="recipe-link" target="_blank">Gå till fullständigt recept</a></p>
} }
</div> </div>
</div> </div>
<script> <script>
function toggleRecipe() { function toggleRecipe() {
var section = document.getElementById('recipe-section'); const section = document.getElementById('recipe-section');
if (section.style.display === 'none') { section.style.display = section.style.display === 'none' ? 'block' : 'none';
section.style.display = 'block';
} else {
section.style.display = 'none';
}
} }
</script> </script>
<style> <style>
.meal-container { .meal-container {
max-width: 700px; max-width: 900px;
margin: 2rem auto; margin: 2rem auto;
background: #fff; background: #fff;
padding: 2rem; padding: 2rem;
border-radius: 10px; border-radius: 16px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
text-align: center; font-family: 'Segoe UI', sans-serif;
} }
.meal-container h1 { .meal-header {
display: flex;
flex-direction: row;
gap: 1.5rem;
align-items: center;
}
.meal-meta {
flex: 1;
}
.meal-title {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 1rem; margin-bottom: 1rem;
color: #333;
} }
.meal-container .meal-image { .meal-image {
width: 100%; width: 250px;
max-height: 400px; height: 250px;
object-fit: cover; object-fit: cover;
border-radius: 10px; border-radius: 12px;
margin-bottom: 1rem; border: 1px solid #ccc;
} }
.meal-container .description { .description {
font-style: italic; font-size: 1.1rem;
color: #555;
margin-top: 0.5rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.meal-container .buttons { .meal-details p {
margin-bottom: 2rem; font-size: 1rem;
color: #333;
margin: 0.3rem 0;
} }
.meal-container .buttons button, .meal-container .buttons .edit-button { .label {
font-weight: bold;
color: #6a0dad;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
font-weight: 600;
margin-bottom: 0.3rem;
display: block;
}
.form-control {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 1rem;
}
.buttons {
margin-top: 1.5rem;
}
.btn,
.btn-outline {
background-color: #6a0dad; background-color: #6a0dad;
color: white; color: white;
border: none; border: none;
padding: 0.7rem 1.5rem; padding: 0.6rem 1.4rem;
border-radius: 5px; border-radius: 6px;
margin: 0.5rem; margin: 0.4rem;
text-decoration: none;
cursor: pointer; cursor: pointer;
font-size: 1rem;
text-decoration: none;
} }
.meal-container .buttons .edit-button { .btn-outline {
display: inline-block; background-color: transparent;
border: 2px solid #6a0dad;
color: #6a0dad;
} }
.meal-container a { .recipe-link {
color: #6a0dad; color: #6a0dad;
text-decoration: underline; text-decoration: underline;
font-weight: 500;
}
.placeholder {
color: #999;
font-style: italic;
} }
</style> </style>

View File

@@ -0,0 +1,8 @@
@model dynamic
<div class="tooltip-content">
<strong>@Model.Name</strong><br />
<em>@Model.ShortDescription</em><br />
@if (Model.Ingredients != null) {
<div><u>Ingredienser:</u> @String.Join(", ", Model.Ingredients)</div>
}
</div>

View File

@@ -208,3 +208,7 @@
.new-meal-form button:hover { .new-meal-form button:hover {
background-color: #F57C00; background-color: #F57C00;
} }
.meal-hover {
cursor: pointer;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,99 +1,84 @@
angular.module('mealMenuApp', []) angular.module('mealMenuApp', ['ngSanitize'])
.controller('MealMenuController', function ($scope, $http) { .controller('MealMenuController', function ($scope, $http, $sce) {
$scope.isEditing = false; console.log("Controller initierad");
$scope.toggleEditMode = function () {
$scope.isEditing = !$scope.isEditing;
};
$scope.viewMode = 'list';
$scope.tooltip = {};
$scope.meals = []; $scope.meals = [];
$scope.menu = {}; $scope.menu = {};
$scope.showNewMealForm = false;
$scope.newMeal = {};
$scope.selectedMealDay = null;
$scope.selectedMealType = null;
const today = new Date(); const today = new Date();
$scope.selectedWeek = getWeek(today); $scope.selectedWeek = getWeek(today);
$scope.selectedYear = today.getFullYear(); $scope.selectedYear = today.getFullYear();
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; $scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
$scope.loadMeals = function () { $scope.loadMeals = function () {
$http.get('/api/mealMenuApi/getMeals') console.log("Hämtar måltider...");
.then(response => { return $http.get('/api/mealMenuApi/getMeals')
$scope.meals = response.data; .then(res => {
console.log("Måltider hämtade:", res.data);
$scope.meals = res.data;
return res;
}) })
.catch(error => console.error("Error fetching meals:", error)); .catch(err => console.error("Fel vid hämtning av måltider:", err));
}; };
$scope.loadMenu = function () { $scope.loadMenu = function () {
console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
$http.get('/api/mealMenuApi/menu', { $http.get('/api/mealMenuApi/menu', {
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear } params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
}).then(response => { }).then(res => {
console.log("Veckomenydata:", response.data); // ✅ logga ut console.log("Menyposter hämtade:", res.data);
$scope.menu = {}; $scope.menu = {};
response.data.forEach(item => {
const dayOfWeek = $scope.daysOfWeek[item.dayOfWeek - 1];
if (!$scope.menu[dayOfWeek]) $scope.menu[dayOfWeek] = {};
if (item.breakfastMealId) { res.data.forEach(item => {
$scope.menu[dayOfWeek].breakfastMealId = item.breakfastMealId; const dayIndex = item.DayOfWeek - 1;
$scope.menu[dayOfWeek].breakfastMealName = getMealNameById(item.breakfastMealId); if (dayIndex < 0 || dayIndex >= $scope.daysOfWeek.length) {
} console.warn("Ogiltig dag:", item.DayOfWeek);
if (item.lunchMealId) { return;
$scope.menu[dayOfWeek].lunchMealId = item.lunchMealId;
$scope.menu[dayOfWeek].lunchMealName = getMealNameById(item.lunchMealId);
}
if (item.dinnerMealId) {
$scope.menu[dayOfWeek].dinnerMealId = item.dinnerMealId;
$scope.menu[dayOfWeek].dinnerMealName = getMealNameById(item.dinnerMealId);
} }
const day = $scope.daysOfWeek[dayIndex];
$scope.menu[day] = {};
['breakfast', 'lunch', 'dinner'].forEach(type => {
// Konvertera till PascalCase
const capitalType = type.charAt(0).toUpperCase() + type.slice(1);
const idKey = capitalType + 'MealId';
const nameKey = capitalType + 'MealName';
if (item[idKey]) {
const m = $scope.meals.find(x => x.Id === item[idKey]);
console.log(`Match för ${type} (${day}):`, m);
$scope.menu[day][type + 'MealId'] = item[idKey];
$scope.menu[day][type + 'MealName'] = m?.Name || item[nameKey] || 'Okänd rätt';
if (m?.ImageUrl) {
$scope.menu[day].imageUrl = m.ImageUrl;
}
}
});
}); });
}).catch(error => console.error("Error fetching weekly menu:", error));
console.log("Byggd meny:", $scope.menu);
}).catch(err => console.error("Fel vid hämtning av veckomeny:", err));
}; };
function getMealNameById(mealId) { $scope.openMeal = function (mealId) {
const meal = $scope.meals.find(m => m.id === mealId); if (!mealId) return;
return meal ? meal.name : "Unknown Meal"; window.open('/Meal/View/' + mealId, '_blank');
}
$scope.handleMealSelection = function (day, mealType) {
if ($scope.menu[day][mealType + "MealId"] === "new") {
$scope.showNewMealForm = true;
$scope.newMeal = { name: "", description: "", proteinType: "", carbType: "", recipeUrl: "" };
$scope.selectedMealDay = day;
$scope.selectedMealType = mealType;
}
}; };
$scope.saveNewMeal = function () { $scope.getDayImage = function (day) {
if (!$scope.newMeal.name) { const item = $scope.menu[day];
alert("Meal name is required"); return (item && item.imageUrl) || '/images/default-meal.jpg';
return;
}
$http.post('/api/mealMenuApi/addMeal', $scope.newMeal)
.then(response => {
const addedMeal = response.data;
$scope.meals.push(addedMeal);
$scope.menu[$scope.selectedMealDay][$scope.selectedMealType + "MealId"] = addedMeal.id;
$scope.showNewMealForm = false;
$scope.newMeal = {}; // Reset new meal data after save
})
.catch(error => console.error("Error saving new meal:", error));
}; };
$scope.cancelNewMeal = function () { $scope.getMealIdByDay = function (day) {
$scope.showNewMealForm = false; const item = $scope.menu[day] || {};
$scope.newMeal = {}; // Reset new meal data when canceled return item.dinnerMealId || item.lunchMealId || item.breakfastMealId || null;
}; };
function getWeek(date) {
const day = date.getDay() || 7;
date.setDate(date.getDate() + 4 - day);
const yearStart = new Date(date.getFullYear(), 0, 1);
return Math.ceil(((date - yearStart) / 86400000 + 1) / 7);
}
$scope.goToPreviousWeek = function () { $scope.goToPreviousWeek = function () {
if ($scope.selectedWeek === 1) { if ($scope.selectedWeek === 1) {
$scope.selectedYear--; $scope.selectedYear--;
@@ -114,6 +99,17 @@
$scope.loadMenu(); $scope.loadMenu();
}; };
$scope.loadMeals(); function getWeek(d) {
$scope.loadMenu(); d = new Date(d.getFullYear(), d.getMonth(), d.getDate());
const dayNum = d.getDay() || 7;
d.setDate(d.getDate() + 4 - dayNum);
const yearStart = new Date(d.getFullYear(), 0, 1);
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
console.log("Initierar måltidsladdning...");
$scope.loadMeals().then(() => {
console.log("Laddar meny efter måltider...");
$scope.loadMenu();
});
}); });