Lots of fixes
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
elias
2025-06-15 10:49:04 +02:00
parent 113cce73ad
commit 76656bb6a8
19 changed files with 1395 additions and 46 deletions

View File

@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace Aberwyn.Controllers
{
public class ErrorController : Controller
{
[Route("Error/{statusCode}")]
public IActionResult HttpStatusCodeHandler(int statusCode)
{
ViewData["ErrorCode"] = statusCode;
return View("Error");
}
[Route("Error")]
public IActionResult Error()
{
var exceptionFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
ViewData["Exception"] = exceptionFeature?.Error;
return View("Error");
}
}
}

View File

@@ -32,12 +32,12 @@ namespace Aberwyn.Controllers
[HttpGet]
public IActionResult PizzaOrder()
{
var pizzas = _menuService.GetMealsByCategory("Pizza")
.Where(p => p.IsAvailable)
.ToList();
var meals = _menuService.GetMealsByCategoryName("Pizza", onlyAvailable: true);
Console.WriteLine("Pizzas: ", meals);
var dtoList = meals.Select(m => MealListDto.FromMeal(m)).ToList();
ViewBag.Pizzas = dtoList;
ViewBag.Pizzas = pizzas;
ViewBag.RestaurantIsOpen = GetRestaurantStatus();
ViewBag.RestaurantIsOpen = GetRestaurantStatus();
int? lastId = HttpContext.Session.GetInt32("LastPizzaOrderId");
if (lastId.HasValue)
@@ -53,6 +53,7 @@ namespace Aberwyn.Controllers
}
[HttpGet]
public IActionResult EditPizzaOrder(int id)
{
@@ -152,10 +153,14 @@ namespace Aberwyn.Controllers
var allMeals = _menuService.GetMeals();
var categories = _menuService.GetMealCategories();
var pizzaCategory = categories.FirstOrDefault(c => c.Name.Equals("Pizza", StringComparison.OrdinalIgnoreCase));
viewModel.AvailablePizzas = allMeals
.Where(m => m.Category == "Pizza")
.OrderBy(m => m.Name)
.ToList();
.Where(m => m.MealCategoryId == pizzaCategory?.Id)
.OrderBy(m => m.Name)
.ToList();
ViewBag.RestaurantIsOpen = GetRestaurantStatus();

View File

@@ -29,7 +29,7 @@ namespace Aberwyn.Controllers
ViewBag.RestaurantIsOpen = isOpen;
var now = DateTime.Now;
var showDate = now.Hour >= 18 ? now.Date.AddDays(1) : now.Date;
var showDate = now.Hour >= 20 ? now.Date.AddDays(1) : now.Date;
var todaysMenu = _menuService.GetMenuForDate(showDate);

View File

@@ -45,7 +45,10 @@ namespace Aberwyn.Controllers
CreatedAt = DateTime.Now
};
}
ViewBag.Categories = _menuService.GetMealCategories()
.Where(c => c.IsActive)
.OrderBy(c => c.Name)
.ToList();
ViewData["IsEditing"] = edit;
return View("View", meal);
}
@@ -164,5 +167,35 @@ namespace Aberwyn.Controllers
//service.DeleteMeal(id);
return RedirectToAction("Edit"); // eller tillbaka till lista
}
[Authorize(Roles = "Admin,Chef")]
[HttpGet("/meal/categories")]
public IActionResult Categories()
{
var categories = _menuService.GetMealCategories()
.Select(cat => {
cat.MealCount = _menuService.GetMealCountForCategory(cat.Id);
return cat;
}).ToList();
return View("MealCategories", categories);
}
[Authorize(Roles = "Admin,Chef")]
[HttpPost("/meal/categories/save")]
public IActionResult SaveCategory(MealCategory category)
{
_menuService.SaveOrUpdateCategory(category);
return RedirectToAction("Categories");
}
[Authorize(Roles = "Admin,Chef")]
[HttpPost("/meal/categories/delete")]
public IActionResult DeleteCategory(int id)
{
_menuService.DeleteCategory(id);
return RedirectToAction("Categories");
}
}
}

View File

