Budget complete!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-05-28 10:29:56 +02:00
parent 0f0eaad7b1
commit eed1ce166f
16 changed files with 1490 additions and 44 deletions

View File

@@ -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<IActionResult> GetItemDefinitions()
{
var definitions = await _context.BudgetItemDefinitions
.OrderBy(d => d.Name)
.ToListAsync();
return Ok(definitions);
}
[HttpGet("definitions/categories")]
public async Task<IActionResult> GetCategoryDefinitions()
{
var definitions = await _context.BudgetCategoryDefinitions
.OrderBy(d => d.Name)
.ToListAsync();
return Ok(definitions);
}
[HttpPost("item")]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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
}
}
}

View File

@@ -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<IActionResult> 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);
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -17,6 +17,9 @@ namespace Aberwyn.Data
public DbSet<PushSubscriber> PushSubscribers { get; set; }
public DbSet<PizzaOrder> PizzaOrders { get; set; }
public DbSet<AppSetting> AppSettings { get; set; }
public DbSet<BudgetItemDefinition> BudgetItemDefinitions { get; set; }
public DbSet<BudgetCategoryDefinition> BudgetCategoryDefinitions { get; set; }
}
}

View File

@@ -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<string>", b =>

View File

@@ -0,0 +1,516 @@
// <auto-generated />
using System;
using Aberwyn.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Aberwyn.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("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<string>("Id")
.HasColumnType("varchar(255)");
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("tinyint(1)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("tinyint(1)");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetime(6)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("longtext");
b.Property<string>("PhoneNumber")
.HasColumnType("longtext");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("tinyint(1)");
b.Property<string>("SecurityStamp")
.HasColumnType("longtext");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("tinyint(1)");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex");
b.ToTable("AspNetUsers", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.AppSetting", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("AppSettings");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int?>("BudgetCategoryDefinitionId")
.HasColumnType("int");
b.Property<int>("BudgetPeriodId")
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryDefinitionId");
b.HasIndex("BudgetPeriodId");
b.ToTable("BudgetCategories");
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategoryDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetCategoryDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<decimal>("Amount")
.HasColumnType("decimal(65,30)");
b.Property<int>("BudgetCategoryId")
.HasColumnType("int");
b.Property<int?>("BudgetItemDefinitionId")
.HasColumnType("int");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BudgetCategoryId");
b.HasIndex("BudgetItemDefinitionId");
b.ToTable("BudgetItems");
});
modelBuilder.Entity("Aberwyn.Models.BudgetItemDefinition", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DefaultCategory")
.HasColumnType("longtext");
b.Property<bool>("IncludeInSummary")
.HasColumnType("tinyint(1)");
b.Property<bool>("IsExpense")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("BudgetItemDefinitions");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("Month")
.HasColumnType("int");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("BudgetPeriods");
});
modelBuilder.Entity("Aberwyn.Models.PizzaOrder", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("CustomerName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("IngredientsJson")
.HasColumnType("longtext");
b.Property<DateTime>("OrderedAt")
.HasColumnType("datetime(6)");
b.Property<string>("PizzaName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PizzaOrders");
});
modelBuilder.Entity("Aberwyn.Models.PushSubscriber", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Auth")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Endpoint")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("P256DH")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PushSubscribers");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.HasColumnType("varchar(255)");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("longtext");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("varchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex");
b.ToTable("AspNetRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("ClaimType")
.HasColumnType("longtext");
b.Property<string>("ClaimValue")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderKey")
.HasColumnType("varchar(255)");
b.Property<string>("ProviderDisplayName")
.HasColumnType("longtext");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("varchar(255)");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("RoleId")
.HasColumnType("varchar(255)");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("varchar(255)");
b.Property<string>("LoginProvider")
.HasColumnType("varchar(255)");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<string>("Value")
.HasColumnType("longtext");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "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<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Aberwyn.Models.ApplicationUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
{
b.Navigation("Items");
});
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
{
b.Navigation("Categories");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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<string>", b =>

View File

@@ -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<BudgetItemDto> 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; }

View File

@@ -0,0 +1,25 @@
namespace Aberwyn.Models
{
public class BudgetReportRequestDto
{
public List<int> 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<DefinitionSumDto> Definitions { get; set; } = new();
}
public class DefinitionSumDto
{
public int DefinitionId { get; set; }
public string DefinitionName { get; set; }
public decimal TotalAmount { get; set; }
}
}

View File

