diff --git a/Aberwyn/Controllers/BudgetApiController.cs b/Aberwyn/Controllers/BudgetApiController.cs index ee3d206..6f198aa 100644 --- a/Aberwyn/Controllers/BudgetApiController.cs +++ b/Aberwyn/Controllers/BudgetApiController.cs @@ -60,7 +60,8 @@ namespace Aberwyn.Controllers Name = i.Name, Amount = i.Amount, IsExpense = i.IsExpense, - IncludeInSummary = i.IncludeInSummary + IncludeInSummary = i.IncludeInSummary, + BudgetItemDefinitionId = i.BudgetItemDefinitionId }).ToList() }).ToList() }; @@ -83,7 +84,37 @@ namespace Aberwyn.Controllers if (category == null) return NotFound(); - // Uppdatera kategoriinformation + // Hämta eller skapa kategori-definition + if (updatedCategory.BudgetCategoryDefinitionId.HasValue) + { + var def = await _context.BudgetCategoryDefinitions + .FirstOrDefaultAsync(d => d.Id == updatedCategory.BudgetCategoryDefinitionId.Value); + if (def != null) + { + category.BudgetCategoryDefinitionId = def.Id; + } + } + else + { + // Om ingen ID angavs, försök hitta via namn + var def = await _context.BudgetCategoryDefinitions + .FirstOrDefaultAsync(d => d.Name.ToLower() == updatedCategory.Name.ToLower()); + + if (def == null) + { + def = new BudgetCategoryDefinition + { + Name = updatedCategory.Name, + Color = updatedCategory.Color ?? "#666666" + }; + _context.BudgetCategoryDefinitions.Add(def); + await _context.SaveChangesAsync(); + } + + category.BudgetCategoryDefinitionId = def.Id; + } + + // ✅ Uppdatera namn och färg (kan avvika från definitionens namn) category.Name = updatedCategory.Name; category.Color = updatedCategory.Color; @@ -173,19 +204,74 @@ namespace Aberwyn.Controllers await _context.SaveChangesAsync(); return Ok(); } + [HttpGet("definitions/items")] + public async Task GetItemDefinitions() + { + var definitions = await _context.BudgetItemDefinitions + .OrderBy(d => d.Name) + .ToListAsync(); + + return Ok(definitions); + } + + [HttpGet("definitions/categories")] + public async Task GetCategoryDefinitions() + { + var definitions = await _context.BudgetCategoryDefinitions + .OrderBy(d => d.Name) + .ToListAsync(); + + return Ok(definitions); + } + [HttpPost("item")] public async Task CreateItem([FromBody] BudgetItem newItem) { - if (newItem == null || newItem.BudgetCategoryId == 0) + if (newItem == null || newItem.BudgetCategoryId == 0 || string.IsNullOrWhiteSpace(newItem.Name)) return BadRequest("Ogiltig data."); + // ✅ Om BudgetItemDefinitionId är angiven, använd den + if (newItem.BudgetItemDefinitionId.HasValue && newItem.BudgetItemDefinitionId.Value > 0) + { + var existingDef = await _context.BudgetItemDefinitions + .FirstOrDefaultAsync(d => d.Id == newItem.BudgetItemDefinitionId); + + if (existingDef == null) + return BadRequest("Ogiltigt definition-ID."); + + // valfritt: du kan här också jämföra `newItem.Name != existingDef.Name` + // och t.ex. logga det som ett användarval av etikett. + } + else + { + // Om ID inte är angivet, sök på namn som fallback + var definition = await _context.BudgetItemDefinitions + .FirstOrDefaultAsync(d => d.Name.ToLower() == newItem.Name.ToLower()); + + if (definition == null) + { + definition = new BudgetItemDefinition + { + Name = newItem.Name, + IsExpense = newItem.IsExpense, + IncludeInSummary = newItem.IncludeInSummary + }; + _context.BudgetItemDefinitions.Add(definition); + await _context.SaveChangesAsync(); + } + + newItem.BudgetItemDefinitionId = definition.Id; + } + _context.BudgetItems.Add(newItem); await _context.SaveChangesAsync(); return Ok(new { id = newItem.Id }); } + + [HttpDelete("item/{id}")] public async Task DeleteItem(int id) { @@ -227,28 +313,41 @@ namespace Aberwyn.Controllers 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 + await _context.SaveChangesAsync(); + } + + var definition = await _context.BudgetCategoryDefinitions + .FirstOrDefaultAsync(d => d.Name.ToLower() == newCategoryDto.Name.ToLower()); + + if (definition == null) + { + definition = new BudgetCategoryDefinition + { + Name = newCategoryDto.Name, + Color = newCategoryDto.Color ?? "#666666" + }; + _context.BudgetCategoryDefinitions.Add(definition); + await _context.SaveChangesAsync(); } var category = new BudgetCategory { Name = newCategoryDto.Name, - Color = newCategoryDto.Color ?? "#666666", + Color = newCategoryDto.Color ?? definition.Color, BudgetPeriodId = period.Id, - Order = newCategoryDto.Order + Order = newCategoryDto.Order, + BudgetCategoryDefinitionId = definition.Id }; _context.BudgetCategories.Add(category); @@ -256,6 +355,7 @@ namespace Aberwyn.Controllers return Ok(new { id = category.Id }); } + [HttpDelete("category/{id}")] public async Task DeleteCategory(int id) { @@ -285,7 +385,6 @@ namespace Aberwyn.Controllers 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) @@ -295,7 +394,6 @@ namespace Aberwyn.Controllers if (previousPeriod == null) return NotFound("Ingen data att kopiera från."); - // Skapa ny period var newPeriod = new BudgetPeriod { Year = year, @@ -305,13 +403,15 @@ namespace Aberwyn.Controllers Name = cat.Name, Color = cat.Color, Order = cat.Order, + BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId, // 🟢 Lägg till denna rad Items = cat.Items.Select(item => new BudgetItem { Name = item.Name, Amount = item.Amount, IsExpense = item.IsExpense, IncludeInSummary = item.IncludeInSummary, - Order = item.Order + Order = item.Order, + BudgetItemDefinitionId = item.BudgetItemDefinitionId // 🟢 Lägg till denna rad }).ToList() }).ToList() }; @@ -323,5 +423,6 @@ namespace Aberwyn.Controllers } + } } diff --git a/Aberwyn/Controllers/ReportApiController.cs b/Aberwyn/Controllers/ReportApiController.cs new file mode 100644 index 0000000..256da90 --- /dev/null +++ b/Aberwyn/Controllers/ReportApiController.cs @@ -0,0 +1,64 @@ +using Aberwyn.Data; +using Aberwyn.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + + +namespace Aberwyn.Controllers +{ + [Authorize(Roles = "Budget")] + [ApiController] + [Route("api/report")] + public class ReportApiController : ControllerBase + + + { + + private readonly ApplicationDbContext _context; + + public ReportApiController(ApplicationDbContext context) + { + _context = context; + } + + [HttpPost] + public async Task GetReport([FromBody] BudgetReportRequestDto request) + { + var start = new DateTime(request.StartYear, request.StartMonth, 1); + var end = new DateTime(request.EndYear, request.EndMonth, 1); + + var items = await _context.BudgetItems + .Include(i => i.BudgetItemDefinition) + .Include(i => i.BudgetCategory) + .ThenInclude(c => c.BudgetPeriod) + .Where(i => + i.BudgetCategory.BudgetPeriod.Year * 12 + i.BudgetCategory.BudgetPeriod.Month >= start.Year * 12 + start.Month && + i.BudgetCategory.BudgetPeriod.Year * 12 + i.BudgetCategory.BudgetPeriod.Month <= end.Year * 12 + end.Month && + request.DefinitionIds.Contains(i.BudgetItemDefinitionId ?? -1)) + .ToListAsync(); + + var grouped = items + .GroupBy(i => new { i.BudgetCategory.BudgetPeriod.Year, i.BudgetCategory.BudgetPeriod.Month }) + .Select(g => new BudgetReportResultDto + { + Year = g.Key.Year, + Month = g.Key.Month, + Definitions = g + .GroupBy(i => new { i.BudgetItemDefinitionId, i.BudgetItemDefinition.Name }) + .Select(dg => new DefinitionSumDto + { + DefinitionId = dg.Key.BudgetItemDefinitionId ?? 0, + DefinitionName = dg.Key.Name, + TotalAmount = dg.Sum(x => x.Amount) + }).ToList() + }) + .OrderBy(r => r.Year).ThenBy(r => r.Month) + .ToList(); + + return Ok(grouped); + } + + + } +} diff --git a/Aberwyn/Controllers/ReportController.cs b/Aberwyn/Controllers/ReportController.cs new file mode 100644 index 0000000..e62f90c --- /dev/null +++ b/Aberwyn/Controllers/ReportController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Aberwyn.Controllers +{ + [Authorize(Roles = "Budget")] + public class ReportController : Controller + { + public IActionResult BudgetReport() + { + return View("BudgetReport"); + } + } +} diff --git a/Aberwyn/Data/ApplicationDbContext.cs b/Aberwyn/Data/ApplicationDbContext.cs index 12c6b0f..79d7bcb 100644 --- a/Aberwyn/Data/ApplicationDbContext.cs +++ b/Aberwyn/Data/ApplicationDbContext.cs @@ -17,6 +17,9 @@ namespace Aberwyn.Data public DbSet PushSubscribers { get; set; } public DbSet PizzaOrders { get; set; } public DbSet AppSettings { get; set; } + public DbSet BudgetItemDefinitions { get; set; } + public DbSet BudgetCategoryDefinitions { get; set; } + } } diff --git a/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs index 29763e8..00293c1 100644 --- a/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs +++ b/Aberwyn/Migrations/20250526121643_AddBudgetDefinitions.Designer.cs @@ -418,7 +418,7 @@ namespace Aberwyn.Migrations modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => { - b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "BudgetItemDefinition") .WithMany() .HasForeignKey("BudgetCategoryDefinitionId"); @@ -430,7 +430,7 @@ namespace Aberwyn.Migrations b.Navigation("BudgetPeriod"); - b.Navigation("Definition"); + b.Navigation("BudgetItemDefinition"); }); modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => @@ -441,13 +441,13 @@ namespace Aberwyn.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Aberwyn.Models.BudgetItemDefinition", "Definition") + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") .WithMany() .HasForeignKey("BudgetItemDefinitionId"); b.Navigation("BudgetCategory"); - b.Navigation("Definition"); + b.Navigation("BudgetItemDefinition"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.Designer.cs b/Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.Designer.cs new file mode 100644 index 0000000..d33f6bb --- /dev/null +++ b/Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.Designer.cs @@ -0,0 +1,516 @@ +// +using System; +using Aberwyn.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Aberwyn.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250526133558_AddBudgetItemDefinitions")] + partial class AddBudgetItemDefinitions + { + 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.HasKey("Id"); + + b.HasIndex("BudgetCategoryId"); + + b.HasIndex("BudgetItemDefinitionId"); + + b.ToTable("BudgetItems"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("DefaultCategory") + .HasColumnType("longtext"); + + b.Property("IncludeInSummary") + .HasColumnType("tinyint(1)"); + + b.Property("IsExpense") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("BudgetItemDefinitions"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("BudgetPeriods"); + }); + + modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("CustomerName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IngredientsJson") + .HasColumnType("longtext"); + + b.Property("OrderedAt") + .HasColumnType("datetime(6)"); + + b.Property("PizzaName") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Status") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PizzaOrders"); + }); + + modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("Auth") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Endpoint") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("P256DH") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("PushSubscribers"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("longtext"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("ClaimType") + .HasColumnType("longtext"); + + b.Property("ClaimValue") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("ProviderKey") + .HasColumnType("varchar(255)"); + + b.Property("ProviderDisplayName") + .HasColumnType("longtext"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("RoleId") + .HasColumnType("varchar(255)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("varchar(255)"); + + b.Property("LoginProvider") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => + { + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetCategoryDefinitionId"); + + b.HasOne("Aberwyn.Models.BudgetPeriod", "BudgetPeriod") + .WithMany("Categories") + .HasForeignKey("BudgetPeriodId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BudgetPeriod"); + + b.Navigation("BudgetItemDefinition"); + }); + + modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => + { + b.HasOne("Aberwyn.Models.BudgetCategory", "BudgetCategory") + .WithMany("Items") + .HasForeignKey("BudgetCategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") + .WithMany() + .HasForeignKey("BudgetItemDefinitionId"); + + b.Navigation("BudgetCategory"); + + b.Navigation("BudgetItemDefinition"); + }); + + modelBuilder.Entity("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/20250526133558_AddBudgetItemDefinitions.cs b/Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.cs new file mode 100644 index 0000000..c5c25e5 --- /dev/null +++ b/Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.cs @@ -0,0 +1,111 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Aberwyn.Migrations +{ + public partial class AddBudgetItemDefinitions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinition_BudgetCategoryDefi~", + table: "BudgetCategories"); + + migrationBuilder.DropForeignKey( + name: "FK_BudgetItems_BudgetItemDefinition_BudgetItemDefinitionId", + table: "BudgetItems"); + + migrationBuilder.DropPrimaryKey( + name: "PK_BudgetItemDefinition", + table: "BudgetItemDefinition"); + + migrationBuilder.DropPrimaryKey( + name: "PK_BudgetCategoryDefinition", + table: "BudgetCategoryDefinition"); + + migrationBuilder.RenameTable( + name: "BudgetItemDefinition", + newName: "BudgetItemDefinitions"); + + migrationBuilder.RenameTable( + name: "BudgetCategoryDefinition", + newName: "BudgetCategoryDefinitions"); + + migrationBuilder.AddPrimaryKey( + name: "PK_BudgetItemDefinitions", + table: "BudgetItemDefinitions", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_BudgetCategoryDefinitions", + table: "BudgetCategoryDefinitions", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinitions_BudgetCategoryDef~", + table: "BudgetCategories", + column: "BudgetCategoryDefinitionId", + principalTable: "BudgetCategoryDefinitions", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetItems_BudgetItemDefinitions_BudgetItemDefinitionId", + table: "BudgetItems", + column: "BudgetItemDefinitionId", + principalTable: "BudgetItemDefinitions", + principalColumn: "Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinitions_BudgetCategoryDef~", + table: "BudgetCategories"); + + migrationBuilder.DropForeignKey( + name: "FK_BudgetItems_BudgetItemDefinitions_BudgetItemDefinitionId", + table: "BudgetItems"); + + migrationBuilder.DropPrimaryKey( + name: "PK_BudgetItemDefinitions", + table: "BudgetItemDefinitions"); + + migrationBuilder.DropPrimaryKey( + name: "PK_BudgetCategoryDefinitions", + table: "BudgetCategoryDefinitions"); + + migrationBuilder.RenameTable( + name: "BudgetItemDefinitions", + newName: "BudgetItemDefinition"); + + migrationBuilder.RenameTable( + name: "BudgetCategoryDefinitions", + newName: "BudgetCategoryDefinition"); + + migrationBuilder.AddPrimaryKey( + name: "PK_BudgetItemDefinition", + table: "BudgetItemDefinition", + column: "Id"); + + migrationBuilder.AddPrimaryKey( + name: "PK_BudgetCategoryDefinition", + table: "BudgetCategoryDefinition", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetCategories_BudgetCategoryDefinition_BudgetCategoryDefi~", + table: "BudgetCategories", + column: "BudgetCategoryDefinitionId", + principalTable: "BudgetCategoryDefinition", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_BudgetItems_BudgetItemDefinition_BudgetItemDefinitionId", + table: "BudgetItems", + column: "BudgetItemDefinitionId", + principalTable: "BudgetItemDefinition", + principalColumn: "Id"); + } + } +} diff --git a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs index 007abe5..4366b85 100644 --- a/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Aberwyn/Migrations/ApplicationDbContextModelSnapshot.cs @@ -150,7 +150,7 @@ namespace Aberwyn.Migrations b.HasKey("Id"); - b.ToTable("BudgetCategoryDefinition"); + b.ToTable("BudgetCategoryDefinitions"); }); modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => @@ -211,7 +211,7 @@ namespace Aberwyn.Migrations b.HasKey("Id"); - b.ToTable("BudgetItemDefinition"); + b.ToTable("BudgetItemDefinitions"); }); modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b => @@ -416,7 +416,7 @@ namespace Aberwyn.Migrations modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b => { - b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition") + b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "BudgetItemDefinition") .WithMany() .HasForeignKey("BudgetCategoryDefinitionId"); @@ -428,7 +428,7 @@ namespace Aberwyn.Migrations b.Navigation("BudgetPeriod"); - b.Navigation("Definition"); + b.Navigation("BudgetItemDefinition"); }); modelBuilder.Entity("Aberwyn.Models.BudgetItem", b => @@ -439,13 +439,13 @@ namespace Aberwyn.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Aberwyn.Models.BudgetItemDefinition", "Definition") + b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition") .WithMany() .HasForeignKey("BudgetItemDefinitionId"); b.Navigation("BudgetCategory"); - b.Navigation("Definition"); + b.Navigation("BudgetItemDefinition"); }); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => diff --git a/Aberwyn/Models/Budget.cs b/Aberwyn/Models/Budget.cs index 420bfe2..4bacdaf 100644 --- a/Aberwyn/Models/Budget.cs +++ b/Aberwyn/Models/Budget.cs @@ -41,7 +41,7 @@ namespace Aberwyn.Models [JsonIgnore] [ValidateNever] - public BudgetItemDefinition? Definition { get; set; } + public BudgetItemDefinition? BudgetItemDefinition { get; set; } public int BudgetCategoryId { get; set; } @@ -67,6 +67,8 @@ namespace Aberwyn.Models public string Color { get; set; } public List Items { get; set; } = new(); public int Order { get; set; } + public int? BudgetCategoryDefinitionId { get; set; } + public int Year { get; set; } public int Month { get; set; } @@ -78,6 +80,7 @@ namespace Aberwyn.Models public string Name { get; set; } public decimal Amount { get; set; } public int Order { get; set; } + public int? BudgetItemDefinitionId { get; set; } public bool IsExpense { get; set; } public bool IncludeInSummary { get; set; } diff --git a/Aberwyn/Models/ReportModel.cs b/Aberwyn/Models/ReportModel.cs new file mode 100644 index 0000000..9b7f3ca --- /dev/null +++ b/Aberwyn/Models/ReportModel.cs @@ -0,0 +1,25 @@ +namespace Aberwyn.Models +{ + public class BudgetReportRequestDto + { + public List DefinitionIds { get; set; } = new(); + public int StartYear { get; set; } + public int StartMonth { get; set; } + public int EndYear { get; set; } + public int EndMonth { get; set; } + } + + public class BudgetReportResultDto + { + public int Year { get; set; } + public int Month { get; set; } + public List Definitions { get; set; } = new(); + } + + public class DefinitionSumDto + { + public int DefinitionId { get; set; } + public string DefinitionName { get; set; } + public decimal TotalAmount { get; set; } + } +} diff --git a/Aberwyn/Views/Budget/Index.cshtml b/Aberwyn/Views/Budget/Index.cshtml index 204540c..48586a8 100644 --- a/Aberwyn/Views/Budget/Index.cshtml +++ b/Aberwyn/Views/Budget/Index.cshtml @@ -1,8 +1,10 @@ -@using Microsoft.AspNetCore.Authorization +@attribute [Authorize(Roles = "Budget")] +@using Microsoft.AspNetCore.Authorization + @{ ViewData["Title"] = "Budget"; - @attribute [Authorize(Roles = "Budget")] } +
@@ -85,8 +87,8 @@ style="opacity: 0.5; padding-right: 6px; cursor: grab;"> - {{ item.name }} - + {{ item.name }} + {{ item.amount | number:0 }} @@ -96,6 +98,9 @@
+ + #{{ menuItem.definitionName }} +
-
- - - -
+ +
@@ -143,6 +149,41 @@
+
+ + + + + +

