More features
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-09-12 00:27:35 +02:00
parent 64aa9cf716
commit 8aed8d16b6
5 changed files with 339 additions and 104 deletions

View File

@@ -434,52 +434,6 @@ namespace Aberwyn.Controllers
}
// POST: api/budget/copy/byname/{targetName}?from={sourceName}
[HttpPost("copy/byname/{targetName}")]
public async Task<IActionResult> CopyFromNamedBudget(string targetName, [FromQuery] string from)
{
var targetPeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == targetName.ToLower());
if (targetPeriod != null && targetPeriod.Categories.Any())
return BadRequest("Det finns redan data för denna budget.");
var sourcePeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == from.ToLower());
if (sourcePeriod == null)
return NotFound("Ingen budget hittades att kopiera från.");
var newPeriod = new BudgetPeriod
{
Name = targetName,
Categories = sourcePeriod.Categories.Select(cat => new BudgetCategory
{
Name = cat.Name,
Color = cat.Color,
Order = cat.Order,
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId,
Items = cat.Items.Select(item => new BudgetItem
{
Name = item.Name,
Amount = item.Amount,
IsExpense = item.IsExpense,
IncludeInSummary = item.IncludeInSummary,
Order = item.Order,
BudgetItemDefinitionId = item.BudgetItemDefinitionId
}).ToList()
}).ToList()
};
_context.BudgetPeriods.Add(newPeriod);
await _context.SaveChangesAsync();
return Ok(new { id = newPeriod.Id });
}
[HttpDelete("item/{id}")]
@@ -629,5 +583,131 @@ namespace Aberwyn.Controllers
// Gemensam intern metod
private async Task<BudgetPeriod?> CopyBudgetAsync(
BudgetPeriod targetPeriod,
BudgetPeriod sourcePeriod)
{
if (sourcePeriod == null) return null;
targetPeriod.Categories = sourcePeriod.Categories.Select(cat => new BudgetCategory
{
Name = cat.Name,
Color = cat.Color,
Order = cat.Order,
BudgetCategoryDefinitionId = cat.BudgetCategoryDefinitionId,
Items = cat.Items.Select(item => new BudgetItem
{
Name = item.Name,
Amount = item.Amount,
IsExpense = item.IsExpense,
IncludeInSummary = item.IncludeInSummary,
Order = item.Order,
BudgetItemDefinitionId = item.BudgetItemDefinitionId
}).ToList()
}).ToList();
await _context.SaveChangesAsync();
return targetPeriod;
}
[HttpPost("copy/byname/{targetName}")]
public async Task<IActionResult> CopyToNamedBudget(
string targetName,
[FromQuery] string? from,
[FromQuery] int? fromYear,
[FromQuery] int? fromMonth)
{
var targetPeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == targetName.ToLower());
if (targetPeriod == null)
{
targetPeriod = new BudgetPeriod { Name = targetName };
_context.BudgetPeriods.Add(targetPeriod);
}
else if (targetPeriod.Categories.Any())
{
return BadRequest("Det finns redan data för denna budget.");
}
// Hämta källperiod
var sourcePeriod = await FindSourcePeriod(from, fromYear, fromMonth);
if (sourcePeriod == null)
return NotFound("Ingen budget hittades att kopiera från.");
await CopyBudgetAsync(targetPeriod, sourcePeriod);
return Ok(new { id = targetPeriod.Id });
}
[HttpPost("copy/{year:int}/{month:int}")]
public async Task<IActionResult> CopyToYearMonth(
int year,
int month,
[FromQuery] string? from,
[FromQuery] int? fromYear,
[FromQuery] int? fromMonth)
{
var targetPeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Year == year && p.Month == month);
if (targetPeriod == null)
{
targetPeriod = new BudgetPeriod { Year = year, Month = month };
_context.BudgetPeriods.Add(targetPeriod);
}
else if (targetPeriod.Categories.Any())
{
return BadRequest("Det finns redan data för denna månad.");
}
// Hämta källperiod
var sourcePeriod = await FindSourcePeriod(from, fromYear, fromMonth, year, month);
if (sourcePeriod == null)
return NotFound("Ingen data att kopiera från.");
await CopyBudgetAsync(targetPeriod, sourcePeriod);
return Ok(new { id = targetPeriod.Id });
}
private async Task<BudgetPeriod?> FindSourcePeriod(
string? from,
int? fromYear,
int? fromMonth,
int? defaultYear = null,
int? defaultMonth = null)
{
BudgetPeriod? sourcePeriod = null;
if (!string.IsNullOrEmpty(from))
{
sourcePeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Name != null && p.Name.ToLower() == from.ToLower());
}
else if (fromYear.HasValue && fromMonth.HasValue)
{
sourcePeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Year == fromYear && p.Month == fromMonth);
}
else if (defaultYear.HasValue && defaultMonth.HasValue)
{
var previous = new DateTime(defaultYear.Value, defaultMonth.Value, 1).AddMonths(-1);
sourcePeriod = await _context.BudgetPeriods
.Include(p => p.Categories)
.ThenInclude(c => c.Items)
.FirstOrDefaultAsync(p => p.Year == previous.Year && p.Month == previous.Month);
}
return sourcePeriod;
}
}
}

