diff --git a/Aberwyn/Aberwyn.csproj b/Aberwyn/Aberwyn.csproj index da92c65..33d4e20 100644 --- a/Aberwyn/Aberwyn.csproj +++ b/Aberwyn/Aberwyn.csproj @@ -16,6 +16,14 @@ + + + + + + + + diff --git a/Aberwyn/Controllers/AdminController.cs b/Aberwyn/Controllers/AdminController.cs index e80f2ce..3412677 100644 --- a/Aberwyn/Controllers/AdminController.cs +++ b/Aberwyn/Controllers/AdminController.cs @@ -36,6 +36,24 @@ namespace Aberwyn.Controllers ViewBag.AllRoles = allRoles; return View(model); } + [HttpPost] + public async Task CreateUser(string email, string password) + { + var user = new ApplicationUser { UserName = email, Email = email }; + var result = await _userManager.CreateAsync(user, password); + + if (result.Succeeded) + { + TempData["Message"] = "Användare skapad!"; + return RedirectToAction("Index"); + } + + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error.Description); + } + return RedirectToAction("Index"); + } [HttpPost] public async Task AddToRole(string userId, string role) diff --git a/Aberwyn/Controllers/BudgetApiController.cs b/Aberwyn/Controllers/BudgetApiController.cs index 2c39080..245ca67 100644 --- a/Aberwyn/Controllers/BudgetApiController.cs +++ b/Aberwyn/Controllers/BudgetApiController.cs @@ -1,97 +1,325 @@ -using Microsoft.AspNetCore.Mvc; -using Aberwyn.Data; +using Aberwyn.Data; using Aberwyn.Models; -using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Aberwyn.Controllers { - [Route("api/[controller]")] + [Authorize(Roles = "Budget")] [ApiController] + [Route("api/budget")] public class BudgetApiController : ControllerBase { - private readonly BudgetService _budgetService; + private readonly ApplicationDbContext _context; - public BudgetApiController(BudgetService budgetService) + public BudgetApiController(ApplicationDbContext context) { - _budgetService = budgetService; + _context = context; } - [HttpGet("items")] - public IActionResult GetBudgetItems([FromQuery] int month, [FromQuery] int year) + [HttpGet("{year:int}/{month:int}")] + public async Task GetBudget(int year, int month) { try { - var items = _budgetService.GetBudgetItems(month, year); - return Ok(items); - } - catch (Exception ex) - { - // Log the exception (consider using a logging framework) - return StatusCode(500, new { message = "An error occurred while fetching budget items.", details = ex.Message }); - } - } + var period = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Year == year && p.Month == month); - [HttpGet("categories")] - public IActionResult GetCategories() - { - try - { - var categories = _budgetService.GetCategories(); - return Ok(categories); - } - catch (Exception ex) - { - // Log the exception - return StatusCode(500, new { message = "An error occurred while fetching categories.", details = ex.Message }); - } - } - - [HttpPut("items")] - public IActionResult UpdateBudgetItem([FromBody] BudgetItem item) - { - if (item == null || item.ID <= 0) - { - return BadRequest("Invalid budget item data."); - } - - try - { - var result = _budgetService.UpdateBudgetItem(item); - if (result) + if (period == null) { - return Ok("Item updated successfully."); + return Ok(new BudgetDto + { + Year = year, + Month = month, + Categories = new List() + }); } - return StatusCode(500, "Error updating item."); - } - catch (Exception ex) - { - // Log the exception - return StatusCode(500, new { message = "An error occurred while updating the item.", details = ex.Message }); - } - } - [HttpPost("items")] - public IActionResult AddBudgetItem([FromBody] BudgetItem item) - { - if (item == null || string.IsNullOrEmpty(item.Name) || item.Amount <= 0) - { - return BadRequest("Invalid budget item data."); - } - try - { - var result = _budgetService.AddBudgetItem(item); - if (result) + var dto = new BudgetDto { - return CreatedAtAction(nameof(GetBudgetItems), new { id = item.ID }, item); - } - return StatusCode(500, "Error adding item."); + Id = period.Id, + Year = period.Year, + Month = period.Month, + Categories = period.Categories + .OrderBy(cat => cat.Order) + .Select(cat => new BudgetCategoryDto + { + Id = cat.Id, + Name = cat.Name, + Color = cat.Color, + Items = cat.Items + .OrderBy(i => i.Order) // ← sortera innan mappning + .Select(i => new BudgetItemDto + { + Id = i.Id, + Name = i.Name, + Amount = i.Amount, + IsExpense = i.IsExpense, + IncludeInSummary = i.IncludeInSummary + }).ToList() + }).ToList() + }; + + return Ok(dto); } catch (Exception ex) { - // Log the exception - return StatusCode(500, new { message = "An error occurred while adding the item.", details = ex.Message }); + return StatusCode(500, $"Fel: {ex.Message} \n{ex.StackTrace}"); } } + + [HttpPut("category/{id}")] + public async Task UpdateCategory(int id, [FromBody] BudgetCategoryDto updatedCategory) + { + var category = await _context.BudgetCategories + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.Id == id); + + if (category == null) + return NotFound(); + + // Uppdatera kategoriinformation + category.Name = updatedCategory.Name; + category.Color = updatedCategory.Color; + + // Spara undan inkommande IDs (ignorera nya med Id = 0) + var incomingItemIds = updatedCategory.Items + .Where(i => i.Id != 0) + .Select(i => i.Id) + .ToList(); + + // Ta bort poster som inte längre finns i inkommande data + var itemsToRemove = category.Items + .Where(i => !incomingItemIds.Contains(i.Id)) + .ToList(); + + _context.BudgetItems.RemoveRange(itemsToRemove); + + // Lägg till nya poster eller uppdatera befintliga + foreach (var incoming in updatedCategory.Items) + { + if (incoming.Id == 0) + { + category.Items.Add(new BudgetItem + { + Name = incoming.Name, + Amount = incoming.Amount, + IsExpense = incoming.IsExpense, + IncludeInSummary = incoming.IncludeInSummary, + Order = incoming.Order, + BudgetCategoryId = category.Id + }); + } + else + { + var existing = category.Items.FirstOrDefault(i => i.Id == incoming.Id); + if (existing != null) + { + existing.Name = incoming.Name; + existing.Amount = incoming.Amount; + existing.IsExpense = incoming.IsExpense; + existing.IncludeInSummary = incoming.IncludeInSummary; + } + } + } + + await _context.SaveChangesAsync(); + return NoContent(); + } + + [HttpPut("category/order")] + public async Task UpdateCategoryOrder([FromBody] List orderedCategories) + { + foreach (var dto in orderedCategories) + { + var cat = await _context.BudgetCategories.FindAsync(dto.Id); + if (cat != null) + cat.Order = dto.Order; + } + + await _context.SaveChangesAsync(); + return NoContent(); + } + + + + [HttpPost] + public async Task CreatePeriod([FromBody] BudgetPeriod newPeriod) + { + _context.BudgetPeriods.Add(newPeriod); + await _context.SaveChangesAsync(); + return CreatedAtAction(nameof(GetBudget), new { year = newPeriod.Year, month = newPeriod.Month }, newPeriod); + } + + [HttpPut("item/{id}")] + public async Task UpdateItem(int id, [FromBody] BudgetItem updatedItem) + { + var item = await _context.BudgetItems.FindAsync(id); + if (item == null) return NotFound(); + + item.Name = updatedItem.Name; + item.Amount = updatedItem.Amount; + item.IsExpense = updatedItem.IsExpense; + item.IncludeInSummary = updatedItem.IncludeInSummary; + item.Order = updatedItem.Order; + item.BudgetCategoryId = updatedItem.BudgetCategoryId; + + await _context.SaveChangesAsync(); + return Ok(); + } + + [HttpPost("item")] + public async Task CreateItem([FromBody] BudgetItem newItem) + { + if (newItem == null || newItem.BudgetCategoryId == 0) + return BadRequest("Ogiltig data."); + + _context.BudgetItems.Add(newItem); + await _context.SaveChangesAsync(); + + return Ok(new { id = newItem.Id }); + } + + [HttpDelete("item/{id}")] + public async Task DeleteItem(int id) + { + var item = await _context.BudgetItems.FindAsync(id); + if (item == null) return NotFound(); + + _context.BudgetItems.Remove(item); + await _context.SaveChangesAsync(); + return NoContent(); + } + [HttpDelete("{year:int}/{month:int}")] + public async Task DeleteMonth(int year, int month) + { + var period = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Year == year && p.Month == month); + + if (period == null) + return NotFound(); + + // Ta bort alla items → kategorier → period + 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(); + } + + + [HttpPost("category")] + public async Task CreateCategory([FromBody] BudgetCategoryDto newCategoryDto) + { + if (newCategoryDto == null || string.IsNullOrWhiteSpace(newCategoryDto.Name)) + return BadRequest("Ogiltig data."); + + // Kontrollera att rätt period finns + var period = await _context.BudgetPeriods + .FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month); + + if (period == null) + { + // Skapa ny period om den inte finns + period = new BudgetPeriod + { + Year = newCategoryDto.Year, + Month = newCategoryDto.Month + }; + _context.BudgetPeriods.Add(period); + await _context.SaveChangesAsync(); // Vi behöver spara för att få period.Id + } + + var category = new BudgetCategory + { + Name = newCategoryDto.Name, + Color = newCategoryDto.Color ?? "#666666", + BudgetPeriodId = period.Id, + Order = newCategoryDto.Order + }; + + _context.BudgetCategories.Add(category); + await _context.SaveChangesAsync(); + + return Ok(new { id = category.Id }); + } + [HttpDelete("category/{id}")] + public async Task DeleteCategory(int id) + { + var category = await _context.BudgetCategories + .Include(c => c.Items) + .FirstOrDefaultAsync(c => c.Id == id); + + if (category == null) + return NotFound(); + + _context.BudgetItems.RemoveRange(category.Items); + _context.BudgetCategories.Remove(category); + + await _context.SaveChangesAsync(); + + return NoContent(); + } + + [HttpPost("copy/{year:int}/{month:int}")] + public async Task CopyFromPreviousMonth(int year, int month) + { + var targetPeriod = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Year == year && p.Month == month); + + if (targetPeriod != null && targetPeriod.Categories.Any()) + return BadRequest("Det finns redan data för denna månad."); + + // Räkna ut föregående månad + var previous = new DateTime(year, month, 1).AddMonths(-1); + var previousPeriod = await _context.BudgetPeriods + .Include(p => p.Categories) + .ThenInclude(c => c.Items) + .FirstOrDefaultAsync(p => p.Year == previous.Year && p.Month == previous.Month); + + if (previousPeriod == null) + return NotFound("Ingen data att kopiera från."); + + // Skapa ny period + var newPeriod = new BudgetPeriod + { + Year = year, + Month = month, + Categories = previousPeriod.Categories.Select(cat => new BudgetCategory + { + Name = cat.Name, + Color = cat.Color, + Order = cat.Order, + Items = cat.Items.Select(item => new BudgetItem + { + Name = item.Name, + Amount = item.Amount, + IsExpense = item.IsExpense, + IncludeInSummary = item.IncludeInSummary, + Order = item.Order + }).ToList() + }).ToList() + }; + + _context.BudgetPeriods.Add(newPeriod); + await _context.SaveChangesAsync(); + + return Ok(); + } + + } } diff --git a/Aberwyn/Controllers/BudgetController.cs b/Aberwyn/Controllers/BudgetController.cs new file mode 100644 index 0000000..e6cbdab --- /dev/null +++ b/Aberwyn/Controllers/BudgetController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Aberwyn.Controllers +{ + public class BudgetController : Controller + { + [Authorize(Roles = "Budget")] + public IActionResult Index() + { + ViewData["HideSidebar"] = true; + return View(); + } + } +} \ No newline at end of file diff --git a/Aberwyn/Controllers/FoodMenuController.cs b/Aberwyn/Controllers/FoodMenuController.cs index ad23c37..1682a63 100644 --- a/Aberwyn/Controllers/FoodMenuController.cs +++ b/Aberwyn/Controllers/FoodMenuController.cs @@ -9,6 +9,7 @@ using System.Globalization; using System.Linq; using System.Collections.Generic; using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Authorization; namespace Aberwyn.Controllers { @@ -22,7 +23,7 @@ namespace Aberwyn.Controllers _configuration = configuration; _env = env; } - + [Authorize(Roles = "Budget")] public IActionResult Veckomeny(int? week, int? year) { var menuService = new MenuService(_configuration, _env); diff --git a/Aberwyn/Controllers/HomeController.cs b/Aberwyn/Controllers/HomeController.cs index dda595f..7395ef3 100644 --- a/Aberwyn/Controllers/HomeController.cs +++ b/Aberwyn/Controllers/HomeController.cs @@ -10,15 +10,14 @@ namespace Aberwyn.Controllers public class HomeController : Controller { private readonly ILogger _logger; - private readonly BudgetService _budgetService; + //private readonly BudgetService _budgetService; private readonly MenuService _menuService; // Constructor to inject dependencies - public HomeController(ILogger logger, BudgetService budgetService, MenuService menuService) + public HomeController(ILogger logger, MenuService menuService) { _logger = logger; - _budgetService = budgetService; _menuService = menuService; } @@ -62,32 +61,12 @@ namespace Aberwyn.Controllers return View(model); } - - - // Optimized Budget Action to fetch filtered data directly from the database - public IActionResult Budget(string month, int? year) + public IActionResult Budget() { - // Default to current month and year if parameters are not provided - int selectedMonth = !string.IsNullOrEmpty(month) - ? Array.IndexOf(CultureInfo.CurrentCulture.DateTimeFormat.MonthNames, month) + 1 - : DateTime.Now.Month; - - int selectedYear = year ?? DateTime.Now.Year; - - // Fetch budget items for the selected month and year directly from the database - var budgetItems = _budgetService.GetBudgetItems(selectedMonth, selectedYear); - - // Create the BudgetModel - var budgetModel = new BudgetModel - { - BudgetItems = budgetItems.ToList() // Ensure this is a list - }; - - // Pass the BudgetModel to the view - return View(budgetModel); + ViewData["HideSidebar"] = true; + return View(); } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { diff --git a/Aberwyn/Controllers/MealController.cs b/Aberwyn/Controllers/MealController.cs index 3d3d35a..d21051c 100644 --- a/Aberwyn/Controllers/MealController.cs +++ b/Aberwyn/Controllers/MealController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Aberwyn.Models; using Aberwyn.Data; +using Microsoft.AspNetCore.Authorization; namespace Aberwyn.Controllers { diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index 1e8eb36..96a2609 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -11,5 +11,8 @@ namespace Aberwyn.Data { } + public DbSet BudgetPeriods { get; set; } + public DbSet BudgetCategories { get; set; } + public DbSet BudgetItems { get; set; } } -} \ No newline at end of file +} diff --git a/Aberwyn/Data/BudgetService.cs b/Aberwyn/Data/BudgetService.cs index e762cfd..17421f5 100644 --- a/Aberwyn/Data/BudgetService.cs +++ b/Aberwyn/Data/BudgetService.cs @@ -117,7 +117,7 @@ namespace Aberwyn.Data return cmd.ExecuteNonQuery() > 0; // Returns true if a row was inserted } } - } + }*/ // New method to fetch all categories public List GetCategories() diff --git a/Aberwyn/Data/TestDataSeeder.cs b/Aberwyn/Data/TestDataSeeder.cs new file mode 100644 index 0000000..9425c67 --- /dev/null +++ b/Aberwyn/Data/TestDataSeeder.cs @@ -0,0 +1,55 @@ +using Aberwyn.Models; +using Microsoft.EntityFrameworkCore; + +namespace Aberwyn.Data +{ + public static class TestDataSeeder + { + public static async Task SeedBudget(ApplicationDbContext context) + { + if (await context.BudgetPeriods.AnyAsync()) return; + + var period = new BudgetPeriod + { + Year = DateTime.Now.Year, + Month = DateTime.Now.Month, + Categories = new List + { + new BudgetCategory + { + Name = "Inkomster", + Color = "#2d6a4f", + Items = new List + { + new BudgetItem { Name = "Elias lön", Amount = 27000, IsExpense = false }, + new BudgetItem { Name = "Elin lön", Amount = 24000, IsExpense = false }, + } + }, + new BudgetCategory + { + Name = "Fasta utgifter", + Color = "#c1121f", + Items = new List + { + new BudgetItem { Name = "Hyra", Amount = 8900, IsExpense = true }, + new BudgetItem { Name = "El", Amount = 1200, IsExpense = true }, + new BudgetItem { Name = "Internet", Amount = 400, IsExpense = true }, + } + }, + new BudgetCategory + { + Name = "Sparande", + Color = "#6a4c93", + Items = new List + { + new BudgetItem { Name = "Buffert", Amount = 3000, IsExpense = false, IncludeInSummary = false }, + } + } + } + }; + + context.BudgetPeriods.Add(period); + await context.SaveChangesAsync(); + } + } +} diff --git a/Aberwyn/Migrations/20250515202922_CreateBudgetSchema.Designer.cs b/Aberwyn/Migrations/20250515202922_CreateBudgetSchema.Designer.cs new file mode 100644 index 0000000..dea295a --- /dev/null +++ b/Aberwyn/Migrations/20250515202922_CreateBudgetSchema.Designer.cs @@ -0,0 +1,375 @@ +// +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("20250515202922_CreateBudgetSchema")] + partial class CreateBudgetSchema + { + 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.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.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("Person") + .IsRequired() + .HasColumnType("longtext"); + + 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("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + 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/20250515202922_CreateBudgetSchema.cs b/Aberwyn/Migrations/20250515202922_CreateBudgetSchema.cs new file mode 100644 index 0000000..961cca1 --- /dev/null +++ b/Aberwyn/Migrations/20250515202922_CreateBudgetSchema.cs @@ -0,0 +1,101 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class CreateBudgetSchema : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "BudgetPeriods", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Year = table.Column(type: "int", nullable: false), + Month = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetPeriods", x => x.Id); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "BudgetCategories", + 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"), + BudgetPeriodId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetCategories", x => x.Id); + table.ForeignKey( + name: "FK_BudgetCategories_BudgetPeriods_BudgetPeriodId", + column: x => x.BudgetPeriodId, + principalTable: "BudgetPeriods", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "BudgetItems", + 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"), + Person = table.Column(type: "longtext", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + Amount = table.Column(type: "decimal(65,30)", nullable: false), + IsExpense = table.Column(type: "tinyint(1)", nullable: false), + IncludeInSummary = table.Column(type: "tinyint(1)", nullable: false), + BudgetCategoryId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BudgetItems", x => x.Id); + table.ForeignKey( + name: "FK_BudgetItems_BudgetCategories_BudgetCategoryId", + column: x => x.BudgetCategoryId, + principalTable: "BudgetCategories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_BudgetCategories_BudgetPeriodId", + table: "BudgetCategories", + column: "BudgetPeriodId"); + + migrationBuilder.CreateIndex( + name: "IX_BudgetItems_BudgetCategoryId", + table: "BudgetItems", + column: "BudgetCategoryId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "BudgetItems"); + + migrationBuilder.DropTable( + name: "BudgetCategories"); + + migrationBuilder.DropTable( + name: "BudgetPeriods"); + } + } +} diff --git a/Aberwyn/Migrations/20250515204407_MakePersonNullable.Designer.cs b/Aberwyn/Migrations/20250515204407_MakePersonNullable.Designer.cs new file mode 100644 index 0000000..388a551 --- /dev/null +++ b/Aberwyn/Migrations/20250515204407_MakePersonNullable.Designer.cs @@ -0,0 +1,374 @@ +// +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("20250515204407_MakePersonNullable")] + partial class MakePersonNullable + { + 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.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.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("Person") + .HasColumnType("longtext"); + + 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("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + 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/20250515204407_MakePersonNullable.cs b/Aberwyn/Migrations/20250515204407_MakePersonNullable.cs new file mode 100644 index 0000000..edab9b1 --- /dev/null +++ b/Aberwyn/Migrations/20250515204407_MakePersonNullable.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class MakePersonNullable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Person", + table: "BudgetItems", + type: "longtext", + nullable: true, + oldClrType: typeof(string), + oldType: "longtext") + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "BudgetItems", + keyColumn: "Person", + keyValue: null, + column: "Person", + value: ""); + + migrationBuilder.AlterColumn( + name: "Person", + table: "BudgetItems", + type: "longtext", + nullable: false, + oldClrType: typeof(string), + oldType: "longtext", + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Aberwyn/Migrations/20250519213736_AddOrderToBudgetCategory.Designer.cs b/Aberwyn/Migrations/20250519213736_AddOrderToBudgetCategory.Designer.cs new file mode 100644 index 0000000..63e2e0d --- /dev/null +++ b/Aberwyn/Migrations/20250519213736_AddOrderToBudgetCategory.Designer.cs @@ -0,0 +1,380 @@ +// +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("20250519213736_AddOrderToBudgetCategory")] + partial class AddOrderToBudgetCategory + { + 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.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("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/20250519213736_AddOrderToBudgetCategory.cs b/Aberwyn/Migrations/20250519213736_AddOrderToBudgetCategory.cs new file mode 100644 index 0000000..481de48 --- /dev/null +++ b/Aberwyn/Migrations/20250519213736_AddOrderToBudgetCategory.cs @@ -0,0 +1,59 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddOrderToBudgetCategory : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Person", + table: "BudgetItems"); + + migrationBuilder.AddColumn( + name: "Order", + table: "BudgetPeriods", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Order", + table: "BudgetItems", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Order", + table: "BudgetCategories", + type: "int", + nullable: false, + defaultValue: 0); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Order", + table: "BudgetPeriods"); + + migrationBuilder.DropColumn( + name: "Order", + table: "BudgetItems"); + + migrationBuilder.DropColumn( + name: "Order", + table: "BudgetCategories"); + + migrationBuilder.AddColumn( + name: "Person", + table: "BudgetItems", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index 7781902..e9f964c 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -83,6 +83,85 @@ namespace Aberwyn.Migrations b.ToTable("AspNetUsers", (string)null); }); + 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("Microsoft.AspNetCore.Identity.IdentityRole", b => { b.Property("Id") @@ -211,6 +290,28 @@ namespace Aberwyn.Migrations 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) @@ -261,6 +362,16 @@ namespace Aberwyn.Migrations .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/Models/Budget.cs b/Aberwyn/Models/Budget.cs new file mode 100644 index 0000000..ee6c29f --- /dev/null +++ b/Aberwyn/Models/Budget.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Aberwyn.Models +{ + public class BudgetPeriod + { + public int Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public int Order { get; set; } + public List Categories { get; set; } = new(); + } + + public class BudgetCategory + { + public int Id { get; set; } + public string Name { get; set; } + public string Color { get; set; } // t.ex. "red", "green", "yellow" + public int BudgetPeriodId { get; set; } + public int Order { get; set; } + + [JsonIgnore] + [ValidateNever] + public BudgetPeriod BudgetPeriod { get; set; } + + public List Items { get; set; } = new(); + } + + public class BudgetItem + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Amount { get; set; } + public bool IsExpense { get; set; } // true = utgift, false = inkomst/spar + public bool IncludeInSummary { get; set; } = true; + public int Order { get; set; } // + + public int BudgetCategoryId { get; set; } + + [JsonIgnore] + [ValidateNever] + public BudgetCategory BudgetCategory { get; set; } + } + // DTOs/BudgetDto.cs + public class BudgetDto + { + public int Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public int Order { get; set; } + + public List Categories { get; set; } = new(); + } + + public class BudgetCategoryDto + { + public int Id { get; set; } + public string Name { get; set; } + public string Color { get; set; } + public List Items { get; set; } = new(); + public int Order { get; set; } + + public int Year { get; set; } + public int Month { get; set; } + } + + public class BudgetItemDto + { + public int Id { get; set; } + public string Name { get; set; } + public decimal Amount { get; set; } + public int Order { get; set; } + + public bool IsExpense { get; set; } + public bool IncludeInSummary { get; set; } + } + + + public class BudgetModel + { + public List BudgetItems { get; set; } = new List(); + } +} diff --git a/Aberwyn/Models/BudgetItem.cs b/Aberwyn/Models/BudgetItem.cs deleted file mode 100644 index 34d50dc..0000000 --- a/Aberwyn/Models/BudgetItem.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Aberwyn.Models -{ - public class BudgetModel - { - public List BudgetItems { get; set; } = new List(); // Initialize with an empty list - } - - public class BudgetItem - { - public int ID { get; set; } - - // Option 1: Initialize with default values - public string Name { get; set; } = string.Empty; // Initialize to an empty string - public decimal Amount { get; set; } - public string Category { get; set; } = string.Empty; // Initialize to an empty string - public string SubCategory { get; set; } = string.Empty; // Initialize to an empty string - public int Month { get; set; } - public int Year { get; set; } - public string Description { get; set; } = string.Empty; // Initialize to an empty string - } - -} diff --git a/Aberwyn/Program.cs b/Aberwyn/Program.cs index 0fd38df..2788ef6 100644 --- a/Aberwyn/Program.cs +++ b/Aberwyn/Program.cs @@ -25,7 +25,7 @@ builder.Services.AddControllersWithViews() builder.Services.AddRazorPages(); builder.Services.AddHttpClient(); -// Configure your DbContext with MySQL +// Configure your DbContext with MySQLs builder.Services.AddDbContext(options => options.UseMySql( @@ -33,17 +33,24 @@ builder.Services.AddDbContext(options => new MySqlServerVersion(new Version(8, 0, 36)) )); + builder.Services.AddIdentity(options => { options.SignIn.RequireConfirmedAccount = false; }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - +builder.Services.Configure(options => +{ + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; +}); builder.Services.AddControllersWithViews(); // Register your services -builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -90,4 +97,12 @@ using (var scope = app.Services.CreateScope()) var services = scope.ServiceProvider; await IdentityDataInitializer.SeedData(services); } +using (var scope = app.Services.CreateScope()) +{ + var services = scope.ServiceProvider; + await IdentityDataInitializer.SeedData(services); + + var context = services.GetRequiredService(); + await TestDataSeeder.SeedBudget(context); +} app.Run(); diff --git a/Aberwyn/ViewComponents/RightSidebarViewComponents.cs b/Aberwyn/ViewComponents/RightSidebarViewComponents.cs index f2000a4..359a919 100644 --- a/Aberwyn/ViewComponents/RightSidebarViewComponents.cs +++ b/Aberwyn/ViewComponents/RightSidebarViewComponents.cs @@ -9,12 +9,7 @@ namespace Aberwyn.ViewComponents { public class RightSidebarViewComponent : ViewComponent { - private readonly BudgetService _budgetService; - public RightSidebarViewComponent(BudgetService budgetService) - { - _budgetService = budgetService; - } public async Task InvokeAsync() { @@ -23,9 +18,8 @@ namespace Aberwyn.ViewComponents int currentYear = DateTime.Now.Year; // Fetch the budget items using the service - var budgetItems = _budgetService.GetBudgetItems(currentMonth, currentYear); - return View(budgetItems); + return View(); } } } diff --git a/Aberwyn/Views/Admin/Index.cshtml b/Aberwyn/Views/Admin/Index.cshtml index e66a6a4..317396c 100644 --- a/Aberwyn/Views/Admin/Index.cshtml +++ b/Aberwyn/Views/Admin/Index.cshtml @@ -8,6 +8,23 @@ +

Skapa ny användare

+ @if (TempData["Message"] != null) +{ +
@TempData["Message"]
+} + +
+
+ + +
+
+ + +
+ +

Alla roller

diff --git a/Aberwyn/Views/Budget/Index.cshtml b/Aberwyn/Views/Budget/Index.cshtml new file mode 100644 index 0000000..3a9926f --- /dev/null +++ b/Aberwyn/Views/Budget/Index.cshtml @@ -0,0 +1,130 @@ +@using Microsoft.AspNetCore.Authorization +@{ + ViewData["Title"] = "Budget"; + @attribute [Authorize(Roles = "Budget")] +} +
+
+
+ + + {{ selectedMonthName }} {{ selectedYear }} + + + +
+
+ + +
+
+ + +
+ +
+
Totalt inkomst
{{ getTotalIncome() | number:0 }}
+
Total utgift
({{ getTotalExpense() | number:0 }})
+
Sparande
{{ getTotalSaving() | number:0 }}
+
Pengar kvar
{{ getLeftover() | number:0 }}
+
+ +
+
+ +
+
{{ cat.name }}
+ + +
+ + + + +
+
+ + + + +
+
+
+
+ + + + {{ item.name }} + + + {{ item.amount | number:0 }} + + +
+
+ +
+
+ +
+ + + + +
+ +
+
Summa
+
{{ getCategorySum(cat) | number:0 }}
+
+
+ +
+
+ Det finns ingen budgetdata för vald månad ({{ selectedMonth }}/{{ selectedYear }}). + +
+
+ + + + + + + \ No newline at end of file diff --git a/Aberwyn/Views/Home/Budget.cshtml b/Aberwyn/Views/Home/Budget.cshtml index e781fc4..28b9226 100644 --- a/Aberwyn/Views/Home/Budget.cshtml +++ b/Aberwyn/Views/Home/Budget.cshtml @@ -162,7 +162,7 @@ document.addEventListener('click', function () { $scope.$apply(() => $scope.isDatePickerVisible = false); }); - }); + }) diff --git a/Aberwyn/Views/Home/Index.cshtml b/Aberwyn/Views/Home/Index.cshtml index defe302..6d21884 100644 --- a/Aberwyn/Views/Home/Index.cshtml +++ b/Aberwyn/Views/Home/Index.cshtml @@ -12,10 +12,7 @@
-

