Optimized menu
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
elias
2025-06-05 20:15:11 +02:00
parent eba00ffbf2
commit bc77e39c72
15 changed files with 922 additions and 101 deletions

1
.gitignore vendored
View File

@@ -364,3 +364,4 @@ FodyWeavers.xsd
# Setupfil för Aberwyn
infrastructure/setup.json
Aberwyn/Infrastructure/setup.json

View File

@@ -45,6 +45,7 @@
<!-- Övrigt -->
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="6.0.18" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.9" />
<PackageReference Include="WebPush" Version="1.0.12" />
</ItemGroup>

View File

@@ -11,6 +11,7 @@ namespace Aberwyn.Controllers
[Authorize(Roles = "Admin")]
public class AdminController : Controller
{
private readonly MenuService _menuService;
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly IConfiguration _configuration;
@@ -18,12 +19,14 @@ namespace Aberwyn.Controllers
private readonly ApplicationDbContext _context;
public AdminController(
MenuService menuService,
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IConfiguration configuration,
IHostEnvironment env,
ApplicationDbContext context)
{
_menuService = menuService;
_userManager = userManager;
_roleManager = roleManager;
_configuration = configuration;
@@ -144,6 +147,13 @@ namespace Aberwyn.Controllers
TempData["Message"] = $"✅ Import av veckomenyer från extern databas klar ({sourceMenus.Count}).";
return RedirectToAction("Index");
}
[HttpPost]
[Authorize(Roles = "Admin")]
public IActionResult GenerateThumbnails()
{
var count = _menuService.GenerateMissingThumbnails();
return Ok($"{count} thumbnails skapades.");
}
[HttpPost]

View File

@@ -2,6 +2,8 @@
using Aberwyn.Models;
using Aberwyn.Data;
using Microsoft.AspNetCore.Authorization;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp;
namespace Aberwyn.Controllers
{
@@ -70,6 +72,15 @@ namespace Aberwyn.Controllers
return StatusCode(500, $"<pre>{ex.Message}</pre>");
}
}
[HttpGet("Meal/Thumbnail/{id}")]
public IActionResult Thumbnail(int id)
{
var meal = _menuService.GetMealById(id);
if (meal == null || meal.ThumbnailData == null)
return NotFound();
return File(meal.ThumbnailData, "image/webp"); // eller image/jpeg om du använder det
}
@@ -93,6 +104,8 @@ namespace Aberwyn.Controllers
ImageFile.CopyTo(ms);
meal.ImageData = ms.ToArray();
meal.ImageMimeType = ImageFile.ContentType;
meal.ThumbnailData = GenerateThumbnail(ImageFile);
}
else
{
@@ -111,8 +124,21 @@ namespace Aberwyn.Controllers
}
private byte[] GenerateThumbnail(IFormFile file)
{
using var image = SixLabors.ImageSharp.Image.Load(file.OpenReadStream());
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(300, 300) // eller vad du vill för korten
}));
using var ms = new MemoryStream();
image.SaveAsWebp(ms); // kräver ImageSharp.Webp-paketet
return ms.ToArray();
}
[HttpPost]

View File

@@ -33,6 +33,16 @@ namespace Aberwyn.Controllers
return Ok(mealDtos);
}
[HttpGet("getWeeklyMenu")]
public IActionResult GetWeeklyMenu(int weekNumber, int year)
{
var menuDtos = _menuService.GetWeeklyMenuDto(weekNumber, year);
Console.WriteLine("Hämtar meals: " + menuDtos);
return Ok(menuDtos);
}
[HttpPut("menu")]
public IActionResult SaveMenu([FromBody] MenuViewModel weeklyMenu)

View File

