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
{
Id = period.Id,
Year = period.Year,
Month = period.Month,
Year = period.Year ?? 0,
Month = period.Month ?? 0,
Categories = period.Categories
.OrderBy(cat => cat.Order)
.Select(cat => new BudgetCategoryDto
@@ -74,6 +74,57 @@ namespace Aberwyn.Controllers
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")]
public IActionResult UpdatePaymentStatus([FromBody] PaymentStatusUpdateDto dto)
{
@@ -199,11 +250,37 @@ namespace Aberwyn.Controllers
[HttpPost]
public async Task<IActionResult> CreatePeriod([FromBody] BudgetPeriod newPeriod)
{
if (!string.IsNullOrWhiteSpace(newPeriod.Name))
{
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 CreatedAtAction(nameof(GetBudget), new { year = newPeriod.Year, month = newPeriod.Month }, newPeriod);
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}")]
public async Task<IActionResult> UpdateItem(int id, [FromBody] BudgetItem updatedItem)
{
@@ -330,19 +407,12 @@ namespace Aberwyn.Controllers
return BadRequest("Ogiltig data.");
var period = await _context.BudgetPeriods
.FirstOrDefaultAsync(p => p.Year == newCategoryDto.Year && p.Month == newCategoryDto.Month);
.FirstOrDefaultAsync(p => p.Id == newCategoryDto.BudgetPeriodId);
if (period == null)
{
period = new BudgetPeriod
{
Year = newCategoryDto.Year,
Month = newCategoryDto.Month
};
_context.BudgetPeriods.Add(period);
await _context.SaveChangesAsync();
}
return NotFound("Kunde inte hitta angiven budgetperiod.");
// 🔁 fortsätt som tidigare…
var definition = await _context.BudgetCategoryDefinitions
.FirstOrDefaultAsync(d => d.Name.ToLower() == newCategoryDto.Name.ToLower());
@@ -372,6 +442,8 @@ namespace Aberwyn.Controllers
return Ok(new { id = category.Id });
}
[HttpDelete("category/{id}")]
public async Task<IActionResult> DeleteCategory(int id)
{

View File

@@ -14,6 +14,14 @@ namespace Aberwyn.Controllers
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
[Route("budget")]
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()
.HasColumnType("int");
b.Property<int>("Month")
b.Property<int?>("Month")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("longtext");
b.Property<int>("Order")
.HasColumnType("int");
b.Property<int>("Year")
b.Property<int?>("Year")
.HasColumnType("int");
b.HasKey("Id");

View File

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

View File

@@ -5,7 +5,7 @@
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="month-nav-bar" style="display: flex; align-items: center; gap: 10px; position: relative;">
<button class="nav-button" ng-click="previousMonth()">←</button>
@@ -190,14 +190,22 @@
<div>Summa</div>
<div class="amount">{{ getCategorySum(cat) | number:0 }}</div>
</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>
<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 }">
<label>Typ:</label>
<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>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<script>
window.initialYear = @ViewBag.Year;
window.initialMonth = @ViewBag.Month;
window.initialYear = @(ViewBag.Year ?? "null");
window.initialMonth = @(ViewBag.Month ?? "null");
window.initialName = "@(ViewBag.BudgetName ?? "")";
</script>
<script src="~/js/budget.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.menuOpen = false;
$scope.chartMode = "pie";
const initialName = window.initialName;
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
$scope.getMonthName = function (month) {
@@ -15,6 +16,7 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.selectedYear = window.initialYear || new Date().getFullYear();
$scope.selectedMonth = window.initialMonth || new Date().getMonth() + 1;
$scope.tempMonth = $scope.monthNames[$scope.selectedMonth - 1];
$scope.tempYear = $scope.selectedYear;
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
@@ -140,10 +142,21 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.loading = true;
$scope.error = 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) {
const raw = response.data;
if (raw && raw.Categories) {
const categories = raw.Categories.map(cat => ({
id: cat.Id,
@@ -151,6 +164,7 @@ app.controller('BudgetController', function ($scope, $http) {
color: cat.Color,
editing: false,
allowDrag: false,
order: cat.Order ?? 0,
items: (cat.Items || []).map((item, index) => {
const definition = $scope.itemDefinitions.find(d => d.id === item.BudgetItemDefinitionId || d.Id === item.BudgetItemDefinitionId);
return {
@@ -165,23 +179,35 @@ app.controller('BudgetController', function ($scope, $http) {
paymentStatus: item.PaymentStatus
};
}).sort((a, b) => a.order - b.order)
}));
$scope.budget = {
id: raw.Id,
name: raw.Name || null,
year: raw.Year,
month: raw.Month,
categories: categories.sort((a, b) => a.order - b.order)
};
$scope.budgetNotFound = false;
if (!useName) {
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
}
} else {
$scope.budget = { categories: [] };
$scope.budgetNotFound = true;
}
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
})
.catch(function (error) {
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 {
$scope.error = "Kunde inte ladda budgetdata.";
$scope.showToast("Fel vid laddning av budgetdata", true);
@@ -194,6 +220,7 @@ app.controller('BudgetController', function ($scope, $http) {
});
};
$scope.saveCategory = function (category) {
if (category.newItemName && category.newItemAmount) {
const newItem = {
@@ -811,6 +838,28 @@ $scope.addItemFromDefinition = function (cat) {
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 () {
const ctx = document.getElementById("expenseChart");
if (!ctx || !$scope.budget?.categories) return;