View File

@@ -28,9 +28,8 @@
<a href="/budget/list" class="nav-button" style="margin-right: 10px;">
Lista
</a>
<span class="month-label">
{{ budget.name }}
</span>
<span class="month-label" ng-bind="budget.name"></span>
</div>
<div class="menu-container" ng-class="{ 'open': menuOpen }">
<button class="icon-button" ng-click="toggleMenu($event)">
@@ -203,20 +202,61 @@
</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>.
<!-- Ingen budget alls -->
<div class="no-data" ng-if="!loading && (!budget || !budget.id)">
<p>
<strong ng-bind="budget && budget.name
? 'Budgeten \" ' + budget.name + ' \" finns inte.'
: 'Det finns ingen budget för ' + getMonthName(selectedMonth) + ' ' + selectedYear + '.' ">
</strong>
</p>
<div style="margin-top: 10px;">
<!-- Skapa ny budget alltid om budget saknas -->
<button ng-click="createEmptyBudget()" style="margin-right: 10px;">
Skapa ny budget
</button>
<button ng-click="copyPreviousMonthSafe()">
</div>
</div>
<!-- Budget finns men inga kategorier -->
<div class="no-data" ng-if="!loading && budget && budget.id && (!budget.categories || budget.categories.length === 0)">
<p>
Budgeten <strong>{{ budget.name || (getMonthName(selectedMonth) + " " + selectedYear) }}</strong> har inga kategorier än.
</p>
<div style="margin-top: 10px;">
<button ng-click="createNewCategory()" style="margin-right: 10px;">
Skapa ny kategori
</button>
<button ng-if="!budget.name" ng-click="copyPreviousMonthSafe()" style="margin-right: 10px;">
Kopiera föregående månad
</button>
<button ng-click="copyBudget()" style="margin-right: 10px;">
Kopiera befintlig budget
</button>
</div>
</div>
<!-- Modal -->
<div class="modal-backdrop" ng-show="showCopyModal">
<div class="modal-content">
<h3>Kopiera budget</h3>
<select ng-model="selectedBudgetToCopy"
ng-options="b as formatBudgetName(b) for b in budgetList track by b.id">
<option value="">Välj budget</option>
</select>
<div style="margin-top: 10px;">
<button ng-click="confirmCopyBudget()">Kopiera</button>
<button ng-click="showCopyModal = false">Avbryt</button>
</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">
@@ -283,7 +323,8 @@
<script>
window.initialYear = @(ViewBag.Year ?? "null");
window.initialMonth = @(ViewBag.Month ?? "null");
window.initialName = "@(ViewBag.BudgetName ?? "")";
window.initialName = "@Html.Raw(ViewBag.BudgetName ?? "")";
</script>
<script src="~/js/budget.js"></script>

View File

@@ -141,21 +141,4 @@
height: 16px;
}
/* Mobilanpassning */
@media (max-width: 768px) {
.budget-card {
min-width: 50px;
}
.month-name {
font-size: 12px;
}
.bar {
width: 6px;
}
.month-bars {
height: 50px;
}
}

View File