Welcome to Aberwyn!

-

Your home for managing all your budget and meal planning needs.

-

We’re glad to have you here. Get started by exploring our features.

- Learn More +

Välkommen

Kika på Menyn om du vill
diff --git a/Aberwyn/Views/Home/Menu.cshtml b/Aberwyn/Views/Home/Menu.cshtml index 426d49e..1ec0b5d 100644 --- a/Aberwyn/Views/Home/Menu.cshtml +++ b/Aberwyn/Views/Home/Menu.cshtml @@ -49,20 +49,25 @@ -
+
-
+
-
{{day}}
-
Frukost: {{menu[day].breakfastMealName}}
-
Lunch: {{menu[day].lunchMealName}}
-
Middag: {{menu[day].dinnerMealName}}
-
Inte bestämd
+
{{day}}
+
Frukost: {{menu[day].breakfastMealName}}
+
Lunch: {{menu[day].lunchMealName}}
+
Middag: {{menu[day].dinnerMealName}}
+
Inte bestämd
-
-
+
- + + + diff --git a/Aberwyn/Views/Shared/_Layout.cshtml b/Aberwyn/Views/Shared/_Layout.cshtml index 4f413d4..90cabce 100644 --- a/Aberwyn/Views/Shared/_Layout.cshtml +++ b/Aberwyn/Views/Shared/_Layout.cshtml @@ -27,13 +27,13 @@
  • Home
  • @if (User.IsInRole("Admin") || User.IsInRole("Budget")) { -
  • Budget
  • +
  • Budget
  • } -
  • Menyöversikt
  • +
  • Veckomeny
  • @if (User.IsInRole("Admin") || User.IsInRole("Chef")) { -
  • Veckomeny
  • -
  • Matlista
  • +
  • Administrera Veckomeny
  • +
  • Måltider
  • } @if (User.IsInRole("Admin")) { diff --git a/Aberwyn/wwwroot/css/budget.css b/Aberwyn/wwwroot/css/budget.css index 4824ce0..526ea4b 100644 --- a/Aberwyn/wwwroot/css/budget.css +++ b/Aberwyn/wwwroot/css/budget.css @@ -1,81 +1,352 @@ -/* Budget Container */ -.budget-container { +:root { + --text-main: #1E293B; + --text-sub: #64748B; + --bg-main: #f9fafb; + --bg-card: #f1f5f9; + --border-color: #e5e7eb; + --card-income: #f97316; + --card-expense: #ef4444; + --card-savings: #facc15; + --card-leftover: #86efac; + --btn-edit: #3b82f6; + --btn-check: #10b981; + --btn-delete: #ef4444; +} + +body { + background-color: var(--bg-main); + color: var(--text-main); + font-family: 'Segoe UI', sans-serif; + font-size: 14px; + font-weight: 500; +} + +.budget-page { + padding: 16px 24px; + margin-left: 0; + max-width: unset; +} + +.budget-header { display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; flex-wrap: wrap; - gap: 10px; /* Reduced gap for tighter layout */ - padding: 0; /* Removed padding */ - margin: 0; /* Removed margin */ + gap: 12px; + padding-right: 8px; } -/* Category Block */ -.category-block { - flex: 1 1 300px; - max-width: 230px; - min-width: 180px; - margin: 0; /* Removed margin */ -} - -.category-header { - display: flex; - justify-content: space-between; /* Aligns the category name and total */ - align-items: center; /* Center vertically */ - padding: 5px; /* Retain padding */ - background-color: #4A6572; /* Muted blue-gray */ - color: #ffffff; - border-radius: 8px; - margin-bottom: 10px; - font-weight: bold; - max-width: 100%; /* Ensures the header doesn't exceed the width of its container */ - box-sizing: border-box; /* Include padding in width calculation */ -} - -.total { - font-weight: bold; - color: #ffffff; /* White for visibility */ - background-color: rgba(74, 101, 114, 0.7); /* Semi-transparent muted blue-gray */ - padding: 5px 10px; /* Add some padding for spacing */ - border-radius: 5px; /* Rounded corners */ -} - -/* Items Layout */ -.items-wrapper { - display: flex; - min-width: 200px; - flex-direction: column; - padding: 0; /* Removed padding */ - margin: 0; /* Removed margin */ -} - -/* Item Styles */ -.item { +.month-nav-bar { display: flex; align-items: center; - margin-bottom: 5px; /* Space between items */ - gap: 5px; /* Reduced gap between item components */ + gap: 6px; + position: relative; } -/* Input Styles */ -.item-name input { - flex: 1; /* Compact size for amount field */ - max-width: 20ch; /* Allow up to 7 characters */ - padding: 6px; /* Slightly reduced padding */ +.month-label { + padding: 4px 10px; + background-color: #e2e8f0; + border-radius: 6px; + cursor: pointer; + font-weight: 600; + font-size: 16px; + text-align: center; + min-width: 90px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.menu-container { + position: relative; +} + +.month-picker-dropdown select, +.month-picker-dropdown input { + font-size: 13px; + padding: 5px; + border-radius: 6px; + border: 1px solid var(--border-color); + width: 100%; + box-sizing: border-box; + margin-bottom: 6px; +} + +.nav-button { + background-color: #e2e8f0; + color: var(--text-main); border: none; - border-radius: 5px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - background-color: #f9f9f9; - transition: background-color 0.3s; + padding: 4px 8px; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + transition: background 0.2s; + font-size: 16px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); } -.item-amount input { - flex: 1; /* Compact size for amount field */ - max-width: 7ch; /* Allow up to 7 characters */ - padding: 6px; /* Slightly reduced padding */ + .nav-button:hover { + background-color: #cbd5e1; + } + +.summary-cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin: 16px 0 12px; +} + +.summary-card { + background-color: var(--bg-card); + padding: 8px 10px; + border-radius: 10px; + text-align: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-weight: 600; + font-size: 13px; + min-width: 130px; +} + + .summary-card.income { + border-top: 3px solid var(--card-income); + } + + .summary-card.expense { + border-top: 3px solid var(--card-expense); + color: var(--card-expense); + } + + .summary-card.savings { + border-top: 3px solid var(--card-savings); + } + + .summary-card.leftover { + border-top: 3px solid var(--card-leftover); + } + +.budget-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; + margin-bottom: 24px; +} + +.budget-card { + display: flex; + flex-direction: column; + height: 350px; + position: relative; + overflow-x: hidden; + overflow-y: visible; +} + +.item-list { + flex: 1 1 auto; + overflow-y: auto; + padding: 8px 12px; + background: rgba(255, 0, 0, 0.05); /* tillfällig bakgrund för felsökning */ +} + + + +.card-header { + position: sticky; + top: 0; + z-index: 3; /* högre än total-row */ + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + font-weight: 600; + color: #ffffff; + font-size: 14px; + flex-wrap: wrap; + gap: 6px; +} + + .card-header.income { + background-color: var(--card-income); + } + + .card-header.expense { + background-color: var(--card-expense); + } + + .card-header.savings { + background-color: var(--card-savings); + } + + .card-header.leftover { + background-color: var(--card-leftover); + } + + + + + +.item-row { + display: flex; + flex-wrap: nowrap; + justify-content: space-between; + align-items: center; + gap: 8px; + font-size: 13px; +} + + .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; + } + +.amount { + min-width: 70px; + text-align: right; + 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 { + background: none; border: none; - border-radius: 5px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - background-color: #f9f9f9; - transition: background-color 0.3s; + cursor: pointer; + font-size: 13px; + padding: 4px; + transition: color 0.2s; } -/* Focus Effect */ -.item-name input:focus + .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); + } + +.dropdown-menu { + position: absolute; + right: 0; + top: calc(100% + 6px); + background-color: #ffffff; + border: 1px solid var(--border-color); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 1000; + padding: 6px; + display: none; + gap: 4px; + flex-wrap: wrap; + min-width: 200px; +} + +.menu-container.open .dropdown-menu { + display: flex; +} + +.dropdown-menu button { + padding: 5px 8px; + border-radius: 6px; + border: 1px solid #ddd; + background: #f3f4f6; + cursor: pointer; + font-size: 12px; + flex: 1; + font-weight: 500; +} + + .dropdown-menu button:hover { + background-color: #e5e7eb; + } + +.no-data { + text-align: center; + padding: 16px; + font-size: 14px; + color: var(--text-sub); + border: 1px dashed var(--border-color); + border-radius: 8px; + background-color: #fff; + margin-top: 12px; + font-weight: 500; +} + +.toast { + position: fixed; + bottom: 20px; + right: 20px; + background: #10B981; + color: white; + padding: 10px 16px; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + opacity: 0; + transform: translateY(20px); + transition: opacity 0.3s, transform 0.3s; + z-index: 9999; + font-weight: 500; +} + + .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; +} + + .drop-placeholder.drag-over { + border-top: 3px solid #3B82F6; /* blå rand */ + background-color: rgba(59, 130, 246, 0.15); /* ljusblå fyllning */ + height: 12px; + } + + +/* 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 */ +} + +.drop-zone.drag-over { + border: 2px dashed #16a34a; /* grön */ + background-color: rgba(22, 163, 74, 0.1); +} diff --git a/Aberwyn/wwwroot/css/menu.css b/Aberwyn/wwwroot/css/menu.css index 324140a..a762d31 100644 --- a/Aberwyn/wwwroot/css/menu.css +++ b/Aberwyn/wwwroot/css/menu.css @@ -4,9 +4,9 @@ --text: #2D3748; --accent: #3182CE; --card-bg: #ffffff; - --border-radius: 10px; - --spacing: 20px; - --font-size: 1rem; + --border-radius: 6px; + --spacing: 12px; + --font-size: 0.9rem; --faint-text: #718096; --subtle-text: #4A5568; } @@ -46,7 +46,7 @@ body { } h1 { - font-size: 2rem; + font-size: 1.5rem; margin-bottom: var(--spacing); text-align: center; color: var(--accent); @@ -56,7 +56,7 @@ h1 { display: flex; justify-content: center; align-items: center; - gap: 1rem; + gap: 0.5rem; margin-bottom: var(--spacing); font-size: var(--font-size); color: var(--subtle-text); @@ -65,12 +65,13 @@ h1 { .date-picker button { background: var(--accent); border: none; - padding: 0.5rem 1rem; + padding: 0.3rem 0.8rem; border-radius: var(--border-radius); cursor: pointer; font-weight: bold; color: #ffffff; transition: background 0.2s ease; + font-size: 0.9rem; } .date-picker button:hover { @@ -83,7 +84,7 @@ h1 { right: var(--spacing); display: flex; align-items: center; - gap: 8px; + gap: 4px; } .view-selector select { @@ -95,8 +96,8 @@ h1 { background: none; border: none; color: var(--text); - font-size: 1.2rem; - padding: 6px; + font-size: 1rem; + padding: 4px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; @@ -109,23 +110,26 @@ h1 { .list-view .day-item { background: var(--card-bg); - margin-bottom: var(--spacing); - padding: var(--spacing); + margin-bottom: 10px; + padding: 10px; border-radius: var(--border-radius); + text-align: center; } .day-header { - font-size: 1.4rem; + font-size: 1.1rem; color: var(--accent); - margin-bottom: 8px; + margin-bottom: 4px; position: relative; + text-align: center; } .day-header::after { content: ''; position: absolute; bottom: -4px; - left: 0; + left: 50%; + transform: translateX(-50%); width: 40px; height: 3px; background: var(--accent); @@ -133,8 +137,8 @@ h1 { } .meal-selection { - margin: 6px 0; - font-size: 1rem; + margin: 2px 0; + font-size: 0.9rem; } .meal-selection a { @@ -149,28 +153,38 @@ h1 { .not-assigned { color: var(--faint-text); font-style: italic; + font-size: 0.85rem; } .card-view .card-container { display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: var(--spacing); + width: 100%; } .meal-card { position: relative; - height: 200px; + height: 160px; + width: 100%; color: #fff; + max-height: 200px; + background-color: #4A5568; /* fallback-bakgrund */ background-size: cover; background-position: center; border-radius: var(--border-radius); overflow: hidden; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); - display: flex; - align-items: flex-end; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.25); cursor: pointer; + transition: transform 0.2s ease; + display: flex; + align-items: stretch; } + .meal-card:hover { + transform: scale(1.02); + } + .meal-card::before { content: ''; position: absolute; @@ -187,18 +201,30 @@ h1 { .card-content { position: relative; - padding: 12px; z-index: 1; width: 100%; + padding: 12px 10px; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + background: rgba(0, 0, 0, 0.4); + text-align: center; + box-sizing: border-box; } .card-content .day { - font-size: 1.2rem; + font-size: 1rem; font-weight: bold; margin-bottom: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .card-content .meal { - font-size: 0.9rem; - line-height: 1.2; + font-size: 0.8rem; + line-height: 1.3; + margin-bottom: 2px; + word-break: break-word; } diff --git a/Aberwyn/wwwroot/js/budget-dragdrop.js b/Aberwyn/wwwroot/js/budget-dragdrop.js new file mode 100644 index 0000000..31823f8 --- /dev/null +++ b/Aberwyn/wwwroot/js/budget-dragdrop.js @@ -0,0 +1,231 @@ +// Drag & Drop directives for Budget Page + +app.directive('draggableItem', function () { + return { + restrict: 'A', + scope: { + item: '=', + category: '=', + onItemDrop: '&' + }, + link: function (scope, element) { + function isEditing() { + return scope.category && scope.category.editing; + } + + function updateDraggableAttr() { + element[0].setAttribute('draggable', isEditing() ? 'true' : 'false'); + element[0].classList.toggle('draggable-enabled', isEditing()); + } + + 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', + scope: { + category: '=', + index: '=', + onDropItem: '&' + }, + link: function (scope, element) { + function isEditing() { + return scope.category && scope.category.editing; + } + + element.addClass('drop-zone'); + element.addClass('drop-placeholder'); + + const label = document.createElement('div'); + label.innerText = 'DROP HERE'; + label.style.textAlign = 'center'; + label.style.fontSize = '12px'; + label.style.color = '#166534'; + label.style.display = 'none'; + element[0].appendChild(label); + + element[0].addEventListener('dragover', function (e) { + if (!isEditing()) return; + const rawData = e.dataTransfer.getData('text/plain'); + if (!rawData.includes('"type":"item"')) return; + + 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; + + scope.$apply(() => { + scope.onDropItem({ + data: data, + targetCategory: scope.category, + targetIndex: scope.index + }); + }); + } catch (err) { + console.error("Error parsing drop data:", err); + } + }); + } + }; +}); + +app.directive('dropFallback', function () { + return { + restrict: 'A', + scope: { + category: '=', + onDropItem: '&' + }, + link: function (scope, element) { + element.addClass('drop-zone'); + + const label = document.createElement('div'); + label.innerText = 'DROP HERE'; + label.style.textAlign = 'center'; + label.style.fontSize = '12px'; + label.style.color = '#166534'; + label.style.display = 'none'; + element[0].appendChild(label); + + element[0].addEventListener('dragover', function (e) { + const data = e.dataTransfer.getData('text/plain'); + if (!data.includes('"type":"item"')) return; + 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) { + e.preventDefault(); + element[0].classList.remove('drag-over'); + label.style.display = 'none'; + + const data = JSON.parse(e.dataTransfer.getData('text/plain')); + if (data.type !== 'item') return; + + scope.$apply(() => { + scope.onDropItem({ + data: data, + targetCategory: scope.category, + targetIndex: scope.category.items.length + }); + }); + }); + } + }; +}); + +app.directive('draggableCategory', function () { + return { + restrict: 'A', + scope: { + category: '=', + onCategoryDrop: '&' + }, + link: function (scope, element) { + const header = element[0].querySelector('.card-header'); + if (!header) return; + + function updateDraggableAttr() { + header.setAttribute('draggable', scope.category.allowDrag ? 'true' : 'false'); + } + + scope.$watch('category.allowDrag', updateDraggableAttr); + updateDraggableAttr(); + + header.addEventListener('dragstart', function (e) { + if (!scope.category.allowDrag) { + e.preventDefault(); + return; + } + e.dataTransfer.setData('text/plain', JSON.stringify({ + type: 'category', + categoryId: scope.category.id + })); + }); + + element[0].addEventListener('dragover', function (e) { + e.preventDefault(); + }); + + 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; + + scope.$apply(() => { + scope.onCategoryDrop({ + data: data, + targetCategory: scope.category + }); + }); + }); + + element[0].addEventListener('dragenter', function (e) { + e.preventDefault(); + element[0].classList.add('drag-over'); + }); + + element[0].addEventListener('dragleave', function (e) { + e.preventDefault(); + element[0].classList.remove('drag-over'); + }); + } + }; +}); diff --git a/Aberwyn/wwwroot/js/budget.js b/Aberwyn/wwwroot/js/budget.js new file mode 100644 index 0000000..577a368 --- /dev/null +++ b/Aberwyn/wwwroot/js/budget.js @@ -0,0 +1,430 @@ +var app = angular.module('budgetApp', []); +console.log("budget.js loaded"); +app.controller('BudgetController', function ($scope, $http) { + $scope.budget = null; + $scope.loading = false; + $scope.error = null; + $scope.menuOpen = false; + + const today = new Date(); + $scope.selectedYear = today.getFullYear(); + $scope.selectedMonth = today.getMonth() + 1; + $scope.tempMonth = $scope.monthNames?.[today.getMonth()] || ""; + $scope.tempYear = $scope.selectedYear; + + $scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"]; + + $scope.getMonthName = function (month) { + return $scope.monthNames[month - 1] || ""; + }; + + $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); + + $scope.toggleMenu = function (e) { + e.stopPropagation(); + $scope.menuOpen = !$scope.menuOpen; + }; + + document.addEventListener('click', function () { + $scope.$apply(() => { + $scope.menuOpen = false; + }); + }); + + $scope.showToast = function (message, isError = false) { + const toast = document.createElement("div"); + toast.className = "toast" + (isError ? " error" : "") + " show"; + toast.innerText = message; + document.body.appendChild(toast); + + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => toast.remove(), 300); + }, 3000); + }; + + $scope.loadBudget = function () { + $scope.loading = true; + $scope.error = null; + $scope.budget = null; + + $http.get(`/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`) + .then(function (response) { + const raw = response.data; + if (raw && raw.Categories) { + const categories = raw.Categories.map(cat => ({ + id: cat.Id, + name: cat.Name, + color: cat.Color, + editing: false, + allowDrag: false, + items: (cat.Items || []).map((item, index) => ({ + id: item.Id, + name: item.Name, + amount: parseFloat(item.Amount), + isExpense: item.IsExpense === true, + includeInSummary: item.IncludeInSummary === true, + order: item.Order ?? index + })).sort((a, b) => a.order - b.order) + })); + + $scope.budget = { + id: raw.Id, + year: raw.Year, + month: raw.Month, + categories: categories.sort((a, b) => a.order - b.order) + }; + } else { + $scope.budget = { categories: [] }; + } + $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); + }) + .catch(function (error) { + if (error.status === 404) { + $scope.budget = { categories: [] }; + } else { + $scope.error = "Kunde inte ladda budgetdata."; + $scope.showToast("Fel vid laddning av budgetdata", true); + console.error("Budget API error:", error); + } + }) + .finally(function () { + $scope.loading = false; + }); + }; + + $scope.saveCategory = function (category) { + if (category.newItemName && category.newItemAmount) { + const newItem = { + name: category.newItemName, + amount: parseFloat(category.newItemAmount), + isExpense: true, + includeInSummary: true, + budgetCategoryId: category.id, + }; + + $http.post("/api/budget/item", newItem) + .then(res => { + if (res.data && res.data.id) { + newItem.id = res.data.id; + category.items.push(newItem); + category.newItemName = ""; + category.newItemAmount = ""; + $scope.showToast("Post sparad!"); + } else { + $scope.showToast("Fel vid sparande av ny post", true); + } + }); + } + + category.editing = false; + + const payload = { + id: category.id, + name: category.name, + color: category.color, + items: category.items.map((item, index) => ({ + id: item.id, + name: item.name, + amount: item.amount, + isExpense: item.isExpense, + includeInSummary: item.includeInSummary, + budgetCategoryId: category.id, + order: index + })) + }; + + $http.put(`/api/budget/category/${category.id}`, payload) + .then(() => $scope.showToast("Kategori sparad!")) + .catch(() => $scope.showToast("Fel vid sparande av kategori", true)); + }; + + $scope.deleteCategory = function (category) { + if (!confirm("Vill du verkligen ta bort kategorin och alla dess poster?")) return; + + $http.delete(`/api/budget/category/${category.id}`) + .then(() => { + $scope.budget.categories = $scope.budget.categories.filter(c => c.id !== category.id); + $scope.showToast("Kategori borttagen!"); + }) + .catch(err => { + console.error("Fel vid borttagning av kategori:", err); + $scope.showToast("Kunde inte ta bort kategori", true); + }); + }; + $scope.copyPreviousMonthSafe = function () { + console.log("click OK"); + + $scope.menuOpen = false; + setTimeout(function () { + $scope.copyPreviousMonth(); + }, 10); + }; + $scope.deleteMonth = function () { + if (!confirm("Vill du verkligen ta bort alla data för denna månad?")) return; + + const year = $scope.selectedYear; + const month = $scope.selectedMonth; + + $http.delete(`/api/budget/${year}/${month}`) + .then(() => { + $scope.showToast("Månad borttagen!"); + $scope.loadBudget(); // tömmer sidan + }) + .catch(err => { + console.error("Kunde inte ta bort månad:", err); + $scope.showToast("Fel vid borttagning av månad", true); + }); + }; + + $scope.copyPreviousMonth = function () { + if (!confirm("Vill du kopiera föregående månad till den aktuella?")) { + return; + } + + console.log("Försöker kopiera föregående månad..."); + + 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 föregående månad:", err); + if (err.status === 404) { + $scope.showToast("Ingen föregående månad att kopiera från", true); + } else if (err.status === 400) { + $scope.showToast("Data finns redan för denna månad", true); + } else { + $scope.showToast("Kunde inte kopiera", true); + } + }); + }; + + $scope.cancelCategoryEdit = function (category) { + category.editing = false; + }; + + function getItemsFlat() { + if (!$scope.budget || !$scope.budget.categories) return []; + return $scope.budget.categories.flatMap(c => c.items || []); + } + + $scope.getCategorySum = function (category) { + return (category.items || []).reduce((sum, i) => sum + i.amount, 0); + }; + + $scope.getTotalIncome = function () { + return getItemsFlat().filter(i => !i.isExpense && i.includeInSummary).reduce((sum, i) => sum + i.amount, 0); + }; + + $scope.getTotalExpense = function () { + return getItemsFlat().filter(i => i.isExpense && i.includeInSummary).reduce((sum, i) => sum + i.amount, 0); + }; + + $scope.getTotalSaving = function () { + return getItemsFlat().filter(i => !i.isExpense && !i.includeInSummary).reduce((sum, i) => sum + i.amount, 0); + }; + + $scope.getLeftover = function () { + return $scope.getTotalIncome() - $scope.getTotalExpense(); + }; + + $scope.applyMonthSelection = function () { + const monthIndex = $scope.monthNames.indexOf($scope.tempMonth); + if (monthIndex >= 0 && $scope.tempYear) { + $scope.selectedMonth = monthIndex + 1; + $scope.selectedYear = parseInt($scope.tempYear); + $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); + $scope.showMonthPicker = false; + $scope.loadBudget(); + } + }; + + $scope.previousMonth = function () { + if ($scope.selectedMonth === 1) { + $scope.selectedMonth = 12; + $scope.selectedYear--; + } else { + $scope.selectedMonth--; + } + $scope.tempMonth = $scope.getMonthName($scope.selectedMonth); + $scope.tempYear = $scope.selectedYear; + $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); + $scope.loadBudget(); + }; + + $scope.nextMonth = function () { + if ($scope.selectedMonth === 12) { + $scope.selectedMonth = 1; + $scope.selectedYear++; + } else { + $scope.selectedMonth++; + } + $scope.tempMonth = $scope.getMonthName($scope.selectedMonth); + $scope.tempYear = $scope.selectedYear; + $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); + $scope.loadBudget(); + }; + + $scope.handleCategoryDrop = function (data, targetCategory) { + if (data.type !== 'category') return; // ⛔ stoppa om det är ett item-drag + + const categories = $scope.budget.categories; + const draggedIndex = categories.findIndex(c => c.id === data.categoryId); + const targetIndex = categories.findIndex(c => c.id === targetCategory.id); + + if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex) return; + + const moved = categories.splice(draggedIndex, 1)[0]; + categories.splice(targetIndex, 0, moved); + + categories.forEach((cat, i) => { + cat.order = i; + }); + + const payload = $scope.budget.categories.map(cat => ({ + id: cat.id, + name: cat.name, + color: cat.color, + order: cat.order, + year: $scope.selectedYear, + month: $scope.selectedMonth, + items: [] // tom lista – backend kräver den, men vi använder den inte här + })); + + + $http.put("/api/budget/category/order", payload) + .then(() => $scope.showToast("Kategorier omordnade!")) + .catch(() => $scope.showToast("Fel vid uppdatering av ordning", true)); + }; + + $scope.handleItemDrop = function ({ data, targetCategory, targetIndex }) { + const sourceCategory = $scope.budget.categories.find(c => c.id === data.fromCategoryId); + const draggedItem = sourceCategory?.items.find(i => i.id === data.itemId); + + if (!draggedItem || !targetCategory) return; + + // Ta bort från ursprung + const draggedIndex = sourceCategory.items.findIndex(i => i.id === draggedItem.id); + if (draggedIndex !== -1) { + sourceCategory.items.splice(draggedIndex, 1); + } + + // Uppdatera värden + draggedItem.budgetCategoryId = targetCategory.id; + const indexToInsert = typeof targetIndex === 'number' ? targetIndex : targetCategory.items.length; + targetCategory.items.splice(indexToInsert, 0, draggedItem); + + // Uppdatera ordning + targetCategory.items.forEach((item, i) => { + item.order = i; + }); + + // ✅ Skicka PUT för det flyttade itemet + const payload = { + id: draggedItem.id, + name: draggedItem.name, + amount: draggedItem.amount, + isExpense: draggedItem.isExpense, + includeInSummary: draggedItem.includeInSummary, + order: draggedItem.order, + budgetCategoryId: draggedItem.budgetCategoryId + }; + + $http.put(`/api/budget/item/${draggedItem.id}`, payload) + .then(() => { + console.log(">>> Sparad!"); + $scope.showToast("Post omplacerad!"); + }) + .catch(err => { + console.error("Kunde inte uppdatera post:", err); + $scope.showToast("Fel vid omplacering", true); + }); + }; + + $scope.handleItemPreciseDrop = function (data, targetCategory, targetIndex) { + if (data.type !== 'item') return; + + const sourceCategory = $scope.budget.categories.find(c => c.id === data.fromCategoryId); + const draggedItem = sourceCategory?.items.find(i => i.id === data.itemId); + + if (!draggedItem || !targetCategory) return; + + // Ta bort från ursprung + const draggedIndex = sourceCategory.items.findIndex(i => i.id === draggedItem.id); + if (draggedIndex !== -1) { + sourceCategory.items.splice(draggedIndex, 1); + } + + // Uppdatera kategori och lägg till på exakt plats + draggedItem.budgetCategoryId = targetCategory.id; + targetCategory.items.splice(targetIndex, 0, draggedItem); + + // Uppdatera ordning + targetCategory.items.forEach((item, i) => { + item.order = i; + }); + + // ✅ PUT för det flyttade itemet + const payload = { + id: draggedItem.id, + name: draggedItem.name, + amount: draggedItem.amount, + isExpense: draggedItem.isExpense, + includeInSummary: draggedItem.includeInSummary, + order: draggedItem.order, + budgetCategoryId: draggedItem.budgetCategoryId + }; + + $http.put(`/api/budget/item/${draggedItem.id}`, payload) + .then(() => { + console.log(">>> Sparad!"); + $scope.showToast("Post omplacerad!"); + }) + .catch(err => { + console.error("Kunde inte uppdatera post:", err); + $scope.showToast("Fel vid omplacering", true); + }); + }; + $scope.createNewCategory = function () { + const defaultName = "Ny kategori"; + const newOrder = $scope.budget.categories.length; // sist i listan + + const newCategory = { + name: defaultName, + color: "#666666", + year: $scope.selectedYear, + month: $scope.selectedMonth, + order: newOrder + }; + + $http.post("/api/budget/category", newCategory) + .then(res => { + if (res.data && res.data.id) { + $scope.budget.categories.push({ + id: res.data.id, + name: defaultName, + color: "#666666", + order: newOrder, + items: [], + editing: true, + allowDrag: false + }); + $scope.showToast("Kategori skapad!"); + } else { + $scope.showToast("Misslyckades med att skapa kategori", true); + } + }) + .catch(err => { + console.error("Fel vid skapande av kategori:", err); + $scope.showToast("Fel vid skapande av kategori", true); + }); + }; + + + $scope.loadBudget(); +});