diff --git a/Aberwyn/Controllers/BudgetApiController.cs b/Aberwyn/Controllers/BudgetApiController.cs index 245ca67..ee3d206 100644 --- a/Aberwyn/Controllers/BudgetApiController.cs +++ b/Aberwyn/Controllers/BudgetApiController.cs @@ -18,6 +18,7 @@ namespace Aberwyn.Controllers _context = context; } + [HttpGet("{year:int}/{month:int}")] public async Task GetBudget(int year, int month) { @@ -123,6 +124,7 @@ namespace Aberwyn.Controllers existing.Amount = incoming.Amount; existing.IsExpense = incoming.IsExpense; existing.IncludeInSummary = incoming.IncludeInSummary; + existing.Order = incoming.Order; } } } diff --git a/Aberwyn/Controllers/FoodMenuController.cs b/Aberwyn/Controllers/FoodMenuController.cs index feb5b12..429c84a 100644 --- a/Aberwyn/Controllers/FoodMenuController.cs +++ b/Aberwyn/Controllers/FoodMenuController.cs @@ -31,8 +31,12 @@ namespace Aberwyn.Controllers [HttpGet] public IActionResult PizzaOrder() { - var pizzas = _menuService.GetMealsByCategory("Pizza"); + var pizzas = _menuService.GetMealsByCategory("Pizza") + .Where(p => p.IsAvailable) + .ToList(); + ViewBag.Pizzas = pizzas; + ViewBag.RestaurantIsOpen = GetRestaurantStatus(); int? lastId = HttpContext.Session.GetInt32("LastPizzaOrderId"); if (lastId.HasValue) @@ -47,6 +51,7 @@ namespace Aberwyn.Controllers return View(); } + [HttpGet] public IActionResult EditPizzaOrder(int id) { @@ -138,18 +143,27 @@ namespace Aberwyn.Controllers .OrderBy(o => o.OrderedAt) .ToList(); - ViewBag.ActiveOrders = allOrders - .Where(o => o.Status != "Finished") - .ToList(); + var viewModel = new PizzaAdminViewModel + { + ActiveOrders = allOrders.Where(o => o.Status != "Finished").ToList(), + CompletedOrders = allOrders.Where(o => o.Status == "Finished").ToList() + }; - ViewBag.CompletedOrders = allOrders - .Where(o => o.Status == "Finished") - .ToList(); + var allMeals = _menuService.GetMeals(); - return View(); + viewModel.AvailablePizzas = allMeals + .Where(m => m.Category == "Pizza") + .OrderBy(m => m.Name) + .ToList(); + + ViewBag.RestaurantIsOpen = GetRestaurantStatus(); + + return View(viewModel); } + + [HttpPost] [Authorize(Roles = "Chef")] public IActionResult UpdatePizzaOrder(int id, string status, string ingredientsJson) @@ -354,6 +368,91 @@ namespace Aberwyn.Controllers return RedirectToAction("Veckomeny", new { week, year }); } + private bool GetRestaurantStatus() + { + var value = _context.AppSettings.FirstOrDefault(s => s.Key == "RestaurantIsOpen")?.Value; + return bool.TryParse(value, out var isOpen) && isOpen; + } + + + private void SetRestaurantStatus(bool isOpen) + { + var setting = _context.AppSettings.FirstOrDefault(s => s.Key == "RestaurantIsOpen"); + if (setting == null) + { + setting = new AppSetting { Key = "RestaurantIsOpen", Value = isOpen.ToString().ToLower() }; + _context.AppSettings.Add(setting); + } + else + { + setting.Value = isOpen.ToString().ToLower(); + _context.AppSettings.Update(setting); + } + _context.SaveChanges(); + } + [HttpPost] + [Authorize(Roles = "Chef")] + public IActionResult ToggleRestaurant(bool isOpen) + { + var existing = _context.AppSettings.FirstOrDefault(s => s.Key == "RestaurantIsOpen"); + if (existing != null) + { + existing.Value = isOpen.ToString(); + } + else + { + _context.AppSettings.Add(new AppSetting { Key = "RestaurantIsOpen", Value = isOpen.ToString() }); + } + _context.SaveChanges(); + + return RedirectToAction("PizzaAdmin"); + } + [Authorize(Roles = "Chef")] + public IActionResult PizzaAdminPartial() + { + var today = DateTime.Today; + var tomorrow = today.AddDays(1); + + var allOrders = _context.PizzaOrders + .Where(o => o.OrderedAt >= today && o.OrderedAt < tomorrow) + .OrderBy(o => o.OrderedAt) + .ToList(); + + var allMeals = _menuService.GetMealsByCategory("Pizza"); + + var vm = new PizzaAdminViewModel + { + ActiveOrders = allOrders.Where(o => o.Status != "Finished").ToList(), + CompletedOrders = allOrders.Where(o => o.Status == "Finished").ToList(), + AvailablePizzas = allMeals, + RestaurantIsOpen = GetRestaurantStatus() + }; + + return PartialView("_PizzaAdminPartial", vm); + } + + + [HttpPost] + [Authorize(Roles = "Chef")] + public IActionResult UpdatePizzaAvailability(List availableIds) + { + availableIds ??= new List(); // Om null, ersätt med tom lista + + var allPizzas = _menuService.GetMealsByCategory("Pizza"); + + foreach (var meal in allPizzas) + { + var isAvailable = availableIds.Contains(meal.Id); + if (meal.IsAvailable != isAvailable) + { + meal.IsAvailable = isAvailable; + _menuService.SaveOrUpdateMeal(meal); + } + } + + return RedirectToAction("PizzaAdmin"); + } + } } diff --git a/Aberwyn/Controllers/HomeController.cs b/Aberwyn/Controllers/HomeController.cs index 7395ef3..38f94e9 100644 --- a/Aberwyn/Controllers/HomeController.cs +++ b/Aberwyn/Controllers/HomeController.cs @@ -12,17 +12,21 @@ namespace Aberwyn.Controllers private readonly ILogger _logger; //private readonly BudgetService _budgetService; private readonly MenuService _menuService; + private readonly ApplicationDbContext _context; // Constructor to inject dependencies - public HomeController(ILogger logger, MenuService menuService) + public HomeController(ApplicationDbContext applicationDbContext, ILogger logger, MenuService menuService) { _logger = logger; _menuService = menuService; + _context = applicationDbContext; } public IActionResult Index() { + var isOpen = _context.AppSettings.FirstOrDefault(x => x.Key == "RestaurantIsOpen")?.Value == "True"; + ViewBag.RestaurantIsOpen = isOpen; return View(); } diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index b8b01d7..12c6b0f 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -16,6 +16,7 @@ namespace Aberwyn.Data public DbSet BudgetItems { get; set; } public DbSet PushSubscribers { get; set; } public DbSet PizzaOrders { get; set; } + public DbSet AppSettings { get; set; } } } diff --git a/Aberwyn/Data/MenuService.cs b/Aberwyn/Data/MenuService.cs index 2e1f6ba..ee9f72f 100644 --- a/Aberwyn/Data/MenuService.cs +++ b/Aberwyn/Data/MenuService.cs @@ -190,27 +190,31 @@ namespace Aberwyn.Data using (var connection = GetConnection()) { connection.Open(); - string query = "SELECT Id, Name, ImageUrl, ImageData, ImageMimeType FROM Meals"; + string query = @" + SELECT Id, Name, ImageUrl, ImageData, ImageMimeType, Category, IsAvailable + FROM Meals"; + using (var cmd = new MySqlCommand(query, connection)) + using (var reader = cmd.ExecuteReader()) { - using (var reader = cmd.ExecuteReader()) + while (reader.Read()) { - while (reader.Read()) + meals.Add(new Meal { - meals.Add(new Meal - { - Id = reader.GetInt32("Id"), - Name = reader.GetString("Name"), - ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"], - ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")), - ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")) - }); - } + Id = reader.GetInt32("Id"), + Name = reader.GetString("Name"), + ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"], + ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString(reader.GetOrdinal("ImageMimeType")), + ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString(reader.GetOrdinal("ImageUrl")), + Category = reader.IsDBNull(reader.GetOrdinal("Category")) ? null : reader.GetString(reader.GetOrdinal("Category")), + IsAvailable = reader.IsDBNull(reader.GetOrdinal("IsAvailable")) ? true : reader.GetBoolean(reader.GetOrdinal("IsAvailable")) + }); } } } return meals; } + public List GetMealsDetailed() { var meals = new List(); @@ -304,9 +308,9 @@ namespace Aberwyn.Data { cmd.CommandText = @" INSERT INTO Meals - (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions) + (Name, Description, ProteinType, CarbType, RecipeUrl, CreatedAt, ImageUrl, ImageData, ImageMimeType, Instructions, Category, IsAvailable) VALUES - (@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType, @Instructions); + (@Name, @Description, @ProteinType, @CarbType, @RecipeUrl, @CreatedAt, @ImageUrl, @ImageData, @ImageMimeType, @Instructions, @Category, @IsAvailable); SELECT LAST_INSERT_ID();"; } else @@ -321,7 +325,9 @@ namespace Aberwyn.Data ImageUrl = @ImageUrl, ImageData = @ImageData, ImageMimeType = @ImageMimeType, - Instructions = @Instructions + Instructions = @Instructions, + Category = @Category, + IsAvailable = @IsAvailable WHERE Id = @Id"; cmd.Parameters.AddWithValue("@Id", meal.Id); } @@ -336,6 +342,8 @@ namespace Aberwyn.Data cmd.Parameters.AddWithValue("@ImageData", (object?)meal.ImageData ?? DBNull.Value); cmd.Parameters.AddWithValue("@ImageMimeType", meal.ImageMimeType ?? (object)DBNull.Value); cmd.Parameters.AddWithValue("@Instructions", meal.Instructions ?? ""); + cmd.Parameters.AddWithValue("@Category", meal.Category ?? ""); + cmd.Parameters.AddWithValue("@IsAvailable", meal.IsAvailable ? 1 : 0); if (meal.Id == 0) { @@ -348,6 +356,7 @@ namespace Aberwyn.Data } } + public void UpdateWeeklyMenu(MenuViewModel menuData) { if (menuData == null || menuData.WeeklyMenus == null) @@ -479,12 +488,12 @@ namespace Aberwyn.Data connection.Open(); string query = @" - SELECT m.Id, m.Name, m.Category, m.Description, m.ImageUrl, m.ImageData, m.ImageMimeType, - i.Id AS IngredientId, i.Quantity, i.Item - FROM Meals m - LEFT JOIN Ingredients i ON m.Id = i.MealId - WHERE m.Category = @category - ORDER BY m.Name, i.Id"; + SELECT m.Id, m.Name, m.Category, m.Description, m.ImageUrl, m.ImageData, m.ImageMimeType, m.IsAvailable, + i.Id AS IngredientId, i.Quantity, i.Item +FROM Meals m +LEFT JOIN Ingredients i ON m.Id = i.MealId +WHERE m.Category = @category +ORDER BY m.Name, i.Id"; using var cmd = new MySqlCommand(query, connection); cmd.Parameters.AddWithValue("@category", category); @@ -507,7 +516,9 @@ namespace Aberwyn.Data ImageUrl = reader.IsDBNull(reader.GetOrdinal("ImageUrl")) ? null : reader.GetString("ImageUrl"), ImageData = reader.IsDBNull(reader.GetOrdinal("ImageData")) ? null : (byte[])reader["ImageData"], ImageMimeType = reader.IsDBNull(reader.GetOrdinal("ImageMimeType")) ? null : reader.GetString("ImageMimeType"), - Ingredients = new List() + Ingredients = new List(), + IsAvailable = reader.IsDBNull(reader.GetOrdinal("IsAvailable")) ? true : reader.GetBoolean(reader.GetOrdinal("IsAvailable")), + }; } diff --git a/Aberwyn/Migrations/20250524173316_AddAppSettings.Designer.cs b/Aberwyn/Migrations/20250524173316_AddAppSettings.Designer.cs new file mode 100644 index 0000000..99fead8 --- /dev/null +++ b/Aberwyn/Migrations/20250524173316_AddAppSettings.Designer.cs @@ -0,0 +1,451 @@ +ï»ż// +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("20250524173316_AddAppSettings")] + partial class AddAppSettings + { + 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("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BudgetPeriodId") + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetPeriodId"); + + b.ToTable("BudgetCategories"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BudgetCategoryId") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod") + .WithMany("Categories") + .HasForeignKey("BudgetPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetPeriod"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory") + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetCategory"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250524173316_AddAppSettings.cs b/Aberwyn/Migrations/20250524173316_AddAppSettings.cs new file mode 100644 index 0000000..4b323c4 --- /dev/null +++ b/Aberwyn/Migrations/20250524173316_AddAppSettings.cs @@ -0,0 +1,36 @@ +ï»żusing Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddAppSettings : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AppSettings", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Key = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Value = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_AppSettings", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppSettings"); + } + } +} diff --git a/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs new file mode 100644 index 0000000..29763e8 --- /dev/null +++ b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs @@ -0,0 +1,516 @@ +ï»ż// +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("20250526121643_AddBudgetDefinitions")] + partial class AddBudgetDefinitions + { + 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("Id") + .HasColumnType("varchar(255)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("longtext"); + + b.Property("PhoneNumber") + .HasColumnType("longtext"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("tinyint(1)"); + + b.Property("SecurityStamp") + .HasColumnType("longtext"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BudgetCategoryDefinitionId") + .HasColumnType("int"); + + b.Property("BudgetPeriodId") + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryDefinitionId"); + + b.HasIndex("BudgetPeriodId"); + + b.ToTable("BudgetCategories"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetCategoryDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("BudgetCategoryId") + .HasColumnType("int"); + + b.Property("BudgetItemDefinitionId") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.HasIndex("BudgetItemDefinitionId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("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", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("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", "Definition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + b.Navigation("BudgetCategory"); + + b.Navigation("Definition"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Aberwyn.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", 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", 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"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.cs b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.cs new file mode 100644 index 0000000..9934288 --- /dev/null +++ b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.cs @@ -0,0 +1,118 @@ +ï»żusing Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddBudgetDefinitions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BudgetItemDefinitionId", + table: "BudgetItems", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "BudgetCategoryDefinitionId", + table: "BudgetCategories", + type: "int", + nullable: true); + + migrationBuilder.CreateTable( + name: "BudgetCategoryDefinition", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Color = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetCategoryDefinition", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "BudgetItemDefinition", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + DefaultCategory = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + IsExpense = table.Column(type: "tinyint(1)", nullable: false), + IncludeInSummary = table.Column(type: "tinyint(1)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetItemDefinition", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_BudgetItems_BudgetItemDefinitionId", + table: "BudgetItems", + column: "BudgetItemDefinitionId"); + + migrationBuilder.CreateIndex( + name: "IX_BudgetCategories_BudgetCategoryDefinitionId", + table: "BudgetCategories", + column: "BudgetCategoryDefinitionId"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinition_BudgetCategoryDefi~", + table: "BudgetCategories", + column: "BudgetCategoryDefinitionId", + principalTable: "BudgetCategoryDefinition", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetItems_BudgetItemDefinition_BudgetItemDefinitionId", + table: "BudgetItems", + column: "BudgetItemDefinitionId", + principalTable: "BudgetItemDefinition", + principalColumn: "Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinition_BudgetCategoryDefi~", + table: "BudgetCategories"); + + migrationBuilder.DropForeignKey( + name: "FK_BudgetItems_BudgetItemDefinition_BudgetItemDefinitionId", + table: "BudgetItems"); + + migrationBuilder.DropTable( + name: "BudgetCategoryDefinition"); + + migrationBuilder.DropTable( + name: "BudgetItemDefinition"); + + migrationBuilder.DropIndex( + name: "IX_BudgetItems_BudgetItemDefinitionId", + table: "BudgetItems"); + + migrationBuilder.DropIndex( + name: "IX_BudgetCategories_BudgetCategoryDefinitionId", + table: "BudgetCategories"); + + migrationBuilder.DropColumn( + name: "BudgetItemDefinitionId", + table: "BudgetItems"); + + migrationBuilder.DropColumn( + name: "BudgetCategoryDefinitionId", + table: "BudgetCategories"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index d7eb181..007abe5 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -83,12 +83,34 @@ namespace Aberwyn.Migrations b.ToTable("AspNetUsers", (string)null); }); + modelBuilder.Entity("Aberwyn.Models.AppSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("AppSettings"); + }); + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("int"); + b.Property("BudgetCategoryDefinitionId") + .HasColumnType("int"); + b.Property("BudgetPeriodId") .HasColumnType("int"); @@ -105,11 +127,32 @@ namespace Aberwyn.Migrations b.HasKey("Id"); + b.HasIndex("BudgetCategoryDefinitionId"); + b.HasIndex("BudgetPeriodId"); b.ToTable("BudgetCategories"); }); + modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetCategoryDefinition"); + }); + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => { b.Property("Id") @@ -122,6 +165,9 @@ namespace Aberwyn.Migrations b.Property("BudgetCategoryId") .HasColumnType("int"); + b.Property("BudgetItemDefinitionId") + .HasColumnType("int"); + b.Property("IncludeInSummary") .HasColumnType("tinyint(1)"); @@ -139,9 +185,35 @@ namespace Aberwyn.Migrations b.HasIndex("BudgetCategoryId"); + b.HasIndex("BudgetItemDefinitionId"); + b.ToTable("BudgetItems"); }); + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinition"); + }); + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => { b.Property("Id") @@ -344,6 +416,10 @@ namespace Aberwyn.Migrations 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") @@ -351,6 +427,8 @@ namespace Aberwyn.Migrations .IsRequired(); b.Navigation("BudgetPeriod"); + + b.Navigation("Definition"); }); modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => @@ -361,7 +439,13 @@ namespace Aberwyn.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "Definition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + b.Navigation("BudgetCategory"); + + b.Navigation("Definition"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/Aberwyn/Models/AppSetting.cs b/Aberwyn/Models/AppSetting.cs new file mode 100644 index 0000000..239ae72 --- /dev/null +++ b/Aberwyn/Models/AppSetting.cs @@ -0,0 +1,10 @@ +ï»żnamespace Aberwyn.Models +{ + public class AppSetting + { + public int Id { get; set; } + public string Key { get; set; } + public string Value { get; set; } + } + +} diff --git a/Aberwyn/Models/Budget.cs b/Aberwyn/Models/Budget.cs index ee6c29f..420bfe2 100644 --- a/Aberwyn/Models/Budget.cs +++ b/Aberwyn/Models/Budget.cs @@ -20,7 +20,8 @@ namespace Aberwyn.Models public string Color { get; set; } // t.ex. "red", "green", "yellow" public int BudgetPeriodId { get; set; } public int Order { get; set; } - + public int? BudgetCategoryDefinitionId { get; set; } + public BudgetCategoryDefinition? Definition { get; set; } [JsonIgnore] [ValidateNever] public BudgetPeriod BudgetPeriod { get; set; } @@ -36,6 +37,11 @@ namespace Aberwyn.Models public bool IsExpense { get; set; } // true = utgift, false = inkomst/spar public bool IncludeInSummary { get; set; } = true; public int Order { get; set; } // + public int? BudgetItemDefinitionId { get; set; } + + [JsonIgnore] + [ValidateNever] + public BudgetItemDefinition? Definition { get; set; } public int BudgetCategoryId { get; set; } @@ -82,4 +88,20 @@ namespace Aberwyn.Models { public List BudgetItems { get; set; } = new List(); } + + public class BudgetItemDefinition + { + public int Id { get; set; } + public string Name { get; set; } // t.ex. "DĂ€ck", "ElrĂ€kning", "Drivmedel" + public string? DefaultCategory { get; set; } // valfritt, kan föreslĂ„ "Bilar" + public bool IsExpense { get; set; } + public bool IncludeInSummary { get; set; } + } + public class BudgetCategoryDefinition + { + public int Id { get; set; } + public string Name { get; set; } // t.ex. "Bilar", "RĂ€kningar", "Semester" + public string Color { get; set; } // standardfĂ€rg, kan modifieras per period + } + } diff --git a/Aberwyn/Models/MenuViewModel.cs b/Aberwyn/Models/MenuViewModel.cs index c23b5a3..90ea593 100644 --- a/Aberwyn/Models/MenuViewModel.cs +++ b/Aberwyn/Models/MenuViewModel.cs @@ -44,6 +44,7 @@ namespace Aberwyn.Models 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[] ImageData { get; set; } public string ImageMimeType { get; set; } // t.ex. "image/jpeg" @@ -63,7 +64,7 @@ namespace Aberwyn.Models public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } - + public bool IsAvailable { get; set; } public string? ImageUrl { get; set; } public string? ImageData { get; set; } // base64 public string? ImageMimeType { get; set; } @@ -75,6 +76,7 @@ namespace Aberwyn.Models Id = meal.Id, Name = meal.Name, Category = meal.Category, + IsAvailable = meal.IsAvailable, ImageUrl = meal.ImageUrl, ImageMimeType = meal.ImageMimeType, ImageData = meal.ImageData != null ? Convert.ToBase64String(meal.ImageData) : null diff --git a/Aberwyn/Models/PizzaOrder.cs b/Aberwyn/Models/PizzaOrder.cs index 1eb2c90..51b6c9e 100644 --- a/Aberwyn/Models/PizzaOrder.cs +++ b/Aberwyn/Models/PizzaOrder.cs @@ -18,5 +18,13 @@ namespace Aberwyn.Models public DateTime OrderedAt { get; set; } = DateTime.Now; } + public class PizzaAdminViewModel + { + public List ActiveOrders { get; set; } + public List CompletedOrders { get; set; } + public List AvailablePizzas { get; set; } + public bool RestaurantIsOpen { get; set; } + } + } diff --git a/Aberwyn/Views/Budget/Index.cshtml b/Aberwyn/Views/Budget/Index.cshtml index 3a9926f..204540c 100644 --- a/Aberwyn/Views/Budget/Index.cshtml +++ b/Aberwyn/Views/Budget/Index.cshtml @@ -66,23 +66,23 @@ - - - - +
-
-
+
+
+
- + {{ item.name }} @@ -90,9 +90,31 @@ {{ item.amount | number:0 }} - + +
+ +
+ +
+ + + +
+
@@ -106,16 +128,14 @@ - -
- -
-
Summa
-
{{ getCategorySum(cat) | number:0 }}
- +
+
Summa
+
{{ getCategorySum(cat) | number:0 }}
+
+
Det finns ingen budgetdata för vald mĂ„nad ({{ selectedMonth }}/{{ selectedYear }}). @@ -125,6 +145,6 @@ - + \ No newline at end of file diff --git a/Aberwyn/Views/FoodMenu/PizzaAdmin.cshtml b/Aberwyn/Views/FoodMenu/PizzaAdmin.cshtml index 859bce9..739fc5e 100644 --- a/Aberwyn/Views/FoodMenu/PizzaAdmin.cshtml +++ b/Aberwyn/Views/FoodMenu/PizzaAdmin.cshtml @@ -1,147 +1,72 @@ ï»ż@using Newtonsoft.Json -@using Newtonsoft.Json.Serialization +@model PizzaAdminViewModel @{ ViewData["Title"] = "Pizza Admin"; - var activeOrders = ViewBag.ActiveOrders as List ?? new List(); - var completedOrders = ViewBag.CompletedOrders as List ?? new List(); + var availablePizzas = Model.AvailablePizzas as List ?? new List(); + var restaurantOpen = ViewBag.RestaurantOpen as bool? ?? true; } - - -

Aktiva bestÀllningar

-
-@foreach (var order in activeOrders) -{ - var ingredients = new List(); - try - { - if (!string.IsNullOrEmpty(order.IngredientsJson)) - { - ingredients = System.Text.Json.JsonSerializer.Deserialize>(order.IngredientsJson); - } - } - catch - { - ingredients = order.IngredientsJson.Split('\n').ToList(); - } - -var cardClass = order.Status switch -{ - "Confirmed" => "border-warning", - "Ready" => "border-success", - _ => "border-info" -}; - - -
-
@order.CustomerName
-
-
@order.PizzaName
-
    - @foreach (var ing in ingredients) +

    TillgÀngliga pizzor

    +
    +
      + @foreach (var pizza in Model.AvailablePizzas) { -
    • @ing
    • +
    • + @pizza.Name + +
    • }
    - - - -
    - @if (order.Status == "Unconfirmed") - { - - - } - else if (order.Status == "Confirmed") - { - - - } - - -
    -
    -
+ +
-}
-

FĂ€rdiga pizzor

-@if (completedOrders.Any()) -{ -
    - @foreach (var order in completedOrders) - { -
  • - @order.CustomerName – @order.PizzaName -
  • - } -
-} -else -{ -
Inga pizzor Àr klara Ànnu.
+@section Scripts { + } diff --git a/Aberwyn/Views/FoodMenu/PizzaOrder.cshtml b/Aberwyn/Views/FoodMenu/PizzaOrder.cshtml index 3733546..201a119 100644 --- a/Aberwyn/Views/FoodMenu/PizzaOrder.cshtml +++ b/Aberwyn/Views/FoodMenu/PizzaOrder.cshtml @@ -15,6 +15,14 @@ var showForm = previousOrder == null || (forceShow && !justOrdered);
+ @if (!(ViewBag.RestaurantIsOpen as bool? ?? true)) + { +
+ 🛑 Restaurangen Ă€r stĂ€ngd just nu.
+ Kom tillbaka senare för att lÀgga din bestÀllning. +
+ return; + } @if (TempData["Success"] != null) { diff --git a/Aberwyn/Views/FoodMenu/_PizzaAdminPartial.cshtml b/Aberwyn/Views/FoodMenu/_PizzaAdminPartial.cshtml new file mode 100644 index 0000000..cd33378 --- /dev/null +++ b/Aberwyn/Views/FoodMenu/_PizzaAdminPartial.cshtml @@ -0,0 +1,79 @@ +ï»ż@using Aberwyn.Models +@using Newtonsoft.Json +@model PizzaAdminViewModel + +
+
+ @foreach (var order in Model.ActiveOrders) + { + var ingredients = new List(); + try + { + if (!string.IsNullOrEmpty(order.IngredientsJson)) + { + ingredients = System.Text.Json.JsonSerializer.Deserialize>(order.IngredientsJson); + } + } + catch + { + ingredients = order.IngredientsJson?.Split('\n').ToList() ?? new List(); + } + + var cardClass = order.Status switch + { + "Confirmed" => "border-warning", + "Finished" => "border-success", + _ => "border-info" + }; + +
+
@order.CustomerName – @order.PizzaName
+
+
    + @foreach (var ing in ingredients) + { +
  • @ing
  • + } +
+
+ + +
+ @if (order.Status == "Unconfirmed") + { + + + } + else if (order.Status == "Confirmed") + { + + + } + +
+
+
+
+ } +
+ +

Klara bestÀllningar

+
+@foreach (var order in Model.CompletedOrders) +{ + var tooltip = string.Join(", ", JsonConvert.DeserializeObject>(order.IngredientsJson ?? "[]")); +
+
+ + + + +
+ @order.CustomerName + @order.PizzaName +
+} +
+ + +
diff --git a/Aberwyn/Views/Home/Menu.cshtml b/Aberwyn/Views/Home/Menu.cshtml index 73df218..5e24439 100644 --- a/Aberwyn/Views/Home/Menu.cshtml +++ b/Aberwyn/Views/Home/Menu.cshtml @@ -10,7 +10,7 @@ - +
diff --git a/Aberwyn/wwwroot/css/budget.css b/Aberwyn/wwwroot/css/budget.css index 526ea4b..e447c6c 100644 --- a/Aberwyn/wwwroot/css/budget.css +++ b/Aberwyn/wwwroot/css/budget.css @@ -125,28 +125,52 @@ body { .budget-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 12px; - margin-bottom: 24px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap:10px; + align-items: stretch; } + .budget-card { display: flex; flex-direction: column; + width: 100%; height: 350px; - position: relative; - overflow-x: hidden; - overflow-y: visible; + border: 1px solid var(--border-color); + border-radius: 12px; + background-color: var(--bg-card); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + overflow: hidden; } .item-list { - flex: 1 1 auto; + flex-grow: 1; overflow-y: auto; - padding: 8px 12px; - background: rgba(255, 0, 0, 0.05); /* tillfĂ€llig bakgrund för felsökning */ + padding: 2px 8px; + display: flex; + flex-direction: column; } +.item-row:last-child { + margin-bottom: 5px; + +} + + + +.card-header { + border-radius: 12px 12px 0 0; +} +.total-row { + padding: 2px 8px; + font-weight: bold; + font-size: 13px; + background: #f1f5f9; + border-top: 1px solid var(--border-color); + border-radius: 0 0 12px 12px; + flex-shrink: 0; +} .card-header { position: sticky; @@ -188,22 +212,51 @@ body { flex-wrap: nowrap; justify-content: space-between; align-items: center; - gap: 8px; + gap: 0px; font-size: 13px; } +.item-list::-webkit-scrollbar { + width: 6px; +} - .item-row input[type="text"], - .item-row input[type="number"] { - flex: 1; - padding: 5px 6px; - font-size: 13px; - border: 1px solid var(--border-color); - border-radius: 6px; - background-color: #ffffff; - color: var(--text-main); - font-weight: 500; - min-width: 120px; - } +.item-list::-webkit-scrollbar-thumb { + background-color: rgba(100, 116, 139, 0.2); + border-radius: 3px; +} + +.item-list::-webkit-scrollbar-track { + background: transparent; +} + +/* === För Firefox === */ +.item-list { + scrollbar-width: thin; + scrollbar-color: rgba(100, 116, 139, 0.2) transparent; +} + + +.item-row input[type="text"] { + flex: 1; + padding: 5px 6px; + font-size: 13px; + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: #ffffff; + color: var(--text-main); + font-weight: 500; + min-width: 70px; +} +.item-row input[type="number"] { + flex: 1; + padding: 5px 6px; + font-size: 13px; + border: 1px solid var(--border-color); + border-radius: 6px; + background-color: #ffffff; + color: var(--text-main); + font-weight: 500; + max-width: 60px; +} .amount { min-width: 70px; @@ -211,17 +264,6 @@ body { font-weight: 600; } -.total-row { - position: sticky; - bottom: 0; - padding: 8px 12px; - font-weight: bold; - font-size: 13px; - background: var(--bg-card); - border-top: 1px solid var(--border-color); - z-index: 1; -} - .icon-button { @@ -232,26 +274,21 @@ body { padding: 4px; transition: color 0.2s; } - - .icon-button:hover { - color: #000000; - } - - .icon-button.danger { +.icon-button:hover { +color: #000000; +} +.icon-button.danger { color: var(--btn-delete); } - - .icon-button.danger:hover { - color: #b91c1c; - } - - .icon-button.edit { - color: var(--btn-edit); - } - - .icon-button.confirm { - color: var(--btn-check); - } +.icon-button.danger:hover { + color: #b91c1c; +} +.icon-button.edit { +color: var(--btn-edit); +} +.icon-button.confirm { +color: var(--btn-check); +} .dropdown-menu { position: absolute; @@ -284,7 +321,7 @@ body { font-weight: 500; } - .dropdown-menu button:hover { +.dropdown-menu button:hover { background-color: #e5e7eb; } @@ -315,38 +352,75 @@ body { z-index: 9999; font-weight: 500; } - - .toast.show { - opacity: 1; - transform: translateY(0); - } - - .toast.error { - background: #ef4444; - } +.toast.show { + opacity: 1; + transform: translateY(0); +} +.toast.error { + background: #ef4444; +} /* === Dropzones === */ .drop-placeholder { - height: 12px; - transition: background-color 0.2s ease; - background-color: transparent; + height: 0px; + transition: all 0.2s ease; + margin: 0; } .drop-placeholder.drag-over { - border-top: 3px solid #3B82F6; /* blĂ„ rand */ - background-color: rgba(59, 130, 246, 0.15); /* ljusblĂ„ fyllning */ - height: 12px; + border-top: 3px solid #3B82F6; + height: 40px; /* gör det mer tydligt nĂ€r man drar */ + transition: height 0.2s ease; } + + /* Gör att man ser var man kan slĂ€ppa */ .drop-zone.active-drop-target { - background-color: rgba(34, 197, 94, 0.15); /* svagt grön basfĂ€rg */ + height: 12px; + background-color: rgba(22, 163, 74, 0.1); /* svagt grön basfĂ€rg */ } .drop-zone.drag-over { border: 2px dashed #16a34a; /* grön */ background-color: rgba(22, 163, 74, 0.1); } + + +.item-floating-menu { + position: fixed !important; /* viktig för att den inte ska följa DOM-flödet */ + z-index: 9999 !important; /* se till att inget annat ligger över */ + background: #1F2C3C; + color: #fff; + border: 1px solid #444; + border-radius: 6px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + padding: 4px 0px 4px 0; + min-width: 20px; + max-width: 130px; + display: block; +} + + +.item-floating-menu button { + background: none; + border: none; + color: #fff; + text-align: left; + padding: 8px 16px; + width: 100%; + cursor: pointer; +} + +.item-floating-menu button:hover { + background: #333; +} + +.item-floating-menu button.disabled { + color: #aaa; + pointer-events: none; + font-style: italic; +} diff --git a/Aberwyn/wwwroot/css/pizza-admin.css b/Aberwyn/wwwroot/css/pizza-admin.css new file mode 100644 index 0000000..ba97f5f --- /dev/null +++ b/Aberwyn/wwwroot/css/pizza-admin.css @@ -0,0 +1,181 @@ +ï»ż.admin-grid { + display: flex; + gap: 24px; + align-items: flex-start; + flex-wrap: nowrap; +} + +.main-orders { + flex: 3; + display: flex; + flex-direction: column; + gap: 16px; +} + +.available-pizzas { + flex: 1; + background-color: #f1f5f9; + padding: 16px; + border-radius: 12px; + box-shadow: 0 1px 4px rgba(0,0,0,0.1); + min-width: 280px; +} + +.card { + border-radius: 10px; + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1); + background: #f9fafb; /* Ljus bakgrund generellt */ + overflow: hidden; + min-width: 220px; + max-width: 260px; + display: flex; + flex-direction: column; + border: 1px solid #dee2e6; + transition: box-shadow 0.2s ease; +} + + .card.border-info { + border-left: 6px solid #0dcaf0; + background-color: #e6f9fd; /* ljusblĂ„ ton */ + } + + .card.border-warning { + border-left: 6px solid #ffc107; + background-color: #fff9e6; /* ljusgul ton */ + } + + .card.border-success { + border-left: 6px solid #28a745; + background-color: #e6f9ec; /* ljusgrön ton */ + } + + .card.border-inprogress { + border-left: 6px solid #fd7e14; + background-color: #fff3e6; /* ljusorange ton */ + } + +.card-header { + font-weight: 700; + font-size: 0.95rem; + background-color: rgba(0, 0, 0, 0.05); /* fallback */ + padding: 10px 12px; + color: #1a202c; + display: flex; + flex-direction: column; + gap: 4px; +} + +.border-info .card-header { + background-color: #0dcaf0; + color: #fff; +} + +.border-warning .card-header { + background-color: #ffc107; + color: #333; +} + +.border-success .card-header { + background-color: #28a745; + color: #fff; +} + +.border-inprogress .card-header { + background-color: #fd7e14; + color: #fff; +} + +.card-header .pizza-name { + font-size: 0.85rem; + font-weight: 500; + opacity: 0.9; +} + +.card-body { + padding: 10px 12px; + font-size: 0.85rem; + display: flex; + flex-direction: column; + gap: 10px; +} + + .card-body ul { + margin: 0; + padding-left: 1.2rem; + list-style: disc; + } + +.card .btn { + font-size: 0.8rem; + padding: 4px 8px; +} + + +/* Klara bestĂ€llningar – smalare liststil */ +#ordersContainer ul.list-group { + margin-top: 12px; +} + +#ordersContainer ul.list-group li { + background: #ffffff; + border: 1px solid #ddd; + border-radius: 6px; + margin-bottom: 6px; + padding: 8px 12px; + font-size: 0.9rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +#ordersContainer ul.list-group li form { + margin: 0; +} + +/* Responsiv */ +@media only screen and (max-width: 900px) { + .admin-grid { + flex-direction: column; + } + + .available-pizzas { + width: 100%; + } +} +.completed-orders-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 16px; +} + +.completed-order-box { + background-color: #e2e8f0; + border-radius: 8px; + padding: 8px 10px; + font-size: 0.85rem; + min-width: 160px; + max-width: 180px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + display: flex; + flex-direction: column; + position: relative; +} + + .completed-order-box strong { + font-weight: 600; + display: block; + margin-bottom: 4px; + } + +.restore-form { + position: absolute; + top: 6px; + right: 6px; +} + + .restore-form button { + padding: 2px 6px; + font-size: 0.75rem; + line-height: 1; + } diff --git a/Aberwyn/wwwroot/images/21f12523-36e9-451f-b120-d9cac21061fe.png b/Aberwyn/wwwroot/images/21f12523-36e9-451f-b120-d9cac21061fe.png deleted file mode 100644 index 116c3be..0000000 Binary files a/Aberwyn/wwwroot/images/21f12523-36e9-451f-b120-d9cac21061fe.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/image_2919e406-aa1c-4ff1-aa3b-562c41538241.png b/Aberwyn/wwwroot/images/meals/image_2919e406-aa1c-4ff1-aa3b-562c41538241.png deleted file mode 100644 index df12176..0000000 Binary files a/Aberwyn/wwwroot/images/meals/image_2919e406-aa1c-4ff1-aa3b-562c41538241.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/image_8ed11611-689b-432d-b5ab-a45d08b4f08d.png b/Aberwyn/wwwroot/images/meals/image_8ed11611-689b-432d-b5ab-a45d08b4f08d.png deleted file mode 100644 index 16e8131..0000000 Binary files a/Aberwyn/wwwroot/images/meals/image_8ed11611-689b-432d-b5ab-a45d08b4f08d.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/image_b2c768d0-78a6-41bf-a729-f53320ff9340.png b/Aberwyn/wwwroot/images/meals/image_b2c768d0-78a6-41bf-a729-f53320ff9340.png deleted file mode 100644 index 16e8131..0000000 Binary files a/Aberwyn/wwwroot/images/meals/image_b2c768d0-78a6-41bf-a729-f53320ff9340.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/mcdonalds_75ac2dd7-1115-43c3-b5fd-2829bf2395de.png b/Aberwyn/wwwroot/images/meals/mcdonalds_75ac2dd7-1115-43c3-b5fd-2829bf2395de.png deleted file mode 100644 index 116c3be..0000000 Binary files a/Aberwyn/wwwroot/images/meals/mcdonalds_75ac2dd7-1115-43c3-b5fd-2829bf2395de.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/test_93c95185-81d2-4b9b-9286-39c7dcd0ddb1.png b/Aberwyn/wwwroot/images/meals/test_93c95185-81d2-4b9b-9286-39c7dcd0ddb1.png deleted file mode 100644 index e39e4c9..0000000 Binary files a/Aberwyn/wwwroot/images/meals/test_93c95185-81d2-4b9b-9286-39c7dcd0ddb1.png and /dev/null differ diff --git a/Aberwyn/wwwroot/images/meals/test_bbecaddf-0f4b-46c5-8666-62582e61de61.png b/Aberwyn/wwwroot/images/meals/test_bbecaddf-0f4b-46c5-8666-62582e61de61.png deleted file mode 100644 index e39e4c9..0000000 Binary files a/Aberwyn/wwwroot/images/meals/test_bbecaddf-0f4b-46c5-8666-62582e61de61.png and /dev/null differ diff --git a/Aberwyn/wwwroot/js/budget-dragdrop.js b/Aberwyn/wwwroot/js/budget-dragdrop.js index 31823f8..c7f438a 100644 --- a/Aberwyn/wwwroot/js/budget-dragdrop.js +++ b/Aberwyn/wwwroot/js/budget-dragdrop.js @@ -9,51 +9,53 @@ app.directive('draggableItem', function () { onItemDrop: '&' }, link: function (scope, element) { + const row = element[0]; + const handle = row.querySelector('.drag-handle'); + function isEditing() { return scope.category && scope.category.editing; } - function updateDraggableAttr() { - element[0].setAttribute('draggable', isEditing() ? 'true' : 'false'); - element[0].classList.toggle('draggable-enabled', isEditing()); + // Gör handtaget till draggable om editing Ă€r aktivt + scope.$watch('category.editing', function (editing) { + if (handle) handle.setAttribute('draggable', editing ? 'true' : 'false'); + }); + + if (handle) { + handle.addEventListener('dragstart', function (e) { + if (!isEditing()) { + e.preventDefault(); + return; + } + + e.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'item', + itemId: scope.item.id, + fromCategoryId: scope.category.id + })); + + const ghost = row.cloneNode(true); + ghost.style.position = 'absolute'; + ghost.style.top = '-9999px'; + ghost.style.left = '-9999px'; + ghost.style.width = `${row.offsetWidth}px`; + ghost.style.opacity = '0.7'; + document.body.appendChild(ghost); + e.dataTransfer.setDragImage(ghost, 0, 0); + setTimeout(() => document.body.removeChild(ghost), 0); + + document.querySelectorAll('.drop-zone').forEach(el => el.classList.add('active-drop-target')); + }); + + handle.addEventListener('dragend', function () { + document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('active-drop-target')); + }); } - - updateDraggableAttr(); - scope.$watch('category.editing', updateDraggableAttr); - - element[0].addEventListener('dragstart', function (e) { - if (!isEditing()) { - e.preventDefault(); - return; - } - - e.dataTransfer.setData('text/plain', JSON.stringify({ - type: 'item', - fromCategoryId: scope.category.id, - itemId: scope.item.id - })); - - const ghost = element[0].cloneNode(true); - ghost.style.position = 'absolute'; - ghost.style.top = '-9999px'; - ghost.style.left = '-9999px'; - ghost.style.width = element[0].offsetWidth + 'px'; - ghost.style.opacity = '0.7'; - document.body.appendChild(ghost); - e.dataTransfer.setDragImage(ghost, 0, 0); - - setTimeout(() => document.body.removeChild(ghost), 0); - - document.querySelectorAll('.drop-zone').forEach(el => el.classList.add('active-drop-target')); - }); - - element[0].addEventListener('dragend', function () { - document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('active-drop-target')); - }); } }; }); + app.directive('dropPlaceholder', function () { return { restrict: 'A', @@ -85,36 +87,30 @@ app.directive('dropPlaceholder', function () { e.preventDefault(); element[0].classList.add('drag-over'); - label.style.display = 'block'; }); element[0].addEventListener('dragleave', function () { element[0].classList.remove('drag-over'); - label.style.display = 'none'; }); element[0].addEventListener('drop', function (e) { if (!isEditing()) return; e.preventDefault(); element[0].classList.remove('drag-over'); - label.style.display = 'none'; const dataText = e.dataTransfer.getData('text/plain'); - try { - const data = JSON.parse(dataText); - if (data.type !== 'item') return; + const data = JSON.parse(dataText); + if (data.type !== 'item') return; - scope.$apply(() => { - scope.onDropItem({ - data: data, - targetCategory: scope.category, - targetIndex: scope.index - }); + scope.$apply(() => { + scope.onDropItem({ + data: data, + targetCategory: scope.category, + targetIndex: scope.index }); - } catch (err) { - console.error("Error parsing drop data:", err); - } + }); }); + } }; }); @@ -181,22 +177,49 @@ app.directive('draggableCategory', function () { const header = element[0].querySelector('.card-header'); if (!header) return; - function updateDraggableAttr() { - header.setAttribute('draggable', scope.category.allowDrag ? 'true' : 'false'); + function isDragEnabled() { + return scope.category && scope.category.editing && scope.category.allowDrag; } - scope.$watch('category.allowDrag', updateDraggableAttr); + function updateDraggableAttr() { + header.setAttribute('draggable', isDragEnabled() ? 'true' : 'false'); + } + + scope.$watchGroup(['category.editing', 'category.allowDrag'], updateDraggableAttr); updateDraggableAttr(); header.addEventListener('dragstart', function (e) { - if (!scope.category.allowDrag) { + if (!isDragEnabled()) { e.preventDefault(); return; } + + // Endast tillĂ„t drag frĂ„n just headern, inte frĂ„n t.ex. knappar + if (e.target !== header) { + e.preventDefault(); + return; + } + e.dataTransfer.setData('text/plain', JSON.stringify({ type: 'category', categoryId: scope.category.id })); + + const ghost = element[0].cloneNode(true); + ghost.style.position = 'absolute'; + ghost.style.top = '-9999px'; + ghost.style.left = '-9999px'; + ghost.style.width = element[0].offsetWidth + 'px'; + ghost.style.opacity = '0.7'; + document.body.appendChild(ghost); + e.dataTransfer.setDragImage(ghost, 0, 0); + setTimeout(() => document.body.removeChild(ghost), 0); + + document.querySelectorAll('.drop-zone').forEach(el => el.classList.add('active-drop-target')); + }); + + header.addEventListener('dragend', function () { + document.querySelectorAll('.drop-zone').forEach(el => el.classList.remove('active-drop-target')); }); element[0].addEventListener('dragover', function (e) { @@ -206,6 +229,7 @@ app.directive('draggableCategory', function () { element[0].addEventListener('drop', function (e) { e.preventDefault(); element[0].classList.remove('drag-over'); + const data = JSON.parse(e.dataTransfer.getData('text/plain')); if (data.type !== 'category') return; diff --git a/Aberwyn/wwwroot/js/budget.js b/Aberwyn/wwwroot/js/budget.js index 577a368..8a19090 100644 --- a/Aberwyn/wwwroot/js/budget.js +++ b/Aberwyn/wwwroot/js/budget.js @@ -24,6 +24,50 @@ app.controller('BudgetController', function ($scope, $http) { e.stopPropagation(); $scope.menuOpen = !$scope.menuOpen; }; + $scope.menuVisible = false; + $scope.menuItem = null; + $scope.menuStyle = {}; + + $scope.openItemMenu = function ($event, item) { + const rect = $event.currentTarget.getBoundingClientRect(); + console.log("Menu position:", rect); + + $scope.menuItem = item; + $scope.menuItem.category = $scope.budget.categories.find(c => c.items.includes(item)); + + $scope.menuStyle = { + top: `${rect.bottom + 4}px`, + left: `${rect.left}px` + }; + $scope.menuVisible = true; + }; + + $scope.setItemType = function (item, type) { + if (type === 'expense') { + item.isExpense = true; + item.includeInSummary = true; + } else if (type === 'income') { + item.isExpense = false; + item.includeInSummary = true; + } else if (type === 'saving') { + item.isExpense = false; + item.includeInSummary = false; + } + $scope.menuVisible = false; + }; + + // Klick utanför stĂ€nger popup + document.addEventListener('click', function (e) { + const menu = document.querySelector('.item-floating-menu'); + if (menu && !$scope.menuVisible) return; + + const isInside = menu?.contains(e.target); + const isButton = e.target.closest('.icon-button'); + + if (!isInside && !isButton) { + $scope.$apply(() => $scope.menuVisible = false); + } + }); document.addEventListener('click', function () { $scope.$apply(() => { @@ -117,8 +161,6 @@ app.controller('BudgetController', function ($scope, $http) { }); } - category.editing = false; - const payload = { id: category.id, name: category.name, @@ -135,8 +177,13 @@ app.controller('BudgetController', function ($scope, $http) { }; $http.put(`/api/budget/category/${category.id}`, payload) - .then(() => $scope.showToast("Kategori sparad!")) - .catch(() => $scope.showToast("Fel vid sparande av kategori", true)); + .then(() => { + $scope.showToast("Kategori sparad!"); + category.editing = false; // ✅ flytta hit + }) + .catch(() => { + $scope.showToast("Fel vid sparande av kategori", true); + }); }; $scope.deleteCategory = function (category) { @@ -390,6 +437,30 @@ app.controller('BudgetController', function ($scope, $http) { $scope.showToast("Fel vid omplacering", true); }); }; + $scope.updateItemType = function (item) { + if (item.type === 'income') { + item.isExpense = false; + item.includeInSummary = true; + } else if (item.type === 'expense') { + item.isExpense = true; + item.includeInSummary = true; + } else if (item.type === 'saving') { + item.isExpense = false; + item.includeInSummary = false; + } + }; + $scope.setItemType = function (item, type) { + if (type === 'income') { + item.isExpense = false; + item.includeInSummary = true; + } else if (type === 'expense') { + item.isExpense = true; + item.includeInSummary = true; + } else if (type === 'saving') { + item.isExpense = false; + item.includeInSummary = false; + } + }; $scope.createNewCategory = function () { const defaultName = "Ny kategori"; const newOrder = $scope.budget.categories.length; // sist i listan @@ -424,6 +495,68 @@ app.controller('BudgetController', function ($scope, $http) { $scope.showToast("Fel vid skapande av kategori", true); }); }; + $scope.addItem = function (category) { + if (!category.newItemName || !category.newItemAmount) return; + + const tempId = `temp-${Date.now()}-${Math.random()}`; // temporĂ€rt ID + + const newItem = { + id: tempId, + name: category.newItemName, + amount: parseFloat(category.newItemAmount), + isExpense: true, + includeInSummary: true, + budgetCategoryId: category.id + }; + + category.items.push(newItem); + category.newItemName = ""; + category.newItemAmount = ""; + }; + $scope.deleteItem = function (category, item) { + console.log("Försöker ta bort:", item); + + if (!item.id || item.id.toString().startsWith("temp-")) { + // Ta bort direkt om det Ă€r ett nytt (osparat) item + category.items = category.items.filter(i => i !== item); + $scope.showToast("Ospard post togs bort"); + return; + } + + if (!confirm("Vill du ta bort denna post?")) return; + + $http.delete(`/api/budget/item/${item.id}`) + .then(() => { + category.items = category.items.filter(i => i.id !== item.id); + $scope.showToast("Post borttagen!"); + }) + .catch(err => { + console.error("Fel vid borttagning av item:", err); + $scope.showToast("Kunde inte ta bort posten", true); + }); + }; + + $scope.handleItemDrop = function (event, data, targetCategory) { + console.log("Item drop received:", data, "to", targetCategory); + // Hitta kĂ€llkategorin + const sourceCat = $scope.budget.categories.find(c => c.id === data.fromCategoryId); + if (!sourceCat) return; + const itemIndex = sourceCat.items.findIndex(i => i.id === data.itemId); + if (itemIndex === -1) return; + const [movedItem] = sourceCat.items.splice(itemIndex, 1); + targetCategory.items.push(movedItem); + $scope.$applyAsync(); + }; + $scope.handleItemPreciseDrop = function (data, targetCategory, targetIndex) { + console.log("Precise drop at index", targetIndex); + const sourceCat = $scope.budget.categories.find(c => c.id === data.fromCategoryId); + if (!sourceCat) return; + const itemIndex = sourceCat.items.findIndex(i => i.id === data.itemId); + if (itemIndex === -1) return; + const [movedItem] = sourceCat.items.splice(itemIndex, 1); + targetCategory.items.splice(targetIndex, 0, movedItem); + $scope.$applyAsync(); + }; $scope.loadBudget(); diff --git a/Aberwyn/wwwroot/js/menu.js b/Aberwyn/wwwroot/js/menu.js index 209b33b..cbd698b 100644 --- a/Aberwyn/wwwroot/js/menu.js +++ b/Aberwyn/wwwroot/js/menu.js @@ -7,7 +7,8 @@ angular.module('mealMenuApp', ['ngSanitize']) .controller('MealMenuController', function ($scope, $http, $sce) { console.log("Controller initierad"); - $scope.viewMode = 'list'; + const savedView = localStorage.getItem('mealViewMode'); + $scope.viewMode = savedView === 'card' || savedView === 'list' ? savedView : 'card'; $scope.tooltip = {}; $scope.meals = []; $scope.menu = {}; @@ -15,10 +16,6 @@ angular.module('mealMenuApp', ['ngSanitize']) $scope.selectedWeek = getWeek(today); $scope.selectedYear = today.getFullYear(); $scope.daysOfWeek = ["MĂ„ndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"]; - const savedViewMode = localStorage.getItem('mealViewMode'); - if (savedViewMode === 'list' || savedViewMode === 'card') { - $scope.viewMode = savedViewMode; - } $scope.loadMeals = function () { console.log("HĂ€mtar mĂ„ltider..."); @@ -131,12 +128,11 @@ angular.module('mealMenuApp', ['ngSanitize']) $scope.viewMode = $scope.viewMode === 'list' ? 'card' : 'list'; localStorage.setItem('mealViewMode', $scope.viewMode); // ← spara lĂ€get - setTimeout(() => { - const btn = document.getElementById('toggle-view'); - if (btn) { - btn.textContent = $scope.getViewIcon(); - } + $timeout(() => { + const viewBtn = document.getElementById('toggle-view'); + if (viewBtn) viewBtn.textContent = $scope.getViewIcon(); }, 0); + }; @@ -174,8 +170,5 @@ document.addEventListener("DOMContentLoaded", function () { } // Initiera ikon för vy - const scope = angular.element(document.body).scope(); - if (viewBtn && scope) { - viewBtn.textContent = scope.viewMode === 'list' ? 'đŸ—’ïž' : '▣'; - } + });