@@ -16,7 +16,20 @@ namespace Aberwyn.Data
base.OnModelCreating(builder);
builder.Entity<WeeklyMenu>().ToTable("WeeklyMenu");
builder.Entity<MealCategory>().HasData(
new MealCategory
{
Id = 1,
Name = "Pizza",
Slug = "pizza",
Icon = "🍕",
Color = "#f97316",
IsActive = true,
DisplayOrder = 1
}
);
}
public DbSet<BudgetPeriod> BudgetPeriods { get; set; }
@@ -33,6 +46,7 @@ namespace Aberwyn.Data
public DbSet<Ingredient> Ingredients { get; set; }
public DbSet<UserPreferences> UserPreferences { get; set; }
public DbSet<StoredPushSubscription> PushSubscriptions { get; set; }
public DbSet<MealCategory> MealCategories { get; set; }
}
}

View File

@@ -292,7 +292,7 @@ public List<Meal> GetMealsSlim(bool includeThumbnail = false)
public List<Meal> GetMealsByCategory(string category)
{
return _context.Meals
.Where(m => m.Category == category)
//.Where(m => m.Category == category)
.Include(m => m.Ingredients)
.OrderBy(m => m.Name)
.ToList();
@@ -383,6 +383,27 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
return menu;
}
public List<Meal> GetMealsByCategoryName(string categoryName, string? searchTerm = null, bool onlyAvailable = false)
{
var query = _context.Meals
.Include(m => m.Category)
.Include(m => m.Ingredients)
.Where(m => m.Category != null && m.Category.Name == categoryName);
if (onlyAvailable)
query = query.Where(m => m.IsAvailable);
if (!string.IsNullOrWhiteSpace(searchTerm))
{
string lowered = searchTerm.Trim().ToLower();
query = query.Where(m => m.Name.ToLower().Contains(lowered));
}
return query
.OrderBy(m => m.Name)
.ToList();
}
public List<WeeklyMenu> GetMenuEntriesByDateRange(DateTime startDate, DateTime endDate)
{
@@ -414,5 +435,47 @@ public List<WeeklyMenu> GetWeeklyMenu(int weekNumber, int year)
}
return results;
}
public List<MealCategory> GetMealCategories()
{
return _context.MealCategories.OrderBy(c => c.DisplayOrder).ToList();
}
public int GetMealCountForCategory(int categoryId)
{
return _context.Meals.Count(m => m.MealCategoryId == categoryId);
}
public void SaveOrUpdateCategory(MealCategory cat)
{
if (cat.Id == 0)
{
_context.MealCategories.Add(cat);
}
else
{
var existing = _context.MealCategories.Find(cat.Id);
if (existing != null)
{
existing.Name = cat.Name;
existing.Slug = cat.Slug;
existing.Description = cat.Description;
existing.Icon = cat.Icon;
existing.Color = cat.Color;
existing.IsActive = cat.IsActive;
existing.DisplayOrder = cat.DisplayOrder;
}
}
_context.SaveChanges();
}
public void DeleteCategory(int id)
{
var cat = _context.MealCategories.Find(id);
if (cat != null)
{
_context.MealCategories.Remove(cat);
_context.SaveChanges();
}
}
}
}

View File