{{ filteredDefinitions.length }} träffar

+
    +
  • + {{ suggestion.Name }} +
  • +
+ + + + + + + + + +
+ +
diff --git a/Aberwyn/Views/Report/BudgetReport.cshtml b/Aberwyn/Views/Report/BudgetReport.cshtml new file mode 100644 index 0000000..3a119e1 --- /dev/null +++ b/Aberwyn/Views/Report/BudgetReport.cshtml @@ -0,0 +1,61 @@ +@{ + ViewData["Title"] = "Budgetrapport"; +} + +
+

Budgetrapport

+ +
+
+ + + + + + +
+ +
+ +
+ +
+
+ + +
+ +
+

Resultat

+ + + + + + + + + + + + + + + + + + + + +
ÅrMånad{{ def.Name }}Totalt
{{ row.Year }}{{ monthName(row.Month) }} + {{ getAmount(row, def.Id) | number:0 }} + {{ getRowTotal(row) | number:0 }}
+
+
+ + + + + diff --git a/Aberwyn/wwwroot/css/budget.css b/Aberwyn/wwwroot/css/budget.css index e447c6c..abf628c 100644 --- a/Aberwyn/wwwroot/css/budget.css +++ b/Aberwyn/wwwroot/css/budget.css @@ -149,6 +149,7 @@ body { padding: 2px 8px; display: flex; flex-direction: column; + position: relative; } @@ -158,10 +159,6 @@ body { } - -.card-header { - border-radius: 12px 12px 0 0; -} .total-row { padding: 2px 8px; font-weight: bold; @@ -173,6 +170,7 @@ body { } .card-header { + border-radius: 12px 12px 0 0; position: sticky; top: 0; z-index: 3; /* högre än total-row */ @@ -186,6 +184,13 @@ body { flex-wrap: wrap; gap: 6px; } +.header-edit { + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--border-color); + flex: 1; + min-width: 120px; /* om du vill att den ska vara större */ +} .card-header.income { background-color: var(--card-income); @@ -424,3 +429,109 @@ color: var(--btn-check); pointer-events: none; font-style: italic; } + + +.add-item-popup { + position: absolute; /* ← Ändrat från fixed */ + z-index: 9999; + background-color: #1F2C3C; + color: white; + border: 1px solid #444; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + padding: 14px; + width: 280px; + font-size: 14px; + margin-top: 6px; +} + + .add-item-popup.above { + margin-top: unset; + margin-bottom: 6px; + } + + + + .add-item-popup label { + display: block; + margin-top: 8px; + font-weight: 600; + font-size: 13px; + color: #cbd5e1; + } + + .add-item-popup input, + .add-item-popup select { + width: 100%; + padding: 6px 8px; + border: none; + border-radius: 6px; + margin-top: 4px; + font-size: 13px; + box-sizing: border-box; + background-color: #334155; + color: white; + } + + .add-item-popup input::placeholder { + color: #94a3b8; + } + + .add-item-popup button { + margin-top: 12px; + margin-right: 8px; + padding: 6px 12px; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: background-color 0.2s; + } + + .add-item-popup button:hover { + background-color: #2563eb; + } +.add-post-btn { + margin: 10px 0 6px 0; + padding: 6px 10px; + background-color: #3b82f6; + color: white; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + transition: background-color 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; +} + + .add-post-btn:hover { + background-color: #2563eb; + } + +.suggestion-list { + position: absolute; + background: #1F2C3C; + color: white; + border: 1px solid #555; + border-radius: 4px; + margin-top: 4px; + padding: 0; + list-style: none; + width: 100%; + max-height: 150px; + overflow-y: auto; + z-index: 10000; /* 👈 ovanför resten */ +} + + .suggestion-list li { + padding: 6px 10px; + cursor: pointer; + } + + .suggestion-list li:hover { + background: #334155; + } diff --git a/Aberwyn/wwwroot/css/report.css b/Aberwyn/wwwroot/css/report.css new file mode 100644 index 0000000..dd39d3c --- /dev/null +++ b/Aberwyn/wwwroot/css/report.css @@ -0,0 +1,108 @@ +/* report.css */ +body { + font-family: 'Segoe UI', sans-serif; + background-color: #f9fafb; + color: #1e293b; + padding: 24px; +} + +.report-page { + max-width: 1000px; + margin: auto; +} + +h1 { + font-size: 28px; + margin-bottom: 20px; +} + +.report-controls { + background: #ffffff; + border: 1px solid #e5e7eb; + padding: 16px; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.05); + margin-bottom: 24px; +} + +.date-select label, +.definition-select label { + font-weight: 600; + margin-right: 8px; + display: inline-block; + margin-bottom: 8px; +} + +.date-select select { + margin-right: 8px; + padding: 4px 6px; + border-radius: 6px; + border: 1px solid #cbd5e1; + font-size: 14px; +} + +.definition-select { + margin-top: 16px; +} + +.checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 6px 12px; + margin-top: 6px; +} + +.btn-generate { + margin-top: 16px; + background-color: #3b82f6; + color: white; + border: none; + padding: 8px 16px; + font-size: 14px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + + .btn-generate:hover { + background-color: #2563eb; + } + +.report-results h2 { + font-size: 22px; + margin-bottom: 12px; +} + +.report-table { + width: 100%; + border-collapse: collapse; + margin-top: 16px; + font-size: 14px; + background-color: white; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); + border-radius: 6px; + overflow: hidden; +} + + .report-table th, .report-table td { + padding: 10px 8px; + border: 1px solid #e5e7eb; + text-align: right; + } + + .report-table th { + background-color: #f1f5f9; + font-weight: 600; + text-align: center; + } + + .report-table td:first-child, + .report-table td:nth-child(2) { + text-align: left; + } + +canvas#reportChart { + width: 100% !important; + max-height: 400px; + margin-top: 24px; +} diff --git a/Aberwyn/wwwroot/js/budget.js b/Aberwyn/wwwroot/js/budget.js index 8a19090..3203d86 100644 --- a/Aberwyn/wwwroot/js/budget.js +++ b/Aberwyn/wwwroot/js/budget.js @@ -102,14 +102,20 @@ app.controller('BudgetController', function ($scope, $http) { 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) + items: (cat.Items || []).map((item, index) => { + const definition = $scope.itemDefinitions.find(d => d.id === item.BudgetItemDefinitionId || d.Id === item.BudgetItemDefinitionId); + return { + id: item.Id, + name: item.Name, + amount: parseFloat(item.Amount), + isExpense: item.IsExpense === true, + includeInSummary: item.IncludeInSummary === true, + order: item.Order ?? index, + budgetItemDefinitionId: item.BudgetItemDefinitionId, + definitionName: definition?.Name || null + }; + }).sort((a, b) => a.order - b.order) + })); $scope.budget = { @@ -165,6 +171,7 @@ app.controller('BudgetController', function ($scope, $http) { id: category.id, name: category.name, color: category.color, + budgetCategoryDefinitionId: category.budgetCategoryDefinitionId || null, items: category.items.map((item, index) => ({ id: item.id, name: item.name, @@ -280,6 +287,19 @@ app.controller('BudgetController', function ($scope, $http) { return $scope.getTotalIncome() - $scope.getTotalExpense(); }; + function positionAddItemPopup(popup, triggerButton) { + const rect = popup.getBoundingClientRect(); + const bottomSpace = window.innerHeight - rect.bottom; + + // Om popupen sticker utanför skärmen, placera den ovanför + if (bottomSpace < 50) { + popup.classList.add('above'); + } else { + popup.classList.remove('above'); + } + } + + $scope.applyMonthSelection = function () { const monthIndex = $scope.monthNames.indexOf($scope.tempMonth); if (monthIndex >= 0 && $scope.tempYear) { @@ -558,6 +578,190 @@ app.controller('BudgetController', function ($scope, $http) { $scope.$applyAsync(); }; + $scope.itemDefinitions = []; - $scope.loadBudget(); + $scope.loadItemDefinitions = function () { + return $http.get("/api/budget/definitions/items") + .then(res => { + console.log("Definitioner laddade:", res.data); + $scope.itemDefinitions = res.data || []; + }); + }; + $scope.getDefinitionName = function (item) { + if (!item || !item.budgetItemDefinitionId) return null; + const def = $scope.itemDefinitions.find(d => d.id === item.budgetItemDefinitionId); + return def?.name || null; + }; + $scope.addPopupAbove = false; + $scope.openItemPopup = function ($event, category) { + const trigger = $event.currentTarget; + const rect = trigger.getBoundingClientRect(); + + $scope.addPopupData = { + category: category, + newItemType: "expense", + newItemDefinition: "", + newItemLabel: "", + newItemAmount: null + }; + + $scope.filteredDefinitions = []; + $scope.addPopupVisible = true; + + // Vänta tills popup finns i DOM + setTimeout(() => { + const popup = document.querySelector('.add-item-popup'); + if (!popup) return; + + const popupHeight = popup.offsetHeight; + const margin = 6; + const spaceBelow = window.innerHeight - rect.bottom - margin; + const spaceAbove = rect.top - margin; + + let top; + let showAbove = false; + + if (spaceBelow >= popupHeight) { + top = rect.bottom + margin; + } else if (spaceAbove >= popupHeight) { + top = rect.top - popupHeight - margin; + showAbove = true; + } else { + // Får inte plats helt – välj bästa plats och justera top + showAbove = spaceAbove > spaceBelow; + top = showAbove + ? Math.max(0, rect.top - popupHeight - margin) + : rect.bottom + margin; + } + + $scope.$apply(() => { + $scope.addPopupStyle = { + position: "fixed", + top: `${top}px`, + left: `${rect.left}px` + }; + $scope.addPopupAbove = showAbove; + }); + }, 0); + + if (!$scope.itemDefinitions || $scope.itemDefinitions.length === 0) { + $scope.loadItemDefinitions(); + } + }; + + + $scope.addPopupVisible = false; + $scope.addPopupStyle = {}; + $scope.addPopupData = {}; + + $scope.filteredDefinitions = []; + $scope.showDefinitionSuggestions = false; + + $scope.updateDefinitionSuggestions = function () { + const term = $scope.addPopupData.newItemDefinition?.toLowerCase() || ''; + console.log("Sökterm:", term); + + $scope.filteredDefinitions = $scope.itemDefinitions.filter(d => + d.Name && d.Name.toLowerCase().includes(term) + ); + + $scope.showDefinitionSuggestions = true; + }; + + + + $scope.selectDefinitionSuggestion = function (name) { + $scope.addPopupData.newItemDefinition = name; + $scope.filteredDefinitions = []; + $scope.showDefinitionSuggestions = false; + }; + + // För att inte stänga direkt vid klick + let suggestionBlurTimeout; + $scope.hideSuggestionsDelayed = function () { + suggestionBlurTimeout = setTimeout(() => { + $scope.$apply(() => { + $scope.showDefinitionSuggestions = false; + }); + }, 200); + }; + document.addEventListener('click', function (e) { + const popup = document.querySelector('.add-item-popup'); + const isInsidePopup = popup?.contains(e.target); + const isButton = e.target.closest('.add-post-btn'); + + if (!isInsidePopup && !isButton) { + $scope.$apply(() => { + $scope.addPopupVisible = false; + $scope.filteredDefinitions = []; + }); + } + }); + +$scope.addItemFromDefinition = function (cat) { + const definitionName = cat.newItemDefinition?.trim(); + const label = cat.newItemLabel?.trim(); + const amount = parseFloat(cat.newItemAmount); + + if (!definitionName || isNaN(amount)) return; + + const matched = $scope.itemDefinitions.find(d => d.name.toLowerCase() === definitionName.toLowerCase()); + + const isExpense = cat.newItemType === "expense"; + const includeInSummary = cat.newItemType !== "saving"; + + const item = { + name: label || definitionName, + amount: amount, + isExpense: isExpense, + includeInSummary: includeInSummary, + budgetCategoryId: cat.id, + budgetItemDefinitionId: matched?.id || null + + }; + + $http.post("/api/budget/item", item).then(res => { + item.id = res.data.id; + cat.items.push(item); + $scope.showToast("Post tillagd!"); + cat.addingItem = false; + + // Om det var ny definition – hämta listan på nytt + if (!matched) $scope.loadItemDefinitions(); + }); + }; + + $scope.addItemFromPopup = function () { + const cat = $scope.addPopupData.category; + const def = $scope.addPopupData.newItemDefinition?.trim(); + const label = $scope.addPopupData.newItemLabel?.trim(); + const amount = parseFloat($scope.addPopupData.newItemAmount); + const type = $scope.addPopupData.newItemType; + + if (!def || isNaN(amount)) return; + + const matched = $scope.itemDefinitions.find(d => d.Name && d.Name.toLowerCase() === def.toLowerCase()); + + const item = { + name: label || def, + amount: amount, + isExpense: type === "expense", + includeInSummary: type !== "saving", + budgetCategoryId: cat.id, + budgetItemDefinitionId: matched?.Id || null + }; + + $http.post("/api/budget/item", item).then(res => { + item.id = res.data.id; + cat.items.push(item); + $scope.showToast("Post tillagd!"); + $scope.addPopupVisible = false; + if (!matched) $scope.loadItemDefinitions(); + }); + }; + + + $scope.loadItemDefinitions().then(() => { + $scope.loadBudget(); + }); }); diff --git a/Aberwyn/wwwroot/js/report.js b/Aberwyn/wwwroot/js/report.js new file mode 100644 index 0000000..31ee85f --- /dev/null +++ b/Aberwyn/wwwroot/js/report.js @@ -0,0 +1,84 @@ +var app = angular.module('reportApp', []); + +app.controller('ReportController', function ($scope, $http) { + $scope.definitions = []; + $scope.results = []; + $scope.years = []; + $scope.months = [ + { value: 1, label: 'Januari' }, + { value: 2, label: 'Februari' }, + { value: 3, label: 'Mars' }, + { value: 4, label: 'April' }, + { value: 5, label: 'Maj' }, + { value: 6, label: 'Juni' }, + { value: 7, label: 'Juli' }, + { value: 8, label: 'Augusti' }, + { value: 9, label: 'September' }, + { value: 10, label: 'Oktober' }, + { value: 11, label: 'November' }, + { value: 12, label: 'December' } + ]; + + $scope.init = function () { + const now = new Date(); + $scope.endYear = now.getFullYear(); + $scope.endMonth = now.getMonth() + 1; + $scope.startYear = $scope.endYear - 1; + $scope.startMonth = $scope.endMonth; + + const baseYear = 2022; + const thisYear = new Date().getFullYear(); + for (let y = baseYear; y <= thisYear + 1; y++) { + $scope.years.push(y); + } + + $http.get('/api/budget/definitions/items') + .then(res => { + $scope.definitions = res.data.map(d => { + d.Selected = true; + return d; + }); + }); + }; + + $scope.monthName = function (month) { + const match = $scope.months.find(m => m.value === month); + return match ? match.label : month; + }; + + $scope.loadReport = function () { + const selectedDefs = $scope.definitions.filter(d => d.Selected); + if (selectedDefs.length === 0) { + alert("Välj minst en post att visa."); + return; + } + + const payload = { + startYear: $scope.startYear, + startMonth: $scope.startMonth, + endYear: $scope.endYear, + endMonth: $scope.endMonth, + definitionIds: selectedDefs.map(d => d.Id) + }; + + + $http.post('/api/report/report', payload) + .then(res => { + $scope.results = res.data; + $scope.activeDefinitions = selectedDefs; + }) + .catch(err => { + console.error("Fel vid hämtning av rapport:", err); + alert("Kunde inte ladda rapporten."); + }); + }; + + $scope.getAmount = function (row, defId) { + const match = row.Definitions.find(d => d.DefinitionId === defId); + return match ? match.TotalAmount : 0; + }; + + $scope.getRowTotal = function (row) { + return row.Definitions.reduce((sum, d) => sum + d.TotalAmount, 0); + }; +});