@@ -1,8 +1,10 @@
@using Microsoft.AspNetCore.Authorization
@attribute [Authorize(Roles = "Budget")]
@using Microsoft.AspNetCore.Authorization
@{
ViewData["Title"] = "Budget";
@attribute [Authorize(Roles = "Budget")]
}
<div ng-app="budgetApp" ng-controller="BudgetController" class="budget-page" ng-init="loadBudget()">
<div class="budget-header" style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
<div class="month-nav-bar" style="display: flex; align-items: center; gap: 10px; position: relative;">
@@ -85,8 +87,8 @@
style="opacity: 0.5; padding-right: 6px; cursor: grab;"></i>
<input type="text" ng-model="item.name" ng-if="cat.editing" />
<span ng-if="!cat.editing">{{ item.name }}</span>
<span ng-if="!cat.editing" title="{{ item.definitionName }}">{{ item.name }}</span>
<!-- <span ng-if="!cat.editing">#{{ item.definitionName }}</span>-->
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
@@ -96,6 +98,9 @@
<i class="fa fa-ellipsis-v"></i>
</button>
<div class="item-floating-menu" ng-show="menuVisible" ng-style="menuStyle">
<span style="font-size: 11px; color: #94a3b8; display: block; margin: 0px 0px 0px 40px;">
#{{ menuItem.definitionName }}
</span>
<button ng-click="deleteItem(menuItem.category, menuItem)">🗑 Ta bort</button>
<hr>
<button
@@ -124,11 +129,12 @@
on-drop-item="handleItemPreciseDrop(data, targetCategory, targetIndex)">
</div>
<div class="item-row add-row" ng-if="cat.editing">
<i class="fa fa-plus" style="padding-right: 6px; opacity: 0.7;"></i>
<input type="text" ng-model="cat.newItemName" placeholder="Ny post" />
<input type="number" ng-model="cat.newItemAmount" placeholder="Belopp" />
</div>
<button class="icon-button add-post-btn"
ng-if="cat.editing && !cat.addingItem"
ng-click="openItemPopup($event, cat)">
Lägg till post
</button>
</div>
<div class="item-row total-row">
@@ -143,6 +149,41 @@
</div>
</div>
<div class="add-item-popup" ng-show="addPopupVisible" ng-style="addPopupStyle" ng-class="{ 'above': addPopupAbove }">
<label>Typ:</label>
<select ng-model="addPopupData.newItemType">
<option value="expense">💸 Utgift</option>
<option value="income">💰 Inkomst</option>
<option value="saving">🏦 Sparande</option>
</select>
<label>Definition:</label>
<input type="text"
ng-model="addPopupData.newItemDefinition"
ng-change="updateDefinitionSuggestions()"
placeholder="Ex: Elhandel"
autocomplete="on"
ng-blur="hideSuggestionsDelayed()"
ng-focus="showDefinitionSuggestions = true" />
<p style="color: red">{{ filteredDefinitions.length }} träffar</p>
<ul class="suggestion-list" ng-show="showDefinitionSuggestions && filteredDefinitions.length > 0">
<li ng-repeat="suggestion in filteredDefinitions"
ng-mousedown="selectDefinitionSuggestion(suggestion.Name)">
{{ suggestion.Name }}
</li>
</ul>
<label>Etikett (valfritt):</label>
<input type="text" ng-model="addPopupData.newItemLabel" placeholder="Ex: Däck till Volvon" />
<label>Belopp:</label>
<input type="number" ng-model="addPopupData.newItemAmount" placeholder="0" />
<button ng-click="addItemFromPopup()">Lägg till</button>
<button ng-click="addPopupVisible = false">Avbryt</button>
</div>
</div>
<link rel="stylesheet" href="~/css/budget.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />

View File

@@ -0,0 +1,61 @@
@{
ViewData["Title"] = "Budgetrapport";
}
<div ng-app="reportApp" ng-controller="ReportController" class="report-page" ng-init="init()">
<h1>Budgetrapport</h1>
<div class="report-controls">
<div class="date-select">
<label>Från:</label>
<select ng-model="startYear" ng-options="y for y in years"></select>
<select ng-model="startMonth" ng-options="m.value as m.label for m in months"></select>
<label>till:</label>
<select ng-model="endYear" ng-options="y for y in years"></select>
<select ng-model="endMonth" ng-options="m.value as m.label for m in months"></select>
</div>
<div class="definition-select">
<label>Välj poster att inkludera:</label>
<div class="checkbox-grid">
<label ng-repeat="def in definitions">
<input type="checkbox" ng-model="def.Selected" /> {{ def.Name }}
</label>
</div>
</div>
<button class="btn-generate" ng-click="loadReport()">Generera rapport</button>
</div>
<div class="report-results" ng-if="results.length > 0">
<h2>Resultat</h2>
<canvas id="reportChart" width="100%" height="50"></canvas>
<table class="report-table">
<thead>
<tr>
<th>År</th>
<th>Månad</th>
<th ng-repeat="def in activeDefinitions">{{ def.Name }}</th>
<th>Totalt</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in results">
<td>{{ row.Year }}</td>
<td>{{ monthName(row.Month) }}</td>
<td ng-repeat="def in activeDefinitions">
{{ getAmount(row, def.Id) | number:0 }}
</td>
<td>{{ getRowTotal(row) | number:0 }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<link rel="stylesheet" href="~/css/report.css" />
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="~/js/report.js"></script>

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -102,14 +102,20 @@ app.controller('BudgetController', function ($scope, $http) {
color: cat.Color,
editing: false,
allowDrag: false,
items: (cat.Items || []).map((item, index) => ({
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
})).sort((a, b) => a.order - b.order)
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.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();
});
});

View File

@@ -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);
};
});