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

This commit is contained in:
Elias Jansson
2025-09-21 14:40:00 +02:00
parent 8aed8d16b6
commit 5a17df917d
11 changed files with 759 additions and 85 deletions

View File

@@ -707,7 +707,205 @@ string targetName,
return sourcePeriod;
}
[HttpGet("metadata")]
public async Task<IActionResult> GetMetadata([FromQuery] int? year, [FromQuery] int? month)
{
var categoriesQuery = _context.BudgetCategories
.Include(c => c.BudgetPeriod)
.Include(c => c.Items)
.ThenInclude(i => i.BudgetItemDefinition)
.AsQueryable();
if (year.HasValue)
categoriesQuery = categoriesQuery.Where(c => c.BudgetPeriod.Year == year.Value);
if (month.HasValue)
categoriesQuery = categoriesQuery.Where(c => c.BudgetPeriod.Month == month.Value);
var categories = await categoriesQuery.ToListAsync();
var categoryDefs = categories
.Where(c => c.BudgetCategoryDefinitionId.HasValue)
.GroupBy(c => c.Definition?.Name ?? c.Name)
.Select(g => new {
Name = g.Key,
Color = g.First().Color ?? "#cccccc" // direkt från kategorin
})
.ToList();
var categoryLabels = categories
.GroupBy(c => c.Name)
.Select(g => new {
Name = g.Key,
Color = g.First().Color ?? "#cccccc"
})
.ToList();
var itemDefs = categories
.SelectMany(c => c.Items)
.Select(i => i.BudgetItemDefinition?.Name ?? i.Name)
.Distinct()
.OrderBy(n => n)
.ToList();
var itemLabels = categories
.SelectMany(c => c.Items)
.Select(i => i.Name)
.Distinct()
.OrderBy(n => n)
.ToList();
Console.WriteLine($"Metadata: {categories.Count} categories, {itemLabels.Count} items");
return Ok(new
{
CategoryDefinitions = categoryDefs,
CategoryLabels = categoryLabels,
ItemDefinitions = itemDefs,
ItemLabels = itemLabels
});
}
[HttpGet("report/spreadsheet")]
public async Task<IActionResult> GetBudgetReport(
[FromQuery] int? year = null,
[FromQuery] List<int>? itemDefinitionIds = null,
[FromQuery] List<int>? categoryDefinitionIds = null,
[FromQuery] string? itemLabel = null,
[FromQuery] string? categoryLabel = null,
[FromQuery] bool includeCategoryDefinitions = false,
[FromQuery] bool includeCategoryLabels = false,
[FromQuery] bool includeItemDefinitions = true,
[FromQuery] bool includeItemLabels = false)
{
try
{
// ✅ Ladda navigationsproperties för definitions
var query = _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Definition) // CategoryDefinition
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.ThenInclude(i => i.BudgetItemDefinition) // ItemDefinition
.Where(p => p.Year.HasValue && p.Month.HasValue)
.AsQueryable();
if (year.HasValue)
query = query.Where(p => p.Year == year);
var periods = await query
.OrderByDescending(p => p.Year)
.ThenByDescending(p => p.Month)
.ToListAsync();
var reportData = periods.Select(p =>
{
var filteredCategories = p.Categories
.Where(c =>
(categoryDefinitionIds == null || categoryDefinitionIds.Count == 0 ||
(c.BudgetCategoryDefinitionId.HasValue &&
categoryDefinitionIds.Contains(c.BudgetCategoryDefinitionId.Value))) &&
(string.IsNullOrEmpty(categoryLabel) ||
(c.Name ?? string.Empty).Contains(categoryLabel, StringComparison.OrdinalIgnoreCase))
)
.ToList(); // Viktigt: gå över till LINQ to Objects
var filteredItems = filteredCategories
.SelectMany(c => c.Items)
.Where(i =>
i.IncludeInSummary &&
(itemDefinitionIds == null || itemDefinitionIds.Count == 0 ||
(i.BudgetItemDefinitionId.HasValue &&
itemDefinitionIds.Contains(i.BudgetItemDefinitionId.Value))) &&
(string.IsNullOrEmpty(itemLabel) ||
(i.Name ?? string.Empty).Contains(itemLabel, StringComparison.OrdinalIgnoreCase))
)
.ToList();
// 🔹 Skapa kolumner
var itemDefColumns = new Dictionary<string, decimal>();
var itemLabelColumns = new Dictionary<string, decimal>();
var catDefColumns = new Dictionary<string, decimal>();
var catLabelColumns = new Dictionary<string, decimal>();
if (includeItemDefinitions)
{
foreach (var g in filteredItems
.Where(i => i.BudgetItemDefinition != null)
.GroupBy(i => i.BudgetItemDefinition!.Name))
{
if (!string.IsNullOrEmpty(g.Key))
itemDefColumns[g.Key] = g.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
}
}
if (includeItemLabels)
{
foreach (var g in filteredItems
.Where(i => !string.IsNullOrEmpty(i.Name))
.GroupBy(i => i.Name))
{
itemLabelColumns[g.Key] = g.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
}
}
if (includeCategoryDefinitions)
{
foreach (var g in filteredCategories
.Where(c => c.Definition != null)
.GroupBy(c => c.Definition!.Name))
{
if (!string.IsNullOrEmpty(g.Key))
{
var total = g.SelectMany(c => c.Items)
.Where(i => i.IncludeInSummary)
.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
catDefColumns[g.Key] = total;
}
}
}
if (includeCategoryLabels)
{
foreach (var g in filteredCategories
.Where(c => !string.IsNullOrEmpty(c.Name))
.GroupBy(c => c.Name))
{
var total = g.SelectMany(c => c.Items)
.Where(i => i.IncludeInSummary)
.Sum(i => i.IsExpense ? -i.Amount : i.Amount);
catLabelColumns[g.Key] = total;
}
}
return new
{
id = p.Id,
year = p.Year ?? 0,
month = p.Month ?? 0,
income = filteredItems.Where(i => !i.IsExpense).Sum(i => i.Amount),
expense = filteredItems.Where(i => i.IsExpense).Sum(i => i.Amount),
net = filteredItems.Sum(i => i.IsExpense ? -i.Amount : i.Amount),
itemDefinitions = itemDefColumns,
itemLabels = itemLabelColumns,
categoryDefinitions = catDefColumns,
categoryLabels = catLabelColumns
};
});
return Ok(reportData);
}
catch (Exception ex)
{
Console.Error.WriteLine($"[GetBudgetReport] {ex}");
return StatusCode(500, $"Ett fel uppstod i rapportgenereringen: {ex.Message}");
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Aberwyn.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Aberwyn.Controllers
@@ -34,5 +35,7 @@ namespace Aberwyn.Controllers
var now = DateTime.Now;
return RedirectToAction("Index", new { year = now.Year, month = now.Month });
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Authorization;
using Aberwyn.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Aberwyn.Controllers
@@ -6,9 +7,9 @@ namespace Aberwyn.Controllers
[Authorize(Roles = "Budget")]
public class ReportController : Controller
{
public IActionResult BudgetReport()
public IActionResult Budget()
{
return View("BudgetReport");
return View(new BudgetReportViewModel());
}
}
}

