diff --git a/Aberwyn/Controllers/BudgetApiController.cs b/Aberwyn/Controllers/BudgetApiController.cs index 1310b3d..742c6a4 100644 --- a/Aberwyn/Controllers/BudgetApiController.cs +++ b/Aberwyn/Controllers/BudgetApiController.cs @@ -24,6 +24,7 @@ namespace Aberwyn.Controllers { try { + var period = await _context.BudgetPeriods .Include(p => p.Categories) .ThenInclude(c => c.Items) @@ -78,10 +79,13 @@ namespace Aberwyn.Controllers [HttpGet("byname/{name}")] public async Task GetBudgetByName(string name) { - var period = await _context.BudgetPeriods + var period = _context.BudgetPeriods .Include(p => p.Categories) .ThenInclude(c => c.Items) - .FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower()); + .AsEnumerable() // hämta från db och gör resten i minnet + .FirstOrDefault(p => p.Name != null && + p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (period == null) { @@ -363,6 +367,119 @@ namespace Aberwyn.Controllers return Ok(new { id = newItem.Id }); } + [HttpGet("list")] + public async Task GetAllBudgets() + { + var periods = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .OrderByDescending(p => p.Year) + .ThenByDescending(p => p.Month) + .ToListAsync(); + + var result = periods.Select(p => new + { + id = p.Id, + name = p.Name, + year = p.Year, + month = p.Month, + categories = p.Categories + .OrderBy(c => c.Order) + .Select(c => new + { + id = c.Id, + name = c.Name, + color = c.Color, + total = c.Items.Sum(i => i.Amount), + items = c.Items + .OrderBy(i => i.Order) + .Select(i => new + { + id = i.Id, + name = i.Name, + amount = i.Amount, + isExpense = i.IsExpense, + includeInSummary = i.IncludeInSummary + }).ToList() + }).ToList(), + total = p.Categories.Sum(c => c.Items.Sum(i => i.Amount)) + }); + + return Ok(result); + } + + + // DELETE: api/budget/byname/{name} + [HttpDelete("byname/{name}")] + public async Task DeleteByName(string name) + { + var period = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower()); + + if (period == null) + return NotFound(); + + foreach (var category in period.Categories) + { + _context.BudgetItems.RemoveRange(category.Items); + } + + _context.BudgetCategories.RemoveRange(period.Categories); + _context.BudgetPeriods.Remove(period); + + await _context.SaveChangesAsync(); + return NoContent(); + } + + + // POST: api/budget/copy/byname/{targetName}?from={sourceName} + [HttpPost("copy/byname/{targetName}")] + public async Task CopyFromNamedBudget(string targetName, [FromQuery] string from) + { + var targetPeriod = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == targetName.ToLower()); + + if (targetPeriod != null && targetPeriod.Categories.Any()) + return BadRequest("Det finns redan data för denna budget."); + + var sourcePeriod = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == from.ToLower()); + + if (sourcePeriod == null) + return NotFound("Ingen budget hittades att kopiera från."); + + var newPeriod = new BudgetPeriod + { + Name = targetName, + Categories = sourcePeriod.Categories.Select(cat => new BudgetCategory + { + Name = cat.Name, + Color = cat.Color, + Order = cat.Order, + BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId, + Items = cat.Items.Select(item => new BudgetItem + { + Name = item.Name, + Amount = item.Amount, + IsExpense = item.IsExpense, + IncludeInSummary = item.IncludeInSummary, + Order = item.Order, + BudgetItemDefinitionId = item.BudgetItemDefinitionId + }).ToList() + }).ToList() + }; + + _context.BudgetPeriods.Add(newPeriod); + await _context.SaveChangesAsync(); + + return Ok(new { id = newPeriod.Id }); + } [HttpDelete("item/{id}")] diff --git a/Aberwyn/Controllers/BudgetController.cs b/Aberwyn/Controllers/BudgetController.cs index 7e2264e..ca056a3 100644 --- a/Aberwyn/Controllers/BudgetController.cs +++ b/Aberwyn/Controllers/BudgetController.cs @@ -13,6 +13,11 @@ namespace Aberwyn.Controllers ViewBag.Month = month; return View(); } + [Route("budget/list")] + public IActionResult List() + { + return View(); + } [Route("budget/{name}")] public IActionResult Index(string name) diff --git a/Aberwyn/Controllers/FoodMenuController.cs b/Aberwyn/Controllers/FoodMenuController.cs index 0fa5171..5701729 100644 --- a/Aberwyn/Controllers/FoodMenuController.cs +++ b/Aberwyn/Controllers/FoodMenuController.cs @@ -488,6 +488,34 @@ namespace Aberwyn.Controllers return RedirectToAction("PizzaAdmin"); } + [Authorize(Roles = "Chef")] + [HttpGet] + public IActionResult Calculator() + { + var plans = _context.DoughPlans + .OrderByDescending(p => p.Datum) + .ThenByDescending(p => p.Id) + .ToList(); + + ViewBag.Plans = plans; + + return View(new DoughPlan { AntalPizzor = 8, ViktPerPizza = 220 }); + } + + + + [Authorize(Roles = "Chef")] + [HttpPost] + public IActionResult SaveDoughPlan([FromBody] DoughPlan model) + { + if (model == null) return BadRequest(); + + _context.DoughPlans.Add(model); + _context.SaveChanges(); + + return Json(new { success = true, id = model.Id }); + } + } } diff --git a/Aberwyn/Controllers/PizzaController.cs b/Aberwyn/Controllers/PizzaController.cs index b74f569..156d6e7 100644 --- a/Aberwyn/Controllers/PizzaController.cs +++ b/Aberwyn/Controllers/PizzaController.cs @@ -59,5 +59,8 @@ namespace Aberwyn.Controllers TempData["Success"] = "Beställningen har lagts!"; return RedirectToAction("Order"); } + + + } } diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index c708384..2897c87 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -38,9 +38,10 @@ namespace Aberwyn.Data builder.Entity() .OwnsOne(t => t.Metadata); } - + public DbSet DoughPlans { get; set; } + public DbSet BudgetPeriods { get; set; } public DbSet BudgetCategories { get; set; } public DbSet BudgetItems { get; set; } diff --git a/Aberwyn/Data/RssProcessor.cs b/Aberwyn/Data/RssProcessor.cs index ca6ad61..cd5ac14 100644 --- a/Aberwyn/Data/RssProcessor.cs +++ b/Aberwyn/Data/RssProcessor.cs @@ -32,7 +32,7 @@ namespace Aberwyn.Data var oneHourAgo = DateTime.UtcNow.AddHours(-1); if (debug) oneHourAgo = DateTime.UtcNow.AddHours(1); - + var activeFeeds = await _context.RssFeeds .Where(f => f.IsActive && f.LastChecked <= oneHourAgo) .ToListAsync(); diff --git a/Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs b/Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs new file mode 100644 index 0000000..280cffe --- /dev/null +++ b/Aberwyn/Migrations/20250903130637_AddDoughPlans.Designer.cs @@ -0,0 +1,1304 @@ +// +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("20250903130637_AddDoughPlans")] + partial class AddDoughPlans + { + 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("BudgetCategoryDefinitions"); + }); + + 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.Property("PaymentStatus") + .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("DefaultPaymentStatus") + .HasColumnType("int"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.DoughPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AntalPizzor") + .HasColumnType("int"); + + b.Property("Datum") + .HasColumnType("datetime(6)"); + + b.Property("Jast") + .HasColumnType("double"); + + b.Property("Mjol") + .HasColumnType("double"); + + b.Property("Namn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Olja") + .HasColumnType("double"); + + b.Property("Salt") + .HasColumnType("double"); + + b.Property("TotalDeg") + .HasColumnType("double"); + + b.Property("Vatten") + .HasColumnType("double"); + + b.Property("ViktPerPizza") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("DoughPlans"); + }); + + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("Ingredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("LabIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Item") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Quantity") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RecipeLabVersionId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabVersionId"); + + b.ToTable("LabVersionIngredients"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CarbType") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("ImageData") + .HasColumnType("longblob"); + + b.Property("ImageMimeType") + .HasColumnType("longtext"); + + b.Property("ImageUrl") + .HasColumnType("longtext"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("IsAvailable") + .HasColumnType("tinyint(1)"); + + b.Property("IsPublished") + .HasColumnType("tinyint(1)"); + + b.Property("MealCategoryId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ProteinType") + .HasColumnType("longtext"); + + b.Property("RecipeUrl") + .HasColumnType("longtext"); + + b.Property("ThumbnailData") + .HasColumnType("longblob"); + + b.HasKey("Id"); + + b.HasIndex("MealCategoryId"); + + b.ToTable("Meals"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Color") + .HasColumnType("longtext"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("DisplayOrder") + .HasColumnType("int"); + + b.Property("Icon") + .HasColumnType("longtext"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Slug") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("MealCategories"); + + b.HasData( + new + { + Id = 1, + Color = "#f97316", + DisplayOrder = 1, + Icon = "🍕", + IsActive = true, + Name = "Pizza", + Slug = "pizza" + }); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("MealId") + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("MealId"); + + b.ToTable("MealRatings"); + }); + + 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.Property("PizzaOrderId") + .HasColumnType("int"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("PizzaOrderId"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BaseMealId") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Inspiration") + .HasColumnType("longtext"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("Tags") + .HasColumnType("longtext"); + + b.Property("TestedBy") + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("BaseMealId"); + + b.ToTable("RecipeLabEntries"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Instructions") + .HasColumnType("longtext"); + + b.Property("RecipeLabEntryId") + .HasColumnType("int"); + + b.Property("ResultNotes") + .HasColumnType("longtext"); + + b.Property("VersionLabel") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("RecipeLabEntryId"); + + b.ToTable("RecipeLabVersions"); + }); + + modelBuilder.Entity("Aberwyn.Models.StoredPushSubscription", 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.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PushSubscriptions"); + }); + + modelBuilder.Entity("Aberwyn.Models.TodoTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AssignedTo") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IsArchived") + .HasColumnType("tinyint(1)"); + + b.Property("Priority") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("TodoTasks"); + }); + + modelBuilder.Entity("Aberwyn.Models.UserPreferences", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("NotifyBudget") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyMenu") + .HasColumnType("tinyint(1)"); + + b.Property("NotifyPizza") + .HasColumnType("tinyint(1)"); + + b.HasKey("UserId"); + + b.ToTable("UserPreferences"); + }); + + modelBuilder.Entity("Aberwyn.Models.WeeklyMenu", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("BreakfastMealId") + .HasColumnType("int"); + + b.Property("Cook") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DayOfWeek") + .HasColumnType("int"); + + b.Property("DinnerMealId") + .HasColumnType("int"); + + b.Property("LunchMealId") + .HasColumnType("int"); + + b.Property("WeekNumber") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("WeeklyMenu", (string)null); + }); + + modelBuilder.Entity("DownloadRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AutoDownload") + .HasColumnType("tinyint(1)"); + + b.Property("CategoryFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("KeywordFilter") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaxSize") + .HasColumnType("bigint"); + + b.Property("MinSeeders") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("DownloadRules"); + }); + + 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("RssFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("LastChecked") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Url") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("RssFeeds"); + }); + + modelBuilder.Entity("TorrentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("longtext"); + + b.Property("Completed") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DownloadKey") + .HasColumnType("longtext"); + + b.Property("InfoHash") + .HasColumnType("varchar(255)"); + + b.Property("Leechers") + .HasColumnType("int"); + + b.Property("MagnetLink") + .HasColumnType("longtext"); + + b.Property("MovieName") + .HasColumnType("longtext"); + + b.Property("PublishDate") + .HasColumnType("datetime(6)"); + + b.Property("RssSource") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Seeders") + .HasColumnType("int"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("TorrentUrl") + .HasColumnType("longtext"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("InfoHash") + .IsUnique(); + + b.ToTable("TorrentItems"); + }); + + modelBuilder.Entity("UserTorrentSeen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("InfoHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SeenDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("UserTorrentSeen"); + }); + + 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", null) + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + 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.LabIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + modelBuilder.Entity("Aberwyn.Models.LabVersionIngredient", b => + { + b.HasOne("Aberwyn.Models.RecipeLabVersion", "Version") + .WithMany("Ingredients") + .HasForeignKey("RecipeLabVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Version"); + }); + + modelBuilder.Entity("Aberwyn.Models.Meal", b => + { + b.HasOne("Aberwyn.Models.MealCategory", "Category") + .WithMany("Meals") + .HasForeignKey("MealCategoryId"); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("Aberwyn.Models.MealRating", b => + { + b.HasOne("Aberwyn.Models.Meal", "Meal") + .WithMany() + .HasForeignKey("MealId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Meal"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.HasOne("Aberwyn.Models.PizzaOrder", "PizzaOrder") + .WithMany() + .HasForeignKey("PizzaOrderId"); + + b.HasOne("Aberwyn.Models.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PizzaOrder"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.HasOne("Aberwyn.Models.Meal", "BaseMeal") + .WithMany() + .HasForeignKey("BaseMealId"); + + b.Navigation("BaseMeal"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.HasOne("Aberwyn.Models.RecipeLabEntry", "Entry") + .WithMany("Versions") + .HasForeignKey("RecipeLabEntryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Entry"); + }); + + 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", 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("TorrentItem", b => + { + b.OwnsOne("MovieMetadata", "Metadata", b1 => + { + b1.Property("TorrentItemId") + .HasColumnType("int"); + + b1.Property("Actors") + .HasColumnType("longtext"); + + b1.Property("Director") + .HasColumnType("longtext"); + + b1.Property("Genre") + .HasColumnType("longtext"); + + b1.Property("ImdbID") + .HasColumnType("longtext"); + + b1.Property("ImdbRating") + .HasColumnType("longtext"); + + b1.Property("Plot") + .HasColumnType("longtext"); + + b1.Property("Poster") + .HasColumnType("longtext"); + + b1.Property("Providers") + .HasColumnType("longtext"); + + b1.Property("Runtime") + .HasColumnType("longtext"); + + b1.Property("Title") + .IsRequired() + .HasColumnType("longtext"); + + b1.Property("Year") + .HasColumnType("longtext"); + + b1.HasKey("TorrentItemId"); + + b1.ToTable("TorrentItems"); + + b1.WithOwner() + .HasForeignKey("TorrentItemId"); + }); + + b.Navigation("Metadata"); + }); + + 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"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabEntry", b => + { + b.Navigation("Ingredients"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("Aberwyn.Models.RecipeLabVersion", b => + { + b.Navigation("Ingredients"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Aberwyn/Migrations/20250903130637_AddDoughPlans.cs b/Aberwyn/Migrations/20250903130637_AddDoughPlans.cs new file mode 100644 index 0000000..2fb7aa8 --- /dev/null +++ b/Aberwyn/Migrations/20250903130637_AddDoughPlans.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddDoughPlans : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DoughPlans", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + AntalPizzor = table.Column(type: "int", nullable: false), + ViktPerPizza = table.Column(type: "double", nullable: false), + Mjol = table.Column(type: "double", nullable: false), + Vatten = table.Column(type: "double", nullable: false), + Olja = table.Column(type: "double", nullable: false), + Salt = table.Column(type: "double", nullable: false), + Jast = table.Column(type: "double", nullable: false), + TotalDeg = table.Column(type: "double", nullable: false), + Datum = table.Column(type: "datetime(6)", nullable: false), + Namn = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_DoughPlans", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DoughPlans"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index 38e29f3..7df10d8 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -243,6 +243,48 @@ namespace Aberwyn.Migrations b.ToTable("BudgetPeriods"); }); + modelBuilder.Entity("Aberwyn.Models.DoughPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AntalPizzor") + .HasColumnType("int"); + + b.Property("Datum") + .HasColumnType("datetime(6)"); + + b.Property("Jast") + .HasColumnType("double"); + + b.Property("Mjol") + .HasColumnType("double"); + + b.Property("Namn") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Olja") + .HasColumnType("double"); + + b.Property("Salt") + .HasColumnType("double"); + + b.Property("TotalDeg") + .HasColumnType("double"); + + b.Property("Vatten") + .HasColumnType("double"); + + b.Property("ViktPerPizza") + .HasColumnType("double"); + + b.HasKey("Id"); + + b.ToTable("DoughPlans"); + }); + modelBuilder.Entity("Aberwyn.Models.Ingredient", b => { b.Property("Id") diff --git a/Aberwyn/Models/PizzaOrder.cs b/Aberwyn/Models/PizzaOrder.cs index 51b6c9e..69eba81 100644 --- a/Aberwyn/Models/PizzaOrder.cs +++ b/Aberwyn/Models/PizzaOrder.cs @@ -26,5 +26,22 @@ namespace Aberwyn.Models public bool RestaurantIsOpen { get; set; } } + public class DoughPlan + { + public int Id { get; set; } + + public int AntalPizzor { get; set; } + public double ViktPerPizza { get; set; } + + public double Mjol { get; set; } + public double Vatten { get; set; } + public double Olja { get; set; } + public double Salt { get; set; } + public double Jast { get; set; } + public double TotalDeg { get; set; } + + public DateTime Datum { get; set; } + public string Namn { get; set; } + } } diff --git a/Aberwyn/Views/Budget/Index.cshtml b/Aberwyn/Views/Budget/Index.cshtml index 8b20523..0ef1989 100644 --- a/Aberwyn/Views/Budget/Index.cshtml +++ b/Aberwyn/Views/Budget/Index.cshtml @@ -5,84 +5,95 @@ ViewData["Title"] = "Budget"; } -
-
-
- - - {{ selectedMonthName }} {{ selectedYear }} - - +
+
-
-
- - -
-
+
+ + Lista + + + + {{ selectedMonthName }} {{ selectedYear }} + + - +
+ + Lista + + + {{ budget.name }} + +
+ +
-
- -
-

Sammanställning

-
    -
  • 💰 Inkomst: {{ getTotalIncome() | number:0 }} kr
  • -
  • 💸 Utgift: {{ getTotalExpense() | number:0 }} kr
  • -
  • 🏦 Sparande: {{ getTotalSaving() | number:0 }} kr
  • -

    - 📈 Kvar: - - {{ getLeftover() | number:0 }} kr - +

    + +
    +

    Sammanställning

    +
      +
    • 💰 Inkomst: {{ getTotalIncome() | number:0 }} kr
    • +
    • 💸 Utgift: {{ getTotalExpense() | number:0 }} kr
    • +
    • 🏦 Sparande: {{ getTotalSaving() | number:0 }} kr
    • +

      + 📈 Kvar: + + {{ getLeftover() | number:0 }} kr + +

      + +
    +
    +
      +
    • + {{ cat.name }} + {{ getCategorySum(cat) | number:0 }} kr +
    • +
    +
    + + +
    +
    +
    +

    Utgifter

    + +

    + Totalt: {{ getTotalExpense() | number:0 }} kr

    -
-
-
    -
  • - {{ cat.name }} - {{ getCategorySum(cat) | number:0 }} kr -
  • -
-
- - -
-
-
-

Utgifter

- -

- Totalt: {{ getTotalExpense() | number:0 }} kr -

- -

- Största kategori:
- {{ topExpenseCategory.name }} ({{ topExpenseCategory.percent | number:1 }}%) -

-
- -
- -
-
+

+ Största kategori:
+ {{ topExpenseCategory.name }} ({{ topExpenseCategory.percent | number:1 }}%) +

+
+ +
+
+
+
diff --git a/Aberwyn/Views/Budget/List.cshtml b/Aberwyn/Views/Budget/List.cshtml new file mode 100644 index 0000000..02bf617 --- /dev/null +++ b/Aberwyn/Views/Budget/List.cshtml @@ -0,0 +1,120 @@ +@attribute [Authorize(Roles = "Budget")] +@using Microsoft.AspNetCore.Authorization + +@{ + ViewData["Title"] = "Budgetlista"; +} + +
+ + +
+ +
+ +
+
{{ year }}
+ +
+
+ +
+
{{ getMonthName(month) }}
+
+
+
+
+ +
+
+ +
+
+ {{ cat.name }} + {{ getCategorySum(cat) | number:0 }} kr +
+
+
+ +
+
+ +
+ + + + + + diff --git a/Aberwyn/Views/FoodMenu/Calculator.cshtml b/Aberwyn/Views/FoodMenu/Calculator.cshtml new file mode 100644 index 0000000..cb554ca --- /dev/null +++ b/Aberwyn/Views/FoodMenu/Calculator.cshtml @@ -0,0 +1,144 @@ +@model Aberwyn.Models.DoughPlan +@{ + var plans = ViewBag.Plans as List; +} +
+
🍕 Pizzakalkylator
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
Resultat
+
+
+ Totalt deg + g +
+
+ Mjöl + g +
+
+ Vatten + g +
+
+ Olja + g +
+
+ Salt + g +
+
+ Jäst + g +
+
+
+
+ +@if (plans?.Any() == true) +{ +
+
📋 Sparade planer
+
+
+ @foreach (var p in plans) + { +
+ @p.Namn (@p.Datum.ToString("yyyy-MM-dd")) + 🍕 @p.AntalPizzor st × @p.ViktPerPizza:F1 g +
+ Totalt: @p.TotalDeg:F1 g + Mjöl: @p.Mjol:F1 g + Vatten: @p.Vatten:F1 g + Olja: @p.Olja:F1 g + Salt: @p.Salt:F1 g + Jäst: @p.Jast:F1 g +
+ } +
+
+
+} + + + diff --git a/Aberwyn/wwwroot/css/budget-list.css b/Aberwyn/wwwroot/css/budget-list.css new file mode 100644 index 0000000..2628226 --- /dev/null +++ b/Aberwyn/wwwroot/css/budget-list.css @@ -0,0 +1,161 @@ +:root { + --text-main: #1F2937; + --text-sub: #64748B; + --bg-main: #f8f9fa; + --bg-card: #ffffff; + --border-color: #e2e8f0; + --card-income: #4ade80; + --card-savings: #facc15; + --card-expenses: #f87171; + --card-leftover: #fb923c; + --card-bar-min-height: 4px; /* minsta höjd för synlighet */ +} + +/* Hela sidan */ +.budget-page { + padding: 16px; + background-color: var(--bg-main); +} + +/* År-headers */ +.year-header { + margin-top: 20px; + margin-bottom: 12px; + font-size: 16px; + font-weight: 600; + color: var(--text-main); +} + +/* Grid för månader (horisontellt med wrap) */ +.budget-row { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +/* Kort per månad */ +.budget-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + background-color: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 10px; + cursor: pointer; + padding: 6px; + transition: transform 0.15s ease, box-shadow 0.15s ease; + min-width: 110px; +} + + .budget-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 14px rgba(0,0,0,0.1); + } + +/* Månadens namn */ +.month-header { + display: flex; + align-items: center; /* staplar och text vertikalt centrerade */ + gap: 6px; /* mellanrum mellan namn och staplar */ + cursor: pointer; +} + +.month-name { + font-weight: 600; + color: var(--text-main); + min-width: 50px; +} + +.month-bars { + position: absolute; + top: 0; + right: 0; + display: flex; + gap: 2px; + opacity: 0.6; + height: 30px; /* maxhöjd på staplar */ +} + + +.bar { + width: 6px; /* lite bredare */ + border-radius: 3px 3px 0 0; + background-color: gray; + transition: height 0.3s ease; + min-height: 8px; /* alltid synlig höjd */ +} + + + .bar.income { + background-color: var(--card-income); + } + + .bar.savings { + background-color: var(--card-savings); + } + + .bar.expenses { + background-color: var(--card-expenses); + } + + .bar.leftover { + background-color: var(--card-leftover); + } + +/* Detaljer */ +.item-list { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.item-row { + display: flex; + justify-content: space-between; + font-size: 12px; + min-width: 180px; +} + +.item-label { + font-weight: 500; + color: var(--text-main); +} + +.amount { + font-weight: 600; +} + +/* Toggle */ +.details-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + padding: 8px 0; +} + + .details-toggle input[type="checkbox"] { + width: 16px; + height: 16px; + } + +/* Mobilanpassning */ +@media (max-width: 768px) { + .budget-card { + min-width: 50px; + } + + .month-name { + font-size: 12px; + } + + .bar { + width: 6px; + } + + .month-bars { + height: 50px; + } +} diff --git a/Aberwyn/wwwroot/css/budget.css b/Aberwyn/wwwroot/css/budget.css index 691624a..d8cb82a 100644 --- a/Aberwyn/wwwroot/css/budget.css +++ b/Aberwyn/wwwroot/css/budget.css @@ -97,9 +97,10 @@ body { .budget-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap:10px; + grid-template-columns: repeat(auto-fit, minmax(220px, 300px)); + gap: 10px; align-items: stretch; + } diff --git a/Aberwyn/wwwroot/css/pizzacalculator.css b/Aberwyn/wwwroot/css/pizzacalculator.css new file mode 100644 index 0000000..26b5f66 --- /dev/null +++ b/Aberwyn/wwwroot/css/pizzacalculator.css @@ -0,0 +1,25 @@ +.completed-orders-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 12px; +} + +.completed-order-box { + background-color: #fff; + border: 1px solid #ddd; + border-radius: 12px; + padding: 12px; + font-size: 0.9rem; + min-width: 200px; + max-width: 220px; + box-shadow: 0 2px 6px rgba(0,0,0,0.08); + display: flex; + flex-direction: column; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + + .completed-order-box:hover { + transform: translateY(-3px); + box-shadow: 0 4px 12px rgba(0,0,0,0.12); + } diff --git a/Aberwyn/wwwroot/js/budget-list.js b/Aberwyn/wwwroot/js/budget-list.js new file mode 100644 index 0000000..ea77476 --- /dev/null +++ b/Aberwyn/wwwroot/js/budget-list.js @@ -0,0 +1,28 @@ +var app = angular.module("budgetListApp", []); +app.controller("BudgetListController", function ($scope, $http) { + $scope.loading = true; + $scope.error = null; + $scope.budgets = []; + + $scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", + "Juli", "Augusti", "September", "Oktober", "November", "December"]; + + $scope.getBudgetUrl = function (b) { + if (b.name) { + return "/budget/" + encodeURIComponent(b.name); + } + return "/budget/" + b.year + "/" + b.month; + }; + + $http.get("/api/budget/list") + .then(res => { + $scope.budgets = res.data; + }) + .catch(err => { + console.error("Kunde inte hämta budgetlista:", err); + $scope.error = "Fel vid laddning av budgetar."; + }) + .finally(() => { + $scope.loading = false; + }); +}); diff --git a/Aberwyn/wwwroot/js/budget.js b/Aberwyn/wwwroot/js/budget.js index 6caa2e2..1ea5ca3 100644 --- a/Aberwyn/wwwroot/js/budget.js +++ b/Aberwyn/wwwroot/js/budget.js @@ -148,7 +148,7 @@ app.controller('BudgetController', function ($scope, $http) { let url = ""; if (useName) { - url = `/api/budget/byname/${initialName}`; + url = `/api/budget/byname/${encodeURIComponent(initialName)}`; } else { url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`; } @@ -308,6 +308,84 @@ app.controller('BudgetController', function ($scope, $http) { $scope.showToast("Fel vid borttagning av månad", true); }); }; + $scope.deleteBudget = function () { + if (!confirm("Vill du verkligen ta bort hela budgeten?")) return; + + if ($scope.budget.name) { + // Namnbudget + $http.delete(`/api/budget/byname/${encodeURIComponent($scope.budget.name)}`) + .then(() => { + $scope.showToast("Budget borttagen!"); + window.location.href = "/budget"; // gå tillbaka till startsida + }) + .catch(err => { + console.error("Kunde inte ta bort budget:", err); + $scope.showToast("Fel vid borttagning", true); + }); + } else { + // Månad + const year = $scope.selectedYear; + const month = $scope.selectedMonth; + $http.delete(`/api/budget/${year}/${month}`) + .then(() => { + $scope.showToast("Månad borttagen!"); + $scope.loadBudget(); + }) + .catch(err => { + console.error("Kunde inte ta bort månad:", err); + $scope.showToast("Fel vid borttagning", true); + }); + } + }; + $scope.copyBudget = function () { + if ($scope.budget.name) { + const from = prompt("Ange namnet på budgeten du vill kopiera ifrån:"); + if (!from) return; + + $http.post(`/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}?from=${encodeURIComponent(from)}`) + .then(() => { + $scope.showToast("Budget kopierad!"); + $scope.loadBudget(); + }) + .catch(err => { + console.error("Kunde inte kopiera budget:", err); + $scope.showToast("Fel vid kopiering", true); + }); + } else { + if (!confirm("Vill du kopiera föregående månad till den aktuella?")) return; + + const year = $scope.selectedYear; + const month = $scope.selectedMonth; + + $http.post(`/api/budget/copy/${year}/${month}`) + .then(() => { + $scope.showToast("Föregående månad kopierad!"); + $scope.loadBudget(); + }) + .catch(err => { + console.error("Kunde inte kopiera månad:", err); + $scope.showToast("Fel vid kopiering av månad", true); + }); + } + }; + $scope.createNamedBudget = function () { + const name = prompt("Ange namn på din nya budget:"); + if (!name) return; + + $http.post('/api/budget', { name: name }) + .then(res => { + $scope.showToast("Ny budget skapad!"); + window.location.href = `/budget/${encodeURIComponent(name)}`; + }) + .catch(err => { + console.error("Fel vid skapande:", err); + if (err.status === 409) { + $scope.showToast("En budget med detta namn finns redan.", true); + } else { + $scope.showToast("Kunde inte skapa budget.", true); + } + }); + }; $scope.copyPreviousMonth = function () { if (!confirm("Vill du kopiera föregående månad till den aktuella?")) { @@ -772,7 +850,9 @@ app.controller('BudgetController', function ($scope, $http) { }); } }); - + $scope.goToBudgetList = function () { + $window.location.href = '/budget/list'; // den route som visar listan + }; $scope.addItemFromDefinition = function (cat) { const definitionName = cat.newItemDefinition?.trim(); const label = cat.newItemLabel?.trim();