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

This commit is contained in:
Elias Jansson
2025-07-21 15:16:31 +02:00
parent 95811ce3f8
commit cc96802637
12 changed files with 3462 additions and 35 deletions

View File

@@ -43,8 +43,8 @@ namespace Aberwyn.Controllers
var dto = new BudgetDto var dto = new BudgetDto
{ {
Id = period.Id, Id = period.Id,
Year = period.Year, Year = period.Year ?? 0,
Month = period.Month, Month = period.Month ?? 0,
Categories = period.Categories Categories = period.Categories
.OrderBy(cat => cat.Order) .OrderBy(cat => cat.Order)
.Select(cat => new BudgetCategoryDto .Select(cat => new BudgetCategoryDto
@@ -74,6 +74,57 @@ namespace Aberwyn.Controllers
return StatusCode(500, $"Fel: {ex.Message} \n{ex.StackTrace}"); return StatusCode(500, $"Fel: {ex.Message} \n{ex.StackTrace}");
} }
} }
[HttpGet("byname/{name}")]
public async Task<IActionResult> GetBudgetByName(string name)
{
var period = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == name.ToLower());
if (period == null)
{
return Ok(new BudgetDto
{
Name = name,
Categories = new List<BudgetCategoryDto>()
});
}
var dto = new BudgetDto
{
Id = period.Id,
Name = period.Name,
Year = period.Year ?? 0,
Month = period.Month ?? 0,
Categories = period.Categories
.OrderBy(cat => cat.Order)
.Select(cat => new BudgetCategoryDto
{
Id = cat.Id,
Name = cat.Name,
Color = cat.Color,
Items = cat.Items
.OrderBy(i => i.Order)
.Select(i => new BudgetItemDto
{
Id = i.Id,
Name = i.Name,
Amount = i.Amount,
IsExpense = i.IsExpense,
IncludeInSummary = i.IncludeInSummary,
BudgetItemDefinitionId = i.BudgetItemDefinitionId,
PaymentStatus = i.PaymentStatus
}).ToList()
}).ToList()
};
return Ok(dto);
}
[HttpPut("updatePaymentStatus")] [HttpPut("updatePaymentStatus")]
public IActionResult UpdatePaymentStatus([FromBody] PaymentStatusUpdateDto dto) public IActionResult UpdatePaymentStatus([FromBody] PaymentStatusUpdateDto dto)
{ {
@@ -199,11 +250,37 @@ namespace Aberwyn.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> CreatePeriod([FromBody] BudgetPeriod newPeriod) public async Task<IActionResult> CreatePeriod([FromBody] BudgetPeriod newPeriod)
{ {
_context.BudgetPeriods.Add(newPeriod); if (!string.IsNullOrWhiteSpace(newPeriod.Name))
await _context.SaveChangesAsync(); {
return CreatedAtAction(nameof(GetBudget), new { year = newPeriod.Year, month = newPeriod.Month }, newPeriod); var existingNamed = await _context.BudgetPeriods
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == newPeriod.Name.ToLower());
if (existingNamed != null)
return Conflict("En budget med detta namn finns redan.");
_context.BudgetPeriods.Add(newPeriod);
await _context.SaveChangesAsync();
return Ok(new { id = newPeriod.Id });
}
if (newPeriod.Year.HasValue && newPeriod.Month.HasValue)
{
var existing = await _context.BudgetPeriods
.FirstOrDefaultAsync(p => p.Year == newPeriod.Year && p.Month == newPeriod.Month);
if (existing != null)
return Conflict("En budget för denna månad finns redan.");
_context.BudgetPeriods.Add(newPeriod);
await _context.SaveChangesAsync();
return Ok(new { id = newPeriod.Id });
}
return BadRequest("Varken namn eller år/månad angivet.");
} }
[HttpPut("item/{id}")] [HttpPut("item/{id}")]
public async Task<IActionResult> UpdateItem(int id, [FromBody] BudgetItem updatedItem) public async Task<IActionResult> UpdateItem(int id, [FromBody] BudgetItem updatedItem)
{ {
@@ -330,19 +407,12 @@ namespace Aberwyn.Controllers
return BadRequest("Ogiltig data."); return BadRequest("Ogiltig data.");
var period = await _context.BudgetPeriods var period = await _context.BudgetPeriods
.FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month); .FirstOrDefaultAsync(p => p.Id == newCategoryDto.BudgetPeriodId);
if (period == null) if (period == null)
{ return NotFound("Kunde inte hitta angiven budgetperiod.");
period = new BudgetPeriod
{
Year = newCategoryDto.Year,
Month = newCategoryDto.Month
};
_context.BudgetPeriods.Add(period);
await _context.SaveChangesAsync();
}
// 🔁 fortsätt som tidigare…
var definition = await _context.BudgetCategoryDefinitions var definition = await _context.BudgetCategoryDefinitions
.FirstOrDefaultAsync(d => d.Name.ToLower() == newCategoryDto.Name.ToLower()); .FirstOrDefaultAsync(d => d.Name.ToLower() == newCategoryDto.Name.ToLower());
@@ -372,6 +442,8 @@ 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)
{ {

View File

@@ -14,6 +14,14 @@ namespace Aberwyn.Controllers
return View(); return View();
} }
[Route("budget/{name}")]
public IActionResult Index(string name)
{
ViewBag.BudgetName = name;
return View();
}
// För fallback när ingen månad/år anges // För fallback när ingen månad/år anges
[Route("budget")] [Route("budget")]
public IActionResult Index() public IActionResult Index()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddNameToBudgetPeriod : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class AddNameToBudgetPeriod2 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Name",
table: "BudgetPeriods",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Name",
table: "BudgetPeriods");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Aberwyn.Migrations
{
public partial class MakeYearMonthNullable : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Year",
table: "BudgetPeriods",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.AlterColumn<int>(
name: "Month",
table: "BudgetPeriods",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<int>(
name: "Year",
table: "BudgetPeriods",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Month",
table: "BudgetPeriods",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
}
}
}