@@ -1,6 +1,9 @@
// Nya versionen av MenuService med Entity Framework
using Aberwyn.Models;
using Microsoft.EntityFrameworkCore;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
using System.Globalization;
using static Aberwyn.Data.SetupService;
@@ -26,6 +29,37 @@ public class MenuService
return new MenuService(context);
}
public List<WeeklyMenuDto> GetWeeklyMenuDto(int weekNumber, int year)
{
var query = from wm in _context.WeeklyMenus
where wm.WeekNumber == weekNumber && wm.Year == year
join mDinner in _context.Meals on wm.DinnerMealId equals mDinner.Id into dinnerJoin
from mDinner in dinnerJoin.DefaultIfEmpty()
join mLunch in _context.Meals on wm.LunchMealId equals mLunch.Id into lunchJoin
from mLunch in lunchJoin.DefaultIfEmpty()
join mBreakfast in _context.Meals on wm.BreakfastMealId equals mBreakfast.Id into breakfastJoin
from mBreakfast in breakfastJoin.DefaultIfEmpty()
select new WeeklyMenuDto
{
Id = wm.Id,
DayOfWeek = wm.DayOfWeek,
WeekNumber = wm.WeekNumber,
Year = wm.Year,
BreakfastMealId = wm.BreakfastMealId,
LunchMealId = wm.LunchMealId,
DinnerMealId = wm.DinnerMealId,
BreakfastMealName = mBreakfast.Name,
LunchMealName = mLunch.Name,
DinnerMealName = mDinner.Name,
DinnerMealThumbnail = mDinner.ThumbnailData
};
return query.ToList();
}
public void UpdateWeeklyMenu(MenuViewModel model)
{
var existing = _context.WeeklyMenus
@@ -70,6 +104,38 @@ public class MenuService
_context.SaveChanges();
}
public int GenerateMissingThumbnails()
{
var updatedCount = 0;
var meals = _context.Meals
.Where(m => m.ImageData != null && m.ThumbnailData == null)
.ToList();
foreach (var meal in meals)
{
using var ms = new MemoryStream(meal.ImageData);
using var image = Image.Load(ms);
image.Mutate(x => x.Resize(new ResizeOptions
{
Mode = ResizeMode.Max,
Size = new Size(300, 300)
}));
using var outStream = new MemoryStream();
var encoder = new WebpEncoder
{
Quality = 75
};
image.Save(outStream, encoder);
meal.ThumbnailData = outStream.ToArray();
updatedCount++;
}
_context.SaveChanges();
return updatedCount;
}
public List<WeeklyMenu> GetAllWeeklyMenus()
{
@@ -166,33 +232,49 @@ public class MenuService
.OrderBy(m => m.Name)
.ToList();
}
public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
{
var menus = _context.WeeklyMenus
.Where(m => m.WeekNumber == weekNumber && m.Year == year)
.ToList();
public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
{
var menus = _context.WeeklyMenus
.Where(m => m.WeekNumber == weekNumber && m.Year == year)
.ToList();
var mealIds = menus
.SelectMany(w => new int?[] { w.BreakfastMealId, w.LunchMealId, w.DinnerMealId })
.Where(id => id.HasValue)
.Select(id => id.Value)
.Distinct()
.ToList();
var allMeals = _context.Meals
.Where(m => mealIds.Contains(m.Id))
.Select(m => new {
m.Id,
m.Name,
m.ThumbnailData // Vi tar med detta även om det bara används för middag
})
.ToList()
.ToDictionary(m => m.Id, m => m);
var allMeals = _context.Meals.ToDictionary(m => m.Id, m => m.Name);
foreach (var wm in menus)
{
wm.BreakfastMealName = wm.BreakfastMealId.HasValue && allMeals.TryGetValue(wm.BreakfastMealId.Value, out var breakfast)
? breakfast
: null;
if (wm.BreakfastMealId.HasValue && allMeals.TryGetValue(wm.BreakfastMealId.Value, out var breakfast))
wm.BreakfastMealName = breakfast.Name;
wm.LunchMealName = wm.LunchMealId.HasValue && allMeals.TryGetValue(wm.LunchMealId.Value, out var lunch)
? lunch
: null;
if (wm.LunchMealId.HasValue && allMeals.TryGetValue(wm.LunchMealId.Value, out var lunch))
wm.LunchMealName = lunch.Name;
wm.DinnerMealName = wm.DinnerMealId.HasValue && allMeals.TryGetValue(wm.DinnerMealId.Value, out var dinner)
? dinner
: null;
if (wm.DinnerMealId.HasValue && allMeals.TryGetValue(wm.DinnerMealId.Value, out var dinner))
{
wm.DinnerMealName = dinner.Name;
}
}
return menus;
}
return menus;
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
var results = new List<WeeklyMenu>();

View File

@@ -0,0 +1,679 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20250605062804_AddThumbnailToMeal")]
partial class AddThumbnailToMeal
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.36")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("Aberwyn.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Item")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("MealId")
.HasColumnType("int");
b.Property<string>("Quantity")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("MealId");
b.ToTable("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<byte[]>("ImageData")
.HasColumnType("longblob");
b.Property<string>("ImageMimeType")
.HasColumnType("longtext");
b.Property<string>("ImageUrl")
.HasColumnType("longtext");
b.Property<string>("Instructions")
.HasColumnType("longtext");
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("ProteinType")
.HasColumnType("longtext");
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.TodoTask", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("AssignedTo")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsArchived")
.HasColumnType("tinyint(1)");
b.Property<int>("Priority")
.HasColumnType("int");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Tags")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("TodoTasks");
});
modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BreakfastMealId")
.HasColumnType("int");
b.Property<string>("Cook")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int>("DayOfWeek")
.HasColumnType("int");
b.Property<int?>("DinnerMealId")
.HasColumnType("int");
b.Property<int?>("LunchMealId")
.HasColumnType("int");
b.Property<int>("WeekNumber")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WeeklyMenu", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
.WithMany()
.HasForeignKey("BudgetCategoryDefinitionId");
b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod")
.WithMany("Categories")
.HasForeignKey("BudgetPeriodId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BudgetPeriod");
b.Navigation("Definition");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory")
.WithMany("Items")
.HasForeignKey("BudgetCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
.WithMany()
.HasForeignKey("BudgetItemDefinitionId");
b.Navigation("BudgetCategory");
b.Navigation("BudgetItemDefinition");
});
modelBuilder.Entity("Aberwyn.Models.Ingredient", b =>
{
b.HasOne("Aberwyn.Models.Meal", null)
.WithMany("Ingredients")
.HasForeignKey("MealId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.Navigation("Ingredients");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddThumbnailToMeal : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "ThumbnailData",
table: "Meals",
type: "longblob",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ThumbnailData",
table: "Meals");
}
}
}