@@ -0,0 +1,821 @@
// <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("20250614211356_AddMealCategorySupport")]
partial class AddMealCategorySupport
{
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<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<int?>("MealCategoryId")
.HasColumnType("int");
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.HasIndex("MealCategoryId");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.MealCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.HasColumnType("longtext");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Slug")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("MealCategories");
});
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.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", 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.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("PushSubscriptions");
});
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.UserPreferences", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<bool>("NotifyBudget")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyMenu")
.HasColumnType("tinyint(1)");
b.Property<bool>("NotifyPizza")
.HasColumnType("tinyint(1)");
b.HasKey("UserId");
b.ToTable("UserPreferences");
});
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("Aberwyn.Models.Meal", b =>
{
b.HasOne("Aberwyn.Models.MealCategory", "Category")
.WithMany("Meals")
.HasForeignKey("MealCategoryId");
b.Navigation("Category");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Aberwyn.Models.UserPreferences", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
.WithOne("Preferences")
.HasForeignKey("Aberwyn.Models.UserPreferences", "UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
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.ApplicationUser", b =>
{
b.Navigation("Preferences")
.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");
});
modelBuilder.Entity("Aberwyn.Models.MealCategory", b =>
{
b.Navigation("Meals");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddMealCategorySupport : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -267,9 +267,6 @@ namespace Aberwyn.Migrations
b.Property<string>("CarbType")
.HasColumnType("longtext");
b.Property<string>("Category")
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
@@ -291,6 +288,9 @@ namespace Aberwyn.Migrations
b.Property<bool>("IsAvailable")
.HasColumnType("tinyint(1)");
b.Property<int?>("MealCategoryId")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
@@ -306,9 +306,44 @@ namespace Aberwyn.Migrations
b.HasKey("Id");
b.HasIndex("MealCategoryId");
b.ToTable("Meals");
});
modelBuilder.Entity("Aberwyn.Models.MealCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.HasColumnType("longtext");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<string>("Icon")
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Slug")
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("MealCategories");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
@@ -660,6 +695,15 @@ namespace Aberwyn.Migrations
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.Meal", b =>
{
b.HasOne("Aberwyn.Models.MealCategory", "Category")
.WithMany("Meals")
.HasForeignKey("MealCategoryId");
b.Navigation("Category");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", "User")
@@ -764,6 +808,11 @@ namespace Aberwyn.Migrations
{
b.Navigation("Ingredients");
});
modelBuilder.Entity("Aberwyn.Models.MealCategory", b =>
{
b.Navigation("Meals");
});
#pragma warning restore 612, 618
}
}

View File

@@ -13,8 +13,8 @@ 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 class WeeklyMenuDto
{
public int Id { get; set; }
public int DayOfWeek { get; set; }
@@ -73,7 +73,10 @@ public class WeeklyMenu
public string? Description { get; set; }
public string? ProteinType { get; set; }
public string? Category { get; set; }
public int? MealCategoryId { get; set; }
[ForeignKey("MealCategoryId")]
public MealCategory? Category { get; set; }
public string? CarbType { get; set; }
public string? RecipeUrl { get; set; }
public string? ImageUrl { get; set; }
@@ -122,24 +125,57 @@ public class WeeklyMenu
}
public class MealListDto
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string? Description { get; set; }
public string? ThumbnailData { get; set; }
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string? Description { get; set; }
public string? ThumbnailData { get; set; }
public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false)
public List<IngredientDto> Ingredients { get; set; } = new();
public static MealListDto FromMeal(Meal meal, bool includeThumbnail = false)
{
return new MealListDto
{
return new MealListDto
Id = meal.Id,
Name = meal.Name,
Description = meal.Description,
ThumbnailData = includeThumbnail && meal.ThumbnailData != null
? Convert.ToBase64String(meal.ThumbnailData)
: null,
Ingredients = meal.Ingredients?.Select(i => new IngredientDto
{
Id = meal.Id,
Name = meal.Name,
Description = meal.Description,
ThumbnailData = includeThumbnail && meal.ThumbnailData != null
? Convert.ToBase64String(meal.ThumbnailData)
: null
};
}
Item = i.Item
}).ToList() ?? new List<IngredientDto>()
};
}
public class IngredientDto
{
public string Item { get; set; } = "";
}
}
public class MealCategory
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public string? Slug { get; set; }
public string? Description { get; set; }
public string? Icon { get; set; }
public string? Color { get; set; }
public bool IsActive { get; set; } = true;
public int DisplayOrder { get; set; } = 0;
[NotMapped] // krävs om du inte har kolumnen i databasen
public int MealCount { get; set; }
public List<Meal> Meals { get; set; } = new();
}
}

View File