View File

@@ -226,13 +226,16 @@ namespace Aberwyn.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Month") b.Property<int?>("Month")
.HasColumnType("int"); .HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<int>("Order") b.Property<int>("Order")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Year") b.Property<int?>("Year")
.HasColumnType("int"); .HasColumnType("int");
b.HasKey("Id"); b.HasKey("Id");

View File

@@ -7,9 +7,11 @@ namespace Aberwyn.Models
public class BudgetPeriod public class BudgetPeriod
{ {
public int Id { get; set; } public int Id { get; set; }
public int Year { get; set; } public int? Year { get; set; }
public int Month { get; set; } public int? Month { get; set; }
public int Order { get; set; } public int Order { get; set; }
public string? Name { get; set; }
public List<BudgetCategory> Categories { get; set; } = new(); public List<BudgetCategory> Categories { get; set; } = new();
} }
@@ -62,8 +64,9 @@ namespace Aberwyn.Models
public class BudgetDto public class BudgetDto
{ {
public int Id { get; set; } public int Id { get; set; }
public int Year { get; set; } public string? Name { get; set; }
public int Month { get; set; } public int? Year { get; set; }
public int? Month { get; set; }
public int Order { get; set; } public int Order { get; set; }
public List<BudgetCategoryDto> Categories { get; set; } = new(); public List<BudgetCategoryDto> Categories { get; set; } = new();
@@ -78,7 +81,7 @@ namespace Aberwyn.Models
public int Order { get; set; } public int Order { get; set; }
public int? BudgetCategoryDefinitionId { get; set; } public int? BudgetCategoryDefinitionId { get; set; }
public int? BudgetPeriodId { get; set; }
public int Year { get; set; } public int Year { get; set; }
public int Month { get; set; } public int Month { get; set; }
} }

View File

@@ -5,7 +5,7 @@
ViewData["Title"] = "Budget"; ViewData["Title"] = "Budget";
} }
<div ng-app="budgetApp" ng-controller="BudgetController" class="budget-page" ng-if="!loading"> <div ng-app="budgetApp" ng-controller="BudgetController">
<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;">
<button class="nav-button" ng-click="previousMonth()">←</button> <button class="nav-button" ng-click="previousMonth()">←</button>
@@ -190,14 +190,22 @@
<div>Summa</div> <div>Summa</div>
<div class="amount">{{ getCategorySum(cat) | number:0 }}</div> <div class="amount">{{ getCategorySum(cat) | number:0 }}</div>
</div> </div>
<div class="no-data" ng-if="!loading && budget && budget.categories.length === 0">
Det finns ingen budgetdata för vald månad ({{ selectedMonth }}/{{ selectedYear }}).
<button ng-click="copyPreviousMonthSafe()">[Test] Kopiera föregående</button>
</div>
</div> </div>
</div> </div>
<div class="no-data" ng-if="!loading && budget && budget.categories.length === 0">
<p>Det finns ingen budgetdata för
<strong>{{ budget.name || (selectedMonth + '/' + selectedYear) }}</strong>.
</p>
<div style="margin-top: 10px;">
<button ng-click="createEmptyBudget()" style="margin-right: 10px;">
Skapa ny budget
</button>
<button ng-click="copyPreviousMonthSafe()">
Kopiera föregående månad
</button>
</div>
</div>
<div class="add-item-popup" ng-show="addPopupVisible" ng-style="addPopupStyle" ng-class="{ 'above': addPopupAbove }"> <div class="add-item-popup" ng-show="addPopupVisible" ng-style="addPopupStyle" ng-class="{ 'above': addPopupAbove }">
<label>Typ:</label> <label>Typ:</label>
<select ng-model="addPopupData.newItemType"> <select ng-model="addPopupData.newItemType">
@@ -262,8 +270,10 @@
<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" />
<script> <script>
window.initialYear = @ViewBag.Year; window.initialYear = @(ViewBag.Year ?? "null");
window.initialMonth = @ViewBag.Month; window.initialMonth = @(ViewBag.Month ?? "null");
window.initialName = "@(ViewBag.BudgetName ?? "")";
</script> </script>
<script src="~/js/budget.js"></script> <script src="~/js/budget.js"></script>
<script src="~/js/budget-dragdrop.js"></script> <script src="~/js/budget-dragdrop.js"></script>