View File

@@ -301,6 +301,9 @@ namespace Aberwyn.Migrations
b.Property<string>("RecipeUrl")
.HasColumnType("longtext");
b.Property<byte[]>("ThumbnailData")
.HasColumnType("longblob");
b.HasKey("Id");
b.ToTable("Meals");

View File

@@ -13,6 +13,25 @@ namespace Aberwyn.Models
public int WeekNumber { get; set; } // Week number for the menu
public int Year { get; set; } // Year for the menu
}
public class WeeklyMenuDto
{
public int Id { get; set; }
public int DayOfWeek { get; set; }
public int WeekNumber { get; set; }
public int Year { get; set; }
public int? BreakfastMealId { get; set; }
public int? LunchMealId { get; set; }
public int? DinnerMealId { get; set; }
public string? BreakfastMealName { get; set; }
public string? LunchMealName { get; set; }
public string? DinnerMealName { get; set; }
public byte[]? DinnerMealThumbnail { get; set; }
}
public class WeeklyMenu
{
public int Id { get; set; }
@@ -55,12 +74,14 @@ public class Meal
public string? CarbType { get; set; }
public string? RecipeUrl { get; set; }
public string? ImageUrl { get; set; }
public bool IsAvailable { get; set; }
public DateTime CreatedAt { get; set; }
public byte[]? ThumbnailData { get; set; }
public byte[]? ImageData { get; set; } // 👈 Viktigt!
public string? ImageMimeType { get; set; } // 👈 Viktigt!
public string? Instructions { get; set; } // 👈 Viktigt!
public byte[]? ImageData { get; set; }
public string? ImageMimeType { get; set; }
public string? Instructions { get; set; }
public List<Ingredient> Ingredients { get; set; } = new();
}

