This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
200
Aberwyn/Views/Report/Budget.cshtml
Normal file
200
Aberwyn/Views/Report/Budget.cshtml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
161
Aberwyn/wwwroot/css/budget-overview.css
Normal file
161
Aberwyn/wwwroot/css/budget-overview.css
Normal 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;
|
||||
}
|
||||
164
Aberwyn/wwwroot/js/budget-overview.js
Normal file
164
Aberwyn/wwwroot/js/budget-overview.js
Normal 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;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}]);
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user