@@ -191,14 +191,6 @@ app.Use(async (context, next) =>
// Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseRouting();
app.UseSession();
app.UseAuthentication();
@@ -211,6 +203,11 @@ app.MapControllerRoute(
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.UseExceptionHandler("/Error");
app.UseStatusCodePagesWithReExecute("/Error/{0}");
app.UseHsts();
// Init: migrera databas och skapa admin
if (setup.IsConfigured)
{

View File

@@ -14,7 +14,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:7290;http://localhost:5290;http://0.0.0.0:5000"
"applicationUrl": "https://localhost:7290;http://localhost:5290"
},
"IIS Express": {
"commandName": "IISExpress",

View File

@@ -0,0 +1,75 @@
@{
Layout = "_Layout";
var errorCode = ViewData["ErrorCode"] as int?;
var exception = ViewData["Exception"] as Exception;
var isDebug = System.Diagnostics.Debugger.IsAttached;
}
<style>
.error-page {
max-width: 600px;
margin: 4rem auto;
padding: 2rem;
background-color: #1F2C3C;
color: white;
border-radius: 16px;
box-shadow: 0 0 10px rgba(0,0,0,0.4);
font-family: 'Segoe UI', sans-serif;
}
.error-page h1 {
font-size: 2.2rem;
margin-bottom: 1rem;
color: #f87171;
}
.error-page .code {
font-size: 1.5rem;
margin-bottom: 1rem;
color: #facc15;
}
.error-page pre {
background-color: #2E3C4F;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin-top: 1rem;
color: #fefefe;
font-size: 0.95rem;
}
.error-page details summary {
cursor: pointer;
color: #6a0dad;
margin-top: 1rem;
font-weight: bold;
}
.error-page p {
margin-top: 0.5rem;
}
</style>
<div class="error-page">
<h1>🚨 Något gick fel</h1>
<a href="/" class="btn-outline">Till startsidan</a>
@if (errorCode.HasValue)
{
<div class="code">Statuskod: <strong>@errorCode</strong></div>
}
@if (exception != null)
{
<h3>Felmeddelande:</h3>
<pre>@exception.Message</pre>
@if (isDebug)
{
<details>
<summary>Visa stacktrace</summary>
<pre>@exception.StackTrace</pre>
</details>
}
}
else
{
<p>Vi kunde tyvärr inte visa sidan. Det kan bero på ett tillfälligt fel.</p>
}
</div>

View File

@@ -2,7 +2,7 @@
@using Newtonsoft.Json.Serialization
@{
ViewData["Title"] = "Beställ pizza";
var pizzas = ViewBag.Pizzas as List<Meal>;
var pizzas = ViewBag.Pizzas as List<MealListDto> ?? new List<MealListDto>();
var previousOrder = ViewBag.PreviousOrder as PizzaOrder;
var lastId = Context.Session.GetInt32("LastPizzaOrderId");
var isCurrentOrder = previousOrder?.Id == lastId;

View File

@@ -13,7 +13,7 @@
<span class="initial W">W</span>
<span class="initial E2">E</span>
<span class="initial L2">L</span></h1>
<p class="welcome-subtitle">Idag är det <strong>@day</strong> dagens meny är:</p>
<p class="welcome-subtitle">Meny för <strong>@day</strong> är:</p>
@if (Model != null)
{

View File

@@ -0,0 +1,74 @@
@model List<Aberwyn.Models.MealCategory>
@{
ViewData["Title"] = "Måltidskategorier";
}
<h1 class="page-title">Måltidskategorier</h1>
<div class="category-list">
@foreach (var cat in Model)
{
<form method="post" action="/meal/categories/save" class="category-row">
<input type="hidden" name="Id" value="@cat.Id" />
<div class="name-with-preview">
<div class="icon-preview" style="background-color: @cat.Color">
@if (!string.IsNullOrWhiteSpace(cat.Icon))
{
<span>@cat.Icon</span>
}
else
{
<span>🍽️</span>
}
</div>
<input name="Name" value="@cat.Name" placeholder="Namn" />
</div>
<input name="Icon" value="@cat.Icon" placeholder="Ikon (emoji eller klass)" />
<input name="Slug" value="@cat.Slug" placeholder="Slug" />
<input name="Color" value="@cat.Color" type="color" title="Färg" />
<label class="checkbox-label">
<input type="checkbox" name="IsActive" @(cat.IsActive ? "checked" : "") />
Aktiv
</label>
<span class="meal-count">@cat.MealCount st</span>
<div class="row-actions">
<button type="submit" title="Spara"><i class="fas fa-save"></i></button>
<form method="post" action="/meal/categories/delete" style="display:inline;">
<input type="hidden" name="id" value="@cat.Id" />
<button type="submit" class="delete" title="Ta bort"><i class="fas fa-trash"></i></button>
</form>
</div>
</form>
}
<!-- Ny kategori -->
<form method="post" action="/meal/categories/save" class="category-row new">
<div class="name-with-preview">
<div class="icon-preview" style="background-color: #6a0dad"></div>
<input name="Name" placeholder="Ny kategori" autofocus />
</div>
<input name="Icon" placeholder="Ikon" />
<input name="Slug" placeholder="Slug" />
<input name="Color" type="color" value="#6a0dad" />
<label class="checkbox-label">
<input type="checkbox" name="IsActive" checked />
Aktiv
</label>
<span></span>
<div class="row-actions">
<button type="submit" title="Lägg till"><i class="fas fa-plus"></i></button>
</div>
</form>
</div>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<link rel="stylesheet" href="/css/meal-categories.css">

View File

@@ -14,7 +14,7 @@
}
else
{
imageSrc = "/images/placeholder-meal.jpg";
imageSrc = "/images/fallback.jpg";
}
bool isChef = HttpContextAccessor.HttpContext.User.IsInRole("Chef");
@@ -64,11 +64,16 @@
<label for="CarbType">Kolhydrat</label>
<input type="text" name="CarbType" value="@Model.CarbType" class="form-control" />
</div>
<div class="form-group">
<label for="Category">Kategori</label>
<input type="text" name="Category" value="@Model.Category" class="form-control" />
<label for="MealCategoryId">Kategori</label>
<select asp-for="MealCategoryId" asp-items="@(new SelectList((List<MealCategory>)ViewBag.Categories, "Id", "Name", Model.MealCategoryId))" class="form-control">
<option value="">-- Välj kategori --</option>
</select>
</div>
<div class="form-group">
<label for="RecipeUrl">Receptlänk</label>
<input type="url" name="RecipeUrl" value="@Model.RecipeUrl" class="form-control" />
@@ -171,6 +176,30 @@
<script src="https://cdn.jsdelivr.net/npm/trumbowyg@2.25.1/dist/trumbowyg.min.js"></script>
<script>
function addIngredientRow() {
const list = document.getElementById('ingredients-list');
const index = list.children.length;
const div = document.createElement('div');
div.className = 'ingredient-row';
const qtyInput = document.createElement('input');
qtyInput.type = 'text';
qtyInput.name = `Ingredients[${index}].Quantity`;
qtyInput.placeholder = 'Mängd';
qtyInput.className = 'form-control ingredient-qty';
const itemInput = document.createElement('input');
itemInput.type = 'text';
itemInput.name = `Ingredients[${index}].Item`;
itemInput.placeholder = 'Ingrediens';
itemInput.className = 'form-control ingredient-item';
div.appendChild(qtyInput);
div.appendChild(itemInput);
list.appendChild(div);
}
function toggleRecipe() {
const section = document.getElementById('recipe-section');
if (!section) return;

View File

@@ -57,6 +57,7 @@
<ul class="dropdown-menu">
<li><a asp-controller="FoodMenu" asp-action="Veckomeny">Planera Veckomeny</a></li>
<li><a asp-controller="FoodMenu" asp-action="PizzaAdmin">Pizza Admin</a></li>
<li><a asp-controller="Meal" asp-action="Categories">Hantera Kategorier</a></li>
</ul>
</li>
}

View File

@@ -0,0 +1,110 @@
.page-title {
font-size: 2rem;
color: white;
margin-bottom: 1rem;
text-align: center;
}
.category-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 1200px;
margin: auto;
}
.category-row {
display: grid;
grid-template-columns: 2.5fr 1.2fr 1.2fr 1fr auto auto auto;
gap: 0.5rem;
background-color: #1F2C3C;
padding: 0.75rem 1rem;
border-radius: 8px;
align-items: center;
color: white;
}
.category-row.new {
background-color: #2E3C4F;
border: 2px dashed #6a0dad;
}
.category-row input[type="text"],
.category-row input[type="color"],
.category-row input:not([type="checkbox"]) {
padding: 0.4rem;
border: none;
border-radius: 6px;
background-color: #334155;
color: #fefefe;
width: 100%;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.9rem;
color: #ddd;
justify-content: center;
}
.row-actions {
display: flex;
gap: 0.5rem;
}
.row-actions button {
background-color: #6a0dad;
border: none;
color: white;
padding: 0.4rem 0.6rem;
border-radius: 6px;
cursor: pointer;
}
.row-actions button.delete {
background-color: #ef4444;
}
.row-actions i {
pointer-events: none;
}
.name-with-preview {
display: flex;
align-items: center;
gap: 0.5rem;
}
.icon-preview {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: #6a0dad;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: white;
flex-shrink: 0;
border: 2px solid #ffffff22;
}
.meal-count {
font-size: 0.85rem;
color: #94a3b8;
text-align: right;
white-space: nowrap;
}
@media (max-width: 768px) {
.category-row {
grid-template-columns: 1fr;
}
.meal-count {
text-align: left;
margin-top: 0.25rem;
}
}