@@ -780,3 +780,23 @@ body.dark-mode {
[ng-cloak] {
display: none !important;
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 6px;
min-width: 300px;
}

View File

@@ -5,6 +5,7 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.loading = true;
$scope.error = null;
$scope.menuOpen = false;
$scope.chartMode = "pie";
const initialName = window.initialName;
@@ -337,37 +338,125 @@ app.controller('BudgetController', function ($scope, $http) {
});
}
};
$scope.selectedBudgetToCopy = null;
$scope.formatBudgetName = function (b) {
if (b.name) return b.name; // Namnbudget
if (b.year && b.month) {
// Visa t.ex. "Mars 2025"
const monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
return monthNames[b.month - 1] + " " + b.year;
}
return "Okänd budget";
};
$scope.showCopyModal = false; // styr om modalen syns
$scope.budgetList = []; // lista över alla budgeter
$scope.selectedBudgetToCopy = null;
$scope.copyBudget = function () {
$scope.loadBudgetList(); // fyller $scope.budgetList
$scope.selectedBudgetToCopy = null;
$scope.showCopyModal = true;
};
$scope.confirmCopyBudget = function () {
if (!$scope.selectedBudgetToCopy) {
$scope.showToast("Välj en budget att kopiera från", true);
return;
}
// Förhindra att kopiera till sig själv
if ($scope.budget.name && $scope.selectedBudgetToCopy.name &&
$scope.budget.name === $scope.selectedBudgetToCopy.name) {
$scope.showToast("Du kan inte kopiera en budget till sig själv", true);
return;
}
if (!$scope.budget.name && $scope.selectedBudgetToCopy.year &&
$scope.selectedBudgetToCopy.month &&
$scope.selectedYear === $scope.selectedBudgetToCopy.year &&
$scope.selectedMonth === $scope.selectedBudgetToCopy.month) {
$scope.showToast("Du kan inte kopiera en budget till sig själv", true);
return;
}
let url;
if ($scope.budget.name) {
const from = prompt("Ange namnet på budgeten du vill kopiera ifrån:");
if (!from) return;
$http.post(`/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}?from=${encodeURIComponent(from)}`)
.then(() => {
$scope.showToast("Budget kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte kopiera budget:", err);
$scope.showToast("Fel vid kopiering", true);
});
// Aktiv budget = namnbudget
url = `/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}`;
if ($scope.selectedBudgetToCopy.name) {
url += `?from=${encodeURIComponent($scope.selectedBudgetToCopy.name)}`;
} else {
url += `?fromYear=${$scope.selectedBudgetToCopy.year}&fromMonth=${$scope.selectedBudgetToCopy.month}`;
}
} else {
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) return;
// Aktiv budget = månad/år
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
$http.post(`/api/budget/copy/${year}/${month}`)
.then(() => {
$scope.showToast("Föregående månad kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte kopiera månad:", err);
$scope.showToast("Fel vid kopiering av månad", true);
});
url = `/api/budget/copy/${year}/${month}`;
if ($scope.selectedBudgetToCopy.name) {
url += `?from=${encodeURIComponent($scope.selectedBudgetToCopy.name)}`;
} else {
url += `?fromYear=${$scope.selectedBudgetToCopy.year}&fromMonth=${$scope.selectedBudgetToCopy.month}`;
}
}
console.log("Postar till URL:", url);
$http.post(url)
.then(() => {
$scope.showToast("Budget kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte kopiera budget:", err);
$scope.showToast("Fel vid kopiering", true);
})
.finally(() => {
$scope.showCopyModal = false;
});
};
$scope.copyExistingBudget = function () {
const from = prompt("Ange namnet på budgeten du vill kopiera från:");
if (!from) return;
let url;
if ($scope.budget.name) {
// Om det är en namnbudget
url = `/api/budget/copy/byname/${encodeURIComponent($scope.budget.name)}?from=${encodeURIComponent(from)}`;
} else {
// Om det är en månadsbudget
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
url = `/api/budget/copy/${year}/${month}?from=${encodeURIComponent(from)}`;
}
$http.post(url)
.then(() => {
$scope.showToast("Budget kopierad!");
$scope.loadBudget();
})
.catch(err => {
console.error("Fel vid kopiering av budget:", err);
if (err.status === 404) {
$scope.showToast("Budgeten du försökte kopiera från finns inte.", true);
} else if (err.status === 400) {
$scope.showToast("Budgeten har redan data.", true);
} else {
$scope.showToast("Kunde inte kopiera budget.", true);
}
});
};
$scope.createNamedBudget = function () {
const name = prompt("Ange namn på din nya budget:");
if (!name) return;
@@ -920,27 +1009,36 @@ $scope.addItemFromDefinition = function (cat) {
};
$scope.createEmptyBudget = function () {
if (!$scope.budget || !$scope.budget.name) {
$scope.showToast("Ogiltigt budgetnamn.");
if (!$scope.budget) {
$scope.showToast("Ogiltig budget.");
return;
}
const dto = {
name: $scope.budget.name
};
let payload = {};
$http.post('/api/budget', dto)
if ($scope.budget.name) {
// Namnbudget
payload.name = $scope.budget.name;
} else {
// Månad/år-budget
payload.year = $scope.selectedYear;
payload.month = $scope.selectedMonth;
}
$http.post('/api/budget', payload)
.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.showToast("Kunde inte skapa budget.", true);
});
};
$scope.drawCategoryChart = function () {
const ctx = document.getElementById("expenseChart");
if (!ctx || !$scope.budget?.categories) return;
@@ -1053,6 +1151,19 @@ $scope.addItemFromDefinition = function (cat) {
$scope.importPreview = [];
};
$scope.budgetList = [];
$scope.loadBudgetList = function () {
return $http.get('/api/budget/list') // eller den endpoint du använder
.then(res => {
$scope.budgetList = res.data;
})
.catch(err => {
console.error("Kunde inte hämta budgetlista:", err);
$scope.showToast("Fel vid hämtning av budgetlista", true);
});
};
$scope.loadBudgetList();
$scope.loading = true;