View File

@@ -1,25 +1,13 @@
namespace Aberwyn.Models
{
public class BudgetReportRequestDto
public class BudgetReportViewModel
{
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 int? Year { get; set; }
public string GroupBy { get; set; } = "month";
public string? ItemLabel { get; set; }
public string? CategoryLabel { get; set; }
public List<int> ItemDefinitionIds { get; set; } = new();
public List<int> CategoryDefinitionIds { get; set; } = new();
}
public class BudgetReportResultDto
{
public int Year { get; set; }
public int Month { get; set; }
public List<DefinitionSumDto> Definitions { get; set; } = new();
}
public class DefinitionSumDto
{
public int DefinitionId { get; set; }
public string DefinitionName { get; set; }
public decimal TotalAmount { get; set; }
}
}

View File

@@ -40,6 +40,8 @@
<button ng-click="copyBudget(); menuOpen = false;">Kopiera</button>
<button ng-click="deleteBudget(); menuOpen = false;" class="danger">Ta bort</button>
<button ng-click="createNewCategory(); menuOpen = false;">Lägg till ny kategori</button>
<button ng-click="openImportModule(); menuOpen = false;">Importera text</button>
</div>
</div>
</div>
@@ -148,6 +150,10 @@
<input type="text" class="item-label" ng-model="item.name" ng-if="cat.editing" />
<span ng-if="!cat.editing" class="item-label" title="{{ item.name }}">{{ item.name }}</span>
<!-- debug
<span ng-if="!cat.editing" class="item-definition" title="{{ item.definitionName }}">#{{ item.budgetItemDefinitionId }} {{ item.definitionName }}</span>
<input type="text" ng-model="item.budgetItemDefinitionId" 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>