View File

@@ -6,6 +6,7 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.error = null; $scope.error = null;
$scope.menuOpen = false; $scope.menuOpen = false;
$scope.chartMode = "pie"; $scope.chartMode = "pie";
const initialName = window.initialName;
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"]; $scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
$scope.getMonthName = function (month) { $scope.getMonthName = function (month) {
@@ -15,6 +16,7 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.selectedYear = window.initialYear || new Date().getFullYear(); $scope.selectedYear = window.initialYear || new Date().getFullYear();
$scope.selectedMonth = window.initialMonth || new Date().getMonth() + 1; $scope.selectedMonth = window.initialMonth || new Date().getMonth() + 1;
$scope.tempMonth = $scope.monthNames[$scope.selectedMonth - 1]; $scope.tempMonth = $scope.monthNames[$scope.selectedMonth - 1];
$scope.tempYear = $scope.selectedYear; $scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth); $scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
@@ -140,10 +142,21 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.loading = true; $scope.loading = true;
$scope.error = null; $scope.error = null;
$scope.budget = null; $scope.budget = null;
$scope.budgetNotFound = false;
$http.get(`/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`) const useName = typeof initialName === 'string' && initialName !== "null" && initialName !== "";
let url = "";
if (useName) {
url = `/api/budget/byname/${initialName}`;
} else {
url = `/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`;
}
$http.get(url)
.then(function (response) { .then(function (response) {
const raw = response.data; const raw = response.data;
if (raw && raw.Categories) { if (raw && raw.Categories) {
const categories = raw.Categories.map(cat => ({ const categories = raw.Categories.map(cat => ({
id: cat.Id, id: cat.Id,
@@ -151,6 +164,7 @@ app.controller('BudgetController', function ($scope, $http) {
color: cat.Color, color: cat.Color,
editing: false, editing: false,
allowDrag: false, allowDrag: false,
order: cat.Order ?? 0,
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); const definition = $scope.itemDefinitions.find(d => d.id === item.BudgetItemDefinitionId || d.Id === item.BudgetItemDefinitionId);
return { return {
@@ -165,23 +179,35 @@ app.controller('BudgetController', function ($scope, $http) {
paymentStatus: item.PaymentStatus paymentStatus: item.PaymentStatus
}; };
}).sort((a, b) => a.order - b.order) }).sort((a, b) => a.order - b.order)
})); }));
$scope.budget = { $scope.budget = {
id: raw.Id, id: raw.Id,
name: raw.Name || null,
year: raw.Year, year: raw.Year,
month: raw.Month, month: raw.Month,
categories: categories.sort((a, b) => a.order - b.order) categories: categories.sort((a, b) => a.order - b.order)
}; };
$scope.budgetNotFound = false;
if (!useName) {
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
}
} else { } else {
$scope.budget = { categories: [] }; $scope.budget = { categories: [] };
$scope.budgetNotFound = true;
} }
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
}) })
.catch(function (error) { .catch(function (error) {
if (error.status === 404) { if (error.status === 404) {
$scope.budget = { categories: [] }; $scope.budget = {
name: useName ? initialName : null,
year: useName ? null : $scope.selectedYear,
month: useName ? null : $scope.selectedMonth,
categories: []
};
$scope.budgetNotFound = true;
} else { } else {
$scope.error = "Kunde inte ladda budgetdata."; $scope.error = "Kunde inte ladda budgetdata.";
$scope.showToast("Fel vid laddning av budgetdata", true); $scope.showToast("Fel vid laddning av budgetdata", true);
@@ -194,6 +220,7 @@ app.controller('BudgetController', function ($scope, $http) {
}); });
}; };
$scope.saveCategory = function (category) { $scope.saveCategory = function (category) {
if (category.newItemName && category.newItemAmount) { if (category.newItemName && category.newItemAmount) {
const newItem = { const newItem = {
@@ -811,6 +838,28 @@ $scope.addItemFromDefinition = function (cat) {
return cat.items.some(i => i.isExpense); return cat.items.some(i => i.isExpense);
}; };
$scope.createEmptyBudget = function () {
if (!$scope.budget || !$scope.budget.name) {
$scope.showToast("Ogiltigt budgetnamn.");
return;
}
const dto = {
name: $scope.budget.name
};
$http.post('/api/budget', dto)
.then(() => {
$scope.showToast("Ny budget skapad.");
$scope.loadBudget(); // ladda om efter skapandet
})
.catch(error => {
console.error("Fel vid skapande:", error);
$scope.showToast("Kunde inte skapa budget.");
});
};
$scope.drawCategoryChart = function () { $scope.drawCategoryChart = function () {
const ctx = document.getElementById("expenseChart"); const ctx = document.getElementById("expenseChart");
if (!ctx || !$scope.budget?.categories) return; if (!ctx || !$scope.budget?.categories) return;