View File

@@ -70,6 +70,8 @@
}
</tbody>
</table>
<button ng-click="generateThumbnails()">Generera thumbnails</button>
<h3>Testa Pushnotis</h3>
<form onsubmit="sendPush(event)">

View File

@@ -56,10 +56,10 @@
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 class="meal" ng-if="menu[day].lunchMealName">Lunch: {{menu[day].lunchMealName}}</div>
<div class="meal" ng-if="menu[day].breakfastMealName">Frukost: {{menu[day].breakfastMealName}}</div>
<div class="meal" ng-if="menu[day] && !menu[day].breakfastMealName && !menu[day].lunchMealName && !menu[day].dinnerMealName">Inte bestämd</div>
</div>
</div>
</div>

View File

@@ -33,7 +33,7 @@
{
<ul class="nav-list">
<li><a asp-controller="Home" asp-action="Index"><i class="fas fa-home"></i> Home</a></li>
@if (User.IsInRole("Admin") || User.IsInRole("Budget"))
@if (User.IsInRole("Budget"))
{
<li><a asp-controller="Budget" asp-action="Index"><i class="fas fa-wallet"></i> Budget</a></li>
}
@@ -42,7 +42,7 @@
{
<li><a asp-controller="FoodMenu" asp-action="PizzaOrder"><i class="fas fa-pizza-slice"></i> Beställ pizza</a></li>
}
@if (User.IsInRole("Admin") || User.IsInRole("Chef"))
@if (User.IsInRole("Chef"))
{
<li><a asp-controller="FoodMenu" asp-action="Veckomeny"><i class="fas fa-calendar-week"></i> Administrera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="MealAdmin"><i class="fas fa-list"></i> Måltider</a></li>

View File

@@ -215,14 +215,15 @@ h1 {
flex-direction: column;
justify-content: flex-start;
align-items: center;
background: rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.3); /* ev. öka från 0.4 till 0.5 */
text-align: center;
box-sizing: border-box;
}
.card-content .day {
font-size: 1rem;
font-size: 1.3rem;
font-weight: bold;
text-shadow: 0 1px 7px rgba(0, 0, 0, 1);
margin-bottom: 6px;
white-space: nowrap;
overflow: hidden;
@@ -230,9 +231,14 @@ h1 {
}
.card-content .meal {
font-size: 0.8rem;
font-size: 1rem;
line-height: 1.3;
margin-bottom: 2px;
text-shadow:
0 0 4px rgba(0, 0, 0, 0.9),
0 0 8px rgba(0, 0, 0, 0.7),
0 1px 10px rgba(0, 0, 0, 1);
word-break: break-word;
}
.menu-header {

View File

@@ -4,79 +4,46 @@
}
angular.module('mealMenuApp', ['ngSanitize'])
.controller('MealMenuController', function ($scope, $http, $sce) {
.controller('MealMenuController', function ($scope, $http, $sce, $timeout) {
console.log("Controller initierad");
const savedView = localStorage.getItem('mealViewMode');
$scope.viewMode = savedView === 'card' || savedView === 'list' ? savedView : 'card';
$scope.tooltip = {};
$scope.meals = [];
$scope.menu = {};
const today = new Date();
$scope.selectedWeek = getWeek(today);
$scope.selectedYear = today.getFullYear();
$scope.daysOfWeek = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
$scope.loadMeals = function () {
console.log("Hämtar måltider...");
return $http.get('/api/mealMenuApi/getMeals')
.then(res => {
console.log("Måltider hämtade:", res.data);
$scope.meals = res.data;
console.log("Alla måltider med bilder:", $scope.meals);
return res;
})
.catch(err => console.error("Fel vid hämtning av måltider:", err));
};
$scope.loadMenu = function () {
console.log("Hämtar meny för vecka:", $scope.selectedWeek, $scope.selectedYear);
$http.get('/api/mealMenuApi/menu', {
$http.get('/api/mealMenuApi/getWeeklyMenu', {
params: { weekNumber: $scope.selectedWeek, year: $scope.selectedYear }
}).then(res => {
console.log("Menyposter hämtade:", res.data);
const rawMenu = res.data;
$scope.menu = {};
res.data.forEach(item => {
rawMenu.forEach(item => {
const dayIndex = item.DayOfWeek - 1;
if (dayIndex < 0 || dayIndex >= $scope.daysOfWeek.length) {
console.warn("Ogiltig dag:", item.DayOfWeek);
return;
}
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 && !$scope.menu[day].imageUrl) {
if (m.ImageData) {
const mime = m.ImageMimeType || "image/jpeg";
$scope.menu[day].imageUrl = `data:${mime};base64,${m.ImageData}`;
} else if (m.ImageUrl) {
$scope.menu[day].imageUrl = m.ImageUrl.startsWith('/')
? m.ImageUrl
: '/' + m.ImageUrl;
}
}
}
});
console.warn("Item:", item);
console.log("day: " + day + "(" + dayIndex + " ) item: " + item.DinnerMealName);
const thumb = item.DinnerMealThumbnail;
$scope.menu[day] = {
breakfastMealId: item.BreakfastMealId,
breakfastMealName: item.BreakfastMealName,
lunchMealId: item.LunchMealId,
lunchMealName: item.LunchMealName,
dinnerMealId: item.DinnerMealId,
dinnerMealName: item.DinnerMealName,
imageUrl: thumb ? `data:image/webp;base64,${thumb}` : '/img/default-thumbnail.webp'
};
});
console.log("Byggd meny:", $scope.menu);
}).catch(err => console.error("Fel vid hämtning av veckomeny:", err));
}).catch(err => console.error("Fel vid hämtning av meny:", err));
};
$scope.openMeal = function (mealId) {
@@ -85,7 +52,7 @@ angular.module('mealMenuApp', ['ngSanitize'])
};
$scope.getDayImage = function (day) {
return $scope.menu[day]?.imageUrl || '/images/default-meal.jpg';
return $scope.menu[day]?.imageUrl || '/img/default-thumbnail.webp';
};
$scope.getMealIdByDay = function (day) {
@@ -120,34 +87,24 @@ angular.module('mealMenuApp', ['ngSanitize'])
const yearStart = new Date(d.getFullYear(), 0, 1);
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
}
$scope.getViewIcon = function () {
return $scope.viewMode === 'list' ? '🗒️' : '▣';
};
$scope.toggleView = function () {
$scope.viewMode = $scope.viewMode === 'list' ? 'card' : 'list';
localStorage.setItem('mealViewMode', $scope.viewMode); // ← spara läget
localStorage.setItem('mealViewMode', $scope.viewMode);
$timeout(() => {
const viewBtn = document.getElementById('toggle-view');
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
}, 0);
};
console.log("Initierar måltidsladdning...");
$scope.loadMeals().then(() => {
console.log("Laddar meny efter måltider...");
$scope.loadMenu();
setTimeout(() => {
const viewBtn = document.getElementById('toggle-view');
if (viewBtn) viewBtn.textContent = $scope.getViewIcon();
}, 0);
});
console.log("Initierar meny och måltidsladdning...");
$scope.loadMenu();
});
document.addEventListener("DOMContentLoaded", function () {
const themeBtn = document.getElementById('toggle-theme');
const viewBtn = document.getElementById('toggle-view');
@@ -168,7 +125,4 @@ document.addEventListener("DOMContentLoaded", function () {
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(systemPrefersDark ? 'dark' : 'light');
}
// Initiera ikon för vy
});