View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<title>Budget Översikt</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<link rel="stylesheet" href="~/css/budget-overview.css" />
</head>
<body ng-app="budgetApp" ng-controller="BudgetOverviewController" ng-cloak>
<h2>Visa kolumner</h2>
<div class="budget-column-selector-wrapper">
<!-- Kategori-definitioner -->
<div class="budget-column-selector">
<span>Kategori-definitioner:</span>
<input type="text" ng-model="searchCategoryDef" placeholder="Sök..."
ng-focus="openDropdown('catDef')" ng-click="$event.stopPropagation()" />
<div class="budget-dropdown-menu" ng-show="dropdownOpenCatDef">
<label ng-repeat="col in categoryDefColumns | filter:searchCategoryDef track by $index">
<input type="checkbox" ng-model="selectedCategoryDef[col]" /> {{ col }}
</label>
</div>
</div>
<!-- Kategori-namn -->
<div class="budget-column-selector">
<span>Kategori-namn:</span>
<input type="text" ng-model="searchCategoryLabel" placeholder="Sök..."
ng-focus="openDropdown('catLabel')" ng-click="$event.stopPropagation()" />
<div class="budget-dropdown-menu" ng-show="dropdownOpenCatLabel">
<label ng-repeat="col in categoryLabelColumns | filter:searchCategoryLabel track by $index">
<input type="checkbox" ng-model="selectedCategoryLabel[col]" /> {{ col }}
</label>
</div>
</div>
<!-- Item-definitioner -->
<div class="budget-column-selector">
<span>Item-definitioner:</span>
<input type="text" ng-model="searchItemDef" placeholder="Sök..."
ng-focus="openDropdown('itemDef')" ng-click="$event.stopPropagation()" />
<div class="budget-dropdown-menu" ng-show="dropdownOpenItemDef">
<label ng-repeat="col in itemDefColumns | filter:searchItemDef track by $index">
<input type="checkbox" ng-model="selectedItemDef[col]" /> {{ col }}
</label>
</div>
</div>
<!-- Item-namn -->
<div class="budget-column-selector">
<span>Item-namn:</span>
<input type="text" ng-model="searchItemLabel" placeholder="Sök..."
ng-focus="openDropdown('itemLabel')" ng-click="$event.stopPropagation()" />
<div class="budget-dropdown-menu" ng-show="dropdownOpenItemLabel">
<label ng-repeat="col in itemLabelColumns | filter:searchItemLabel track by $index">
<input type="checkbox" ng-model="selectedItemLabel[col]" /> {{ col }}
</label>
</div>
</div>
<!-- Månadssökfält med checkboxar och popup -->
<div class="budget-column-selector">
<span>Visa månader:</span>
<input type="text" ng-model="searchMonth" placeholder="Sök månad eller år..."
ng-focus="dropdownOpenMonth = true"
ng-click="$event.stopPropagation()" />
<div class="budget-dropdown-menu" ng-show="dropdownOpenMonth">
<label ng-repeat="month in allMonths | filter:searchMonth track by month.id">
<input type="checkbox" ng-model="selectedMonths[month.id]" /> {{ month.year }} - {{ month.name }}
</label>
</div>
</div>
</div>
<div class="budget-column-selector">
<label>
<input type="checkbox" ng-model="splitByYear" /> Dela upp per år
</label>
</div>
<div ng-if="!years.length" class="budget-loading-indicator">Laddar data…</div>
<!-- Budgetblad -->
<div ng-repeat="yearData in (splitByYear ? years : flatYears) | orderBy:'-year' track by yearData.year">
<div class="budget-year-header">{{ yearData.year }}</div>
<!-- Header -->
<div class="budget-sheet-header">
<div class="budget-sheet-cell month">Månad</div>
<div class="budget-sheet-cell income">Inkomst</div>
<div class="budget-sheet-cell expense">Utgift</div>
<div class="budget-sheet-cell net">Netto</div>
<div class="budget-extra-columns">
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryDef track by $index"
ng-if="selectedCategoryDef[colName]"
ng-style="{'background-color': categoryColors.catDef[colName]}">
{{ colName }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryLabel track by $index"
ng-if="selectedCategoryLabel[colName]"
ng-style="{'background-color': categoryColors.catLabel[colName]}">
{{ colName }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemDef track by $index"
ng-if="selectedItemDef[colName]">
{{ colName }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemLabel track by $index"
ng-if="selectedItemLabel[colName]">
{{ colName }}
</div>
</div>
</div>
<!-- Rader -->
<div class="budget-sheet-row" ng-repeat="month in yearData.months | filter:filterMonths track by month.id">
<div class="budget-sheet-cell month">{{ month.year }} - {{ monthNames[month.month-1] }}</div>
<div class="budget-sheet-cell income" ng-class="{'budget-positive': month.income >=0, 'budget-negative': month.income <0}">
{{ month.income | number:0 }}
</div>
<div class="budget-sheet-cell expense" ng-class="{'budget-positive': month.expense >=0, 'budget-negative': month.expense <0}">
{{ month.expense | number:0 }}
</div>
<div class="budget-sheet-cell net" ng-class="{'budget-positive': month.net >=0, 'budget-negative': month.net <0}">
{{ month.net | number:0 }}
</div>
<div class="budget-extra-columns">
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryDef track by $index"
ng-if="selectedCategoryDef[colName]"
ng-class="{'budget-positive': month.categoryDefinitions[colName] >=0, 'budget-negative': month.categoryDefinitions[colName] <0}">
{{ month.categoryDefinitions[colName] || 0 | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryLabel track by $index"
ng-if="selectedCategoryLabel[colName]"
ng-class="{'budget-positive': month.categoryLabels[colName] >=0, 'budget-negative': month.categoryLabels[colName] <0}">
{{ month.categoryLabels[colName] || 0 | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemDef track by $index"
ng-if="selectedItemDef[colName]"
ng-class="{'budget-positive': month.itemDefinitions[colName] >=0, 'budget-negative': month.itemDefinitions[colName] <0}">
{{ month.itemDefinitions[colName] || 0 | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemLabel track by $index"
ng-if="selectedItemLabel[colName]"
ng-class="{'budget-positive': month.itemLabels[colName] >=0, 'budget-negative': month.itemLabels[colName] <0}">
{{ month.itemLabels[colName] || 0 | number:0 }}
</div>
</div>
</div>
<!-- Totalsumma -->
<div class="budget-sheet-row budget-total-row" ng-if="yearData.months.length">
<div class="budget-sheet-cell month">Total</div>
<div class="budget-sheet-cell income">{{ getYearTotal(yearData.months, 'income') | number:0 }}</div>
<div class="budget-sheet-cell expense">{{ getYearTotal(yearData.months, 'expense') | number:0 }}</div>
<div class="budget-sheet-cell net">{{ getYearTotal(yearData.months, 'net') | number:0 }}</div>
<div class="budget-extra-columns">
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryDef track by $index"
ng-if="selectedCategoryDef[colName]">
{{ getYearTotal(yearData.months, 'categoryDefinitions', colName) | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.categoryLabel track by $index"
ng-if="selectedCategoryLabel[colName]">
{{ getYearTotal(yearData.months, 'categoryLabels', colName) | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemDef track by $index"
ng-if="selectedItemDef[colName]">
{{ getYearTotal(yearData.months, 'itemDefinitions', colName) | number:0 }}
</div>
<div class="budget-sheet-cell other"
ng-repeat="colName in columnOrder.itemLabel track by $index"
ng-if="selectedItemLabel[colName]">
{{ getYearTotal(yearData.months, 'itemLabels', colName) | number:0 }}
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script src="~/js/budget-overview.js"></script>
</body>
</html>

View File

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

View File

@@ -61,6 +61,19 @@
</li>
}
@if (User.IsInRole("Budget"))
{
<li class="dropdown">
<a href="#" class="dropdown-toggle" role="button" tabindex="0">
Rapporter <i class="fas fa-caret-down"></i>
</a>
<ul class="dropdown-menu">
<li><a asp-controller="report" asp-action="Budget">Budget</a></li>
</ul>
</li>
}
@if (User.IsInRole("Chef"))
{
<li class="dropdown">

View File

@@ -0,0 +1,161 @@
/* ===================== Budget Overview CSS ===================== */
body {
font-family: sans-serif;
font-size: 13px;
line-height: 1.4;
margin: 16px;
}
/* -------- Column selectors -------- */
.budget-column-selector-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.budget-column-selector {
position: relative; /* Viktigt för dropdown */
display: flex;
flex-direction: column;
min-width: 160px;
}
.budget-column-selector span {
font-weight: 500;
margin-bottom: 2px;
}
.budget-column-selector input[type="text"] {
padding: 3px 6px;
font-size: 13px;
border-radius: 4px;
border: 1px solid #ccc;
}
/* -------- Dropdown -------- */
.budget-dropdown-menu {
position: absolute;
top: 100%; /* Ligger direkt under inputfältet */
left: 0;
right: 0; /* Matchar inputens bredd */
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
z-index: 999;
max-height: 200px;
overflow-y: auto;
display: flex;
flex-direction: column; /* Vertikal lista */
padding: 2px 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
.budget-dropdown-menu label {
padding: 4px 8px;
cursor: pointer;
font-size: 12px;
}
.budget-dropdown-menu label:hover {
background: #f5f5f5;
}
/* -------- Spreadsheet -------- */
.budget-year-header {
font-weight: 600;
margin-top: 16px;
margin-bottom: 4px;
}
.budget-sheet-header, .budget-sheet-row {
display: flex;
align-items: center;
min-height: 28px;
border-bottom: 1px solid #ccc;
}
.budget-sheet-header {
font-weight: 600;
background: #f2f2f2;
}
/* Fixed main columns */
.budget-sheet-cell.month {
flex: 0 0 120px;
text-align: left;
padding: 4px 6px;
}
.budget-sheet-cell.income,
.budget-sheet-cell.expense,
.budget-sheet-cell.net {
flex: 0 0 100px;
text-align: right;
padding: 4px 6px;
}
/* Extra columns scrolls horizontally */
.budget-extra-columns-wrapper {
flex: 1;
overflow-x: auto;
}
.budget-extra-columns {
display: flex;
}
.budget-sheet-cell.other {
flex: 0 0 90px;
text-align: right;
padding: 4px 6px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Row hover & stripe */
.budget-sheet-row:nth-child(even) {
background: #f9f9f9;
}
.budget-sheet-row:hover {
background: #f0f0f0;
}
/* Positive/Negative coloring */
.budget-positive {
color: #2a9d8f;
font-weight: 300;
}
.budget-negative {
color: #e63946;
font-weight: 300;
}
/* Loading indicator */
.budget-loading-indicator {
font-style: italic;
color: #aaa;
margin: 12px 0;
}
.budget-total-row {
font-weight: bold;
background-color: #f0f0f0;
}
.budget-month-selector {
margin-bottom: 10px;
}
.budget-dropdown-menu {
border: 1px solid #ccc;
max-height: 200px;
overflow-y: auto;
position: absolute;
background: white;
z-index: 100;
padding: 5px;
}

View File

@@ -0,0 +1,164 @@
angular.module('budgetApp', [])
.controller('BudgetOverviewController', ['$scope', '$http', function ($scope, $http) {
$scope.years = [];
$scope.categoryDefColumns = [];
$scope.categoryLabelColumns = [];
$scope.itemDefColumns = [];
$scope.itemLabelColumns = [];
$scope.selectedCategoryDef = {};
$scope.selectedCategoryLabel = {};
$scope.selectedItemDef = {};
$scope.selectedItemLabel = {};
$scope.dropdownOpenCatDef = false;
$scope.dropdownOpenCatLabel = false;
$scope.dropdownOpenItemDef = false;
$scope.dropdownOpenItemLabel = false;
$scope.searchCategoryDef = '';
$scope.searchCategoryLabel = '';
$scope.searchItemDef = '';
$scope.searchItemLabel = '';
$scope.selectedMonths = {};
$scope.dropdownOpenMonth = false;
$scope.searchMonth = '';
$scope.splitByYear = true; // standard: per år
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
$scope.categoryColors = { catDef: {}, catLabel: {} };
$scope.columnOrder = { categoryDef: [], categoryLabel: [], itemDef: [], itemLabel: [] };
$scope.openDropdown = function (type) {
$scope.dropdownOpenCatDef = false;
$scope.dropdownOpenCatLabel = false;
$scope.dropdownOpenItemDef = false;
$scope.dropdownOpenItemLabel = false;
if (type === 'catDef') $scope.dropdownOpenCatDef = true;
if (type === 'catLabel') $scope.dropdownOpenCatLabel = true;
if (type === 'itemDef') $scope.dropdownOpenItemDef = true;
if (type === 'itemLabel') $scope.dropdownOpenItemLabel = true;
};
document.addEventListener('click', function (event) {
const el = document.querySelector('.budget-column-selector-wrapper');
if (el && !el.contains(event.target)) {
$scope.$apply(function () {
$scope.dropdownOpenCatDef = false;
$scope.dropdownOpenCatLabel = false;
$scope.dropdownOpenItemDef = false;
$scope.dropdownOpenItemLabel = false;
$scope.dropdownOpenMonth = false;
});
}
});
$http.get('/api/budget/metadata').then(function (metaRes) {
// Fyll metadata-kolumner
$scope.categoryDefColumns = (metaRes.data.CategoryDefinitions || []).map(c => c.Name);
$scope.categoryLabelColumns = (metaRes.data.CategoryLabels || []).map(c => c.Name);
$scope.itemDefColumns = (metaRes.data.ItemDefinitions || []).map(c => c.Name);
$scope.itemLabelColumns = (metaRes.data.ItemLabels || []).map(c => c.Name);
// Initiera val
$scope.categoryDefColumns.forEach(c => $scope.selectedCategoryDef[c] = false);
$scope.categoryLabelColumns.forEach(c => $scope.selectedCategoryLabel[c] = false);
$scope.itemDefColumns.forEach(c => $scope.selectedItemDef[c] = false);
$scope.itemLabelColumns.forEach(c => $scope.selectedItemLabel[c] = false);
// Färginställningar
(metaRes.data.CategoryDefinitions || []).forEach(c => $scope.categoryColors.catDef[c.Name] = c.Color || '#eee');
(metaRes.data.CategoryLabels || []).forEach(c => $scope.categoryColors.catLabel[c.Name] = c.Color || '#eee');
// Kolumnordning
$scope.columnOrder.categoryDef = [...$scope.categoryDefColumns];
$scope.columnOrder.categoryLabel = [...$scope.categoryLabelColumns];
$scope.columnOrder.itemDef = [...$scope.itemDefColumns];
$scope.columnOrder.itemLabel = [...$scope.itemLabelColumns];
// Hämta rapportdata
return $http.get('/api/budget/report/spreadsheet?includeItemDefinitions=true&includeItemLabels=true&includeCategoryDefinitions=true&includeCategoryLabels=true');
}).then(function (res) {
const sorted = res.data.sort((a, b) => (a.year || 0) - (b.year || 0) || (a.month || 0) - (b.month || 0));
sorted.forEach(function (month) {
['itemDefinitions', 'itemLabels', 'categoryDefinitions', 'categoryLabels'].forEach(function (section) {
Object.keys(month[section] || {}).forEach(function (col) {
if (section === 'itemDefinitions' && !$scope.selectedItemDef.hasOwnProperty(col)) {
$scope.itemDefColumns.push(col);
$scope.selectedItemDef[col] = false;
$scope.columnOrder.itemDef.push(col);
}
if (section === 'itemLabels' && !$scope.selectedItemLabel.hasOwnProperty(col)) {
$scope.itemLabelColumns.push(col);
$scope.selectedItemLabel[col] = false;
$scope.columnOrder.itemLabel.push(col);
}
if (section === 'categoryDefinitions' && !$scope.selectedCategoryDef.hasOwnProperty(col)) {
$scope.categoryDefColumns.push(col);
$scope.selectedCategoryDef[col] = false;
$scope.columnOrder.categoryDef.push(col);
}
if (section === 'categoryLabels' && !$scope.selectedCategoryLabel.hasOwnProperty(col)) {
$scope.categoryLabelColumns.push(col);
$scope.selectedCategoryLabel[col] = false;
$scope.columnOrder.categoryLabel.push(col);
}
});
});
});
// Bygg år och månader
const yearsMap = {};
sorted.forEach(function (m) {
if (!yearsMap[m.year]) yearsMap[m.year] = [];
yearsMap[m.year].push(m);
});
$scope.years = Object.keys(yearsMap).map(y => ({ year: parseInt(y), months: yearsMap[y] })).sort((a, b) => b.year - a.year);
// ✅ Bygg månadsväljaren här
$scope.allMonths = [];
$scope.years.forEach(y => {
y.months.forEach(m => {
$scope.allMonths.push({
id: `${m.year}-${(m.month < 10 ? '0' : '') + m.month}`,
year: m.year,
name: $scope.monthNames[m.month - 1]
});
});
});
}).catch(function (err) {
console.error('Fel vid API-anrop:', err);
});
$scope.getYearTotal = function (months, section, colName) {
return months.reduce(function (sum, m) {
if (section === 'income' || section === 'expense' || section === 'net') { return sum + (m[section] || 0); }
return sum + ((m[section] && m[section][colName]) || 0);
}, 0);
};
$scope.filterMonths = function (month) {
const anySelected = Object.values($scope.selectedMonths).some(v => v);
if (!anySelected) return true; // Visa alla om inget valt
const monthId = month.year + '-' + (month.month < 10 ? '0' : '') + month.month;
return !!$scope.selectedMonths[monthId];
};
$scope.$watch('splitByYear', function (newVal) {
if (!newVal) {
const allMonths = [];
$scope.years.forEach(y => allMonths.push(...y.months));
$scope.flatYears = [{ year: 'Allt', months: allMonths }];
} else {
$scope.flatYears = $scope.years;
}
});
}]);

View File

@@ -257,6 +257,7 @@ app.controller('BudgetController', function ($scope, $http) {
amount: item.amount,
isExpense: item.isExpense,
includeInSummary: item.includeInSummary,
budgetItemDefinitionId: item.budgetItemDefinitionId,
budgetCategoryId: category.id,
order: index
}))