This commit is contained in:
@@ -60,7 +60,8 @@ namespace Aberwyn.Controllers
|
|||||||
Name = i.Name,
|
Name = i.Name,
|
||||||
Amount = i.Amount,
|
Amount = i.Amount,
|
||||||
IsExpense = i.IsExpense,
|
IsExpense = i.IsExpense,
|
||||||
IncludeInSummary = i.IncludeInSummary
|
IncludeInSummary = i.IncludeInSummary,
|
||||||
|
BudgetItemDefinitionId = i.BudgetItemDefinitionId
|
||||||
}).ToList()
|
}).ToList()
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
@@ -83,7 +84,37 @@ namespace Aberwyn.Controllers
|
|||||||
if (category == null)
|
if (category == null)
|
||||||
return NotFound();
|
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.Name = updatedCategory.Name;
|
||||||
category.Color = updatedCategory.Color;
|
category.Color = updatedCategory.Color;
|
||||||
|
|
||||||
@@ -173,19 +204,74 @@ namespace Aberwyn.Controllers
|
|||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
return Ok();
|
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")]
|
[HttpPost("item")]
|
||||||
public async Task<IActionResult> CreateItem([FromBody] BudgetItem newItem)
|
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.");
|
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);
|
_context.BudgetItems.Add(newItem);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
return Ok(new { id = newItem.Id });
|
return Ok(new { id = newItem.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[HttpDelete("item/{id}")]
|
[HttpDelete("item/{id}")]
|
||||||
public async Task<IActionResult> DeleteItem(int id)
|
public async Task<IActionResult> DeleteItem(int id)
|
||||||
{
|
{
|
||||||
@@ -227,28 +313,41 @@ namespace Aberwyn.Controllers
|
|||||||
if (newCategoryDto == null || string.IsNullOrWhiteSpace(newCategoryDto.Name))
|
if (newCategoryDto == null || string.IsNullOrWhiteSpace(newCategoryDto.Name))
|
||||||
return BadRequest("Ogiltig data.");
|
return BadRequest("Ogiltig data.");
|
||||||
|
|
||||||
// Kontrollera att rätt period finns
|
|
||||||
var period = await _context.BudgetPeriods
|
var period = await _context.BudgetPeriods
|
||||||
.FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month);
|
.FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month);
|
||||||
|
|
||||||
if (period == null)
|
if (period == null)
|
||||||
{
|
{
|
||||||
// Skapa ny period om den inte finns
|
|
||||||
period = new BudgetPeriod
|
period = new BudgetPeriod
|
||||||
{
|
{
|
||||||
Year = newCategoryDto.Year,
|
Year = newCategoryDto.Year,
|
||||||
Month = newCategoryDto.Month
|
Month = newCategoryDto.Month
|
||||||
};
|
};
|
||||||
_context.BudgetPeriods.Add(period);
|
_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
|
var category = new BudgetCategory
|
||||||
{
|
{
|
||||||
Name = newCategoryDto.Name,
|
Name = newCategoryDto.Name,
|
||||||
Color = newCategoryDto.Color ?? "#666666",
|
Color = newCategoryDto.Color ?? definition.Color,
|
||||||
BudgetPeriodId = period.Id,
|
BudgetPeriodId = period.Id,
|
||||||
Order = newCategoryDto.Order
|
Order = newCategoryDto.Order,
|
||||||
|
BudgetCategoryDefinitionId = definition.Id
|
||||||
};
|
};
|
||||||
|
|
||||||
_context.BudgetCategories.Add(category);
|
_context.BudgetCategories.Add(category);
|
||||||
@@ -256,6 +355,7 @@ namespace Aberwyn.Controllers
|
|||||||
|
|
||||||
return Ok(new { id = category.Id });
|
return Ok(new { id = category.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("category/{id}")]
|
[HttpDelete("category/{id}")]
|
||||||
public async Task<IActionResult> DeleteCategory(int id)
|
public async Task<IActionResult> DeleteCategory(int id)
|
||||||
{
|
{
|
||||||
@@ -285,7 +385,6 @@ namespace Aberwyn.Controllers
|
|||||||
if (targetPeriod != null && targetPeriod.Categories.Any())
|
if (targetPeriod != null && targetPeriod.Categories.Any())
|
||||||
return BadRequest("Det finns redan data för denna månad.");
|
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 previous = new DateTime(year, month, 1).AddMonths(-1);
|
||||||
var previousPeriod = await _context.BudgetPeriods
|
var previousPeriod = await _context.BudgetPeriods
|
||||||
.Include(p => p.Categories)
|
.Include(p => p.Categories)
|
||||||
@@ -295,7 +394,6 @@ namespace Aberwyn.Controllers
|
|||||||
if (previousPeriod == null)
|
if (previousPeriod == null)
|
||||||
return NotFound("Ingen data att kopiera från.");
|
return NotFound("Ingen data att kopiera från.");
|
||||||
|
|
||||||
// Skapa ny period
|
|
||||||
var newPeriod = new BudgetPeriod
|
var newPeriod = new BudgetPeriod
|
||||||
{
|
{
|
||||||
Year = year,
|
Year = year,
|
||||||
@@ -305,13 +403,15 @@ namespace Aberwyn.Controllers
|
|||||||
Name = cat.Name,
|
Name = cat.Name,
|
||||||
Color = cat.Color,
|
Color = cat.Color,
|
||||||
Order = cat.Order,
|
Order = cat.Order,
|
||||||
|
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId, // 🟢 Lägg till denna rad
|
||||||
Items = cat.Items.Select(item => new BudgetItem
|
Items = cat.Items.Select(item => new BudgetItem
|
||||||
{
|
{
|
||||||
Name = item.Name,
|
Name = item.Name,
|
||||||
Amount = item.Amount,
|
Amount = item.Amount,
|
||||||
IsExpense = item.IsExpense,
|
IsExpense = item.IsExpense,
|
||||||
IncludeInSummary = item.IncludeInSummary,
|
IncludeInSummary = item.IncludeInSummary,
|
||||||
Order = item.Order
|
Order = item.Order,
|
||||||
|
BudgetItemDefinitionId = item.BudgetItemDefinitionId // 🟢 Lägg till denna rad
|
||||||
}).ToList()
|
}).ToList()
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
@@ -323,5 +423,6 @@ namespace Aberwyn.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
Aberwyn/Controllers/ReportApiController.cs
Normal file
64
Aberwyn/Controllers/ReportApiController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Aberwyn/Controllers/ReportController.cs
Normal file
14
Aberwyn/Controllers/ReportController.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ namespace Aberwyn.Data
|
|||||||
public DbSet<PushSubscriber> PushSubscribers { get; set; }
|
public DbSet<PushSubscriber> PushSubscribers { get; set; }
|
||||||
public DbSet<PizzaOrder> PizzaOrders { get; set; }
|
public DbSet<PizzaOrder> PizzaOrders { get; set; }
|
||||||
public DbSet<AppSetting> AppSettings { get; set; }
|
public DbSet<AppSetting> AppSettings { get; set; }
|
||||||
|
public DbSet<BudgetItemDefinition> BudgetItemDefinitions { get; set; }
|
||||||
|
public DbSet<BudgetCategoryDefinition> BudgetCategoryDefinitions { get; set; }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "BudgetItemDefinition")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("BudgetCategoryDefinitionId");
|
.HasForeignKey("BudgetCategoryDefinitionId");
|
||||||
|
|
||||||
@@ -430,7 +430,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
b.Navigation("BudgetPeriod");
|
b.Navigation("BudgetPeriod");
|
||||||
|
|
||||||
b.Navigation("Definition");
|
b.Navigation("BudgetItemDefinition");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
||||||
@@ -441,13 +441,13 @@ namespace Aberwyn.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("BudgetItemDefinitionId");
|
.HasForeignKey("BudgetItemDefinitionId");
|
||||||
|
|
||||||
b.Navigation("BudgetCategory");
|
b.Navigation("BudgetCategory");
|
||||||
|
|
||||||
b.Navigation("Definition");
|
b.Navigation("BudgetItemDefinition");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
|||||||
516
Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.Designer.cs
generated
Normal file
516
Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.cs
Normal file
111
Aberwyn/Migrations/20250526133558_AddBudgetItemDefinitions.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -150,7 +150,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("BudgetCategoryDefinition");
|
b.ToTable("BudgetCategoryDefinitions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
||||||
@@ -211,7 +211,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("BudgetItemDefinition");
|
b.ToTable("BudgetItemDefinitions");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetPeriod", b =>
|
||||||
@@ -416,7 +416,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetCategory", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetCategoryDefinition", "BudgetItemDefinition")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("BudgetCategoryDefinitionId");
|
.HasForeignKey("BudgetCategoryDefinitionId");
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ namespace Aberwyn.Migrations
|
|||||||
|
|
||||||
b.Navigation("BudgetPeriod");
|
b.Navigation("BudgetPeriod");
|
||||||
|
|
||||||
b.Navigation("Definition");
|
b.Navigation("BudgetItemDefinition");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
modelBuilder.Entity("Aberwyn.Models.BudgetItem", b =>
|
||||||
@@ -439,13 +439,13 @@ namespace Aberwyn.Migrations
|
|||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "Definition")
|
b.HasOne("Aberwyn.Models.BudgetItemDefinition", "BudgetItemDefinition")
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("BudgetItemDefinitionId");
|
.HasForeignKey("BudgetItemDefinitionId");
|
||||||
|
|
||||||
b.Navigation("BudgetCategory");
|
b.Navigation("BudgetCategory");
|
||||||
|
|
||||||
b.Navigation("Definition");
|
b.Navigation("BudgetItemDefinition");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace Aberwyn.Models
|
|||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[ValidateNever]
|
[ValidateNever]
|
||||||
public BudgetItemDefinition? Definition { get; set; }
|
public BudgetItemDefinition? BudgetItemDefinition { get; set; }
|
||||||
|
|
||||||
public int BudgetCategoryId { get; set; }
|
public int BudgetCategoryId { get; set; }
|
||||||
|
|
||||||
@@ -67,6 +67,8 @@ namespace Aberwyn.Models
|
|||||||
public string Color { get; set; }
|
public string Color { get; set; }
|
||||||
public List<BudgetItemDto> Items { get; set; } = new();
|
public List<BudgetItemDto> Items { get; set; } = new();
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
|
public int? BudgetCategoryDefinitionId { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public int Year { get; set; }
|
public int Year { get; set; }
|
||||||
public int Month { get; set; }
|
public int Month { get; set; }
|
||||||
@@ -78,6 +80,7 @@ namespace Aberwyn.Models
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
|
public int? BudgetItemDefinitionId { get; set; }
|
||||||
|
|
||||||
public bool IsExpense { get; set; }
|
public bool IsExpense { get; set; }
|
||||||
public bool IncludeInSummary { get; set; }
|
public bool IncludeInSummary { get; set; }
|
||||||
|
|||||||
25
Aberwyn/Models/ReportModel.cs
Normal file
25
Aberwyn/Models/ReportModel.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
@using Microsoft.AspNetCore.Authorization
|
@attribute [Authorize(Roles = "Budget")]
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Budget";
|
ViewData["Title"] = "Budget";
|
||||||
@attribute [Authorize(Roles = "Budget")]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<div ng-app="budgetApp" ng-controller="BudgetController" class="budget-page" ng-init="loadBudget()">
|
<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="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;">
|
<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>
|
style="opacity: 0.5; padding-right: 6px; cursor: grab;"></i>
|
||||||
|
|
||||||
<input type="text" ng-model="item.name" ng-if="cat.editing" />
|
<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" />
|
<input type="number" ng-model="item.amount" ng-if="cat.editing" />
|
||||||
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
|
<span class="amount" ng-if="!cat.editing">{{ item.amount | number:0 }}</span>
|
||||||
|
|
||||||
@@ -96,6 +98,9 @@
|
|||||||
<i class="fa fa-ellipsis-v"></i>
|
<i class="fa fa-ellipsis-v"></i>
|
||||||
</button>
|
</button>
|
||||||
<div class="item-floating-menu" ng-show="menuVisible" ng-style="menuStyle">
|
<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>
|
<button ng-click="deleteItem(menuItem.category, menuItem)">🗑 Ta bort</button>
|
||||||
<hr>
|
<hr>
|
||||||
<button
|
<button
|
||||||
@@ -124,11 +129,12 @@
|
|||||||
on-drop-item="handleItemPreciseDrop(data, targetCategory, targetIndex)">
|
on-drop-item="handleItemPreciseDrop(data, targetCategory, targetIndex)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-row add-row" ng-if="cat.editing">
|
<button class="icon-button add-post-btn"
|
||||||
<i class="fa fa-plus" style="padding-right: 6px; opacity: 0.7;"></i>
|
ng-if="cat.editing && !cat.addingItem"
|
||||||
<input type="text" ng-model="cat.newItemName" placeholder="Ny post" />
|
ng-click="openItemPopup($event, cat)">
|
||||||
<input type="number" ng-model="cat.newItemAmount" placeholder="Belopp" />
|
➕ Lägg till post
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-row total-row">
|
<div class="item-row total-row">
|
||||||
@@ -143,6 +149,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</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" />
|
<link rel="stylesheet" href="~/css/budget.css" />
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
|
<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" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
||||||
|
|||||||
61
Aberwyn/Views/Report/BudgetReport.cshtml
Normal file
61
Aberwyn/Views/Report/BudgetReport.cshtml
Normal 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>
|
||||||
@@ -149,6 +149,7 @@ body {
|
|||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -158,10 +159,6 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
}
|
|
||||||
.total-row {
|
.total-row {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -173,6 +170,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 3; /* högre än total-row */
|
z-index: 3; /* högre än total-row */
|
||||||
@@ -186,6 +184,13 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
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 {
|
.card-header.income {
|
||||||
background-color: var(--card-income);
|
background-color: var(--card-income);
|
||||||
@@ -424,3 +429,109 @@ color: var(--btn-check);
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
font-style: italic;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
108
Aberwyn/wwwroot/css/report.css
Normal file
108
Aberwyn/wwwroot/css/report.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -102,14 +102,20 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
color: cat.Color,
|
color: cat.Color,
|
||||||
editing: false,
|
editing: false,
|
||||||
allowDrag: false,
|
allowDrag: false,
|
||||||
items: (cat.Items || []).map((item, index) => ({
|
items: (cat.Items || []).map((item, index) => {
|
||||||
id: item.Id,
|
const definition = $scope.itemDefinitions.find(d => d.id === item.BudgetItemDefinitionId || d.Id === item.BudgetItemDefinitionId);
|
||||||
name: item.Name,
|
return {
|
||||||
amount: parseFloat(item.Amount),
|
id: item.Id,
|
||||||
isExpense: item.IsExpense === true,
|
name: item.Name,
|
||||||
includeInSummary: item.IncludeInSummary === true,
|
amount: parseFloat(item.Amount),
|
||||||
order: item.Order ?? index
|
isExpense: item.IsExpense === true,
|
||||||
})).sort((a, b) => a.order - b.order)
|
includeInSummary: item.IncludeInSummary === true,
|
||||||
|
order: item.Order ?? index,
|
||||||
|
budgetItemDefinitionId: item.BudgetItemDefinitionId,
|
||||||
|
definitionName: definition?.Name || null
|
||||||
|
};
|
||||||
|
}).sort((a, b) => a.order - b.order)
|
||||||
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$scope.budget = {
|
$scope.budget = {
|
||||||
@@ -165,6 +171,7 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
id: category.id,
|
id: category.id,
|
||||||
name: category.name,
|
name: category.name,
|
||||||
color: category.color,
|
color: category.color,
|
||||||
|
budgetCategoryDefinitionId: category.budgetCategoryDefinitionId || null,
|
||||||
items: category.items.map((item, index) => ({
|
items: category.items.map((item, index) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -280,6 +287,19 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
return $scope.getTotalIncome() - $scope.getTotalExpense();
|
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 () {
|
$scope.applyMonthSelection = function () {
|
||||||
const monthIndex = $scope.monthNames.indexOf($scope.tempMonth);
|
const monthIndex = $scope.monthNames.indexOf($scope.tempMonth);
|
||||||
if (monthIndex >= 0 && $scope.tempYear) {
|
if (monthIndex >= 0 && $scope.tempYear) {
|
||||||
@@ -558,6 +578,190 @@ app.controller('BudgetController', function ($scope, $http) {
|
|||||||
$scope.$applyAsync();
|
$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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
84
Aberwyn/wwwroot/js/report.js
Normal file
84
Aberwyn/wwwroot/js/report.js
Normal 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);
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user