Files
Aberwyn/Aberwyn/wwwroot/js/budget.js
Elias Jansson 7b3c0998a0
All checks were successful
continuous-integration/drone/push Build is passing
Same same
2026-02-11 19:56:31 +01:00

1185 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
var app = angular.module('budgetApp', []);
console.log("budget.js loaded");
app.controller('BudgetController', function ($scope, $http) {
$scope.budget = null;
$scope.loading = true;
$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) {
return $scope.monthNames[month - 1] || "";
};
$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);
$scope.getMonthName = function (month) {
return $scope.monthNames[month - 1] || "";
};
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.toggleMenu = function (e) {
e.stopPropagation();
$scope.menuOpen = !$scope.menuOpen;
};
$scope.menuVisible = false;
$scope.menuItem = null;
$scope.menuStyle = {};
$scope.openItemMenu = function ($event, item) {
const rect = $event.currentTarget.getBoundingClientRect();
console.log("Menu position:", rect);
$scope.menuItem = item;
$scope.menuItem.category = $scope.budget.categories.find(c => c.items.includes(item));
$scope.menuStyle = {
top: `${rect.bottom + 4}px`,
left: `${rect.left}px`
};
$scope.menuVisible = true;
};
$scope.updateMonthAndUrl = function () {
const year = $scope.selectedYear;
const month = $scope.selectedMonth.toString();
const newUrl = `/budget/${year}/${month}`;
window.history.replaceState(null, '', newUrl); // Uppdaterar URL utan reload
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
$scope.tempMonth = $scope.selectedMonthName;
$scope.tempYear = $scope.selectedYear;
$scope.loadBudget();
};
$scope.loadSalaryDate = function () {
const year = $scope.selectedYear;
const month = $scope.selectedMonth -1;
$http.get(`/api/budget/${year}/${month}/salary-date`)
.then(function (res) {
$scope.salaryPayDate = res.data.display;
});
};
$scope.setItemType = function (item, type) {
if (type === 'expense') {
item.isExpense = true;
item.includeInSummary = true;
} else if (type === 'income') {
item.isExpense = false;
item.includeInSummary = true;
} else if (type === 'saving') {
item.isExpense = false;
item.includeInSummary = false;
}
$scope.menuVisible = false;
};
// Klick utanför stänger popup
document.addEventListener('click', function (e) {
const menu = document.querySelector('.item-floating-menu');
if (menu && !$scope.menuVisible) return;
const isInside = menu?.contains(e.target);
const isButton = e.target.closest('.icon-button');
if (!isInside && !isButton) {
$scope.$apply(() => $scope.menuVisible = false);
}
});
document.addEventListener('click', function () {
$scope.$apply(() => {
$scope.menuOpen = false;
});
});
let lastTapTime = 0;
$scope.handleItemInteraction = function (event, item) {
const now = new Date().getTime();
// Ctrl-klick på desktop
if (event.ctrlKey) {
togglePaymentStatus(item);
return;
}
// Dubbeltap på mobil (inom 400ms)
if (now - lastTapTime < 400) {
togglePaymentStatus(item);
event.preventDefault();
}
lastTapTime = now;
};
function togglePaymentStatus(item) {
item.paymentStatus = (item.paymentStatus + 1) % 3;
$http.put("/api/budget/updatePaymentStatus", {
itemId: item.id,
status: item.paymentStatus
}).then(() => {
$scope.showToast("Betalstatus uppdaterad");
});
}
$scope.showToast = function (message, isError = false) {
const toast = document.createElement("div");
toast.className = "toast" + (isError ? " error" : "") + " show";
toast.innerText = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 3000);
};
$scope.loadBudget = function () {
$scope.loading = true;
$scope.error = null;
$scope.budget = null;
$scope.budgetNotFound = false;
const useName = typeof initialName === 'string' && initialName !== "null" && initialName !== "";
let url = "";
if (useName) {
url = `/api/budget/byname/${encodeURIComponent(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,
name: cat.Name,
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 {
id: item.Id,
name: item.Name,
amount: parseFloat(item.Amount),
isExpense: item.IsExpense === true,
includeInSummary: item.IncludeInSummary === true,
order: item.Order ?? index,
budgetItemDefinitionId: item.BudgetItemDefinitionId,
definitionName: definition?.Name || null,
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;
}
})
.catch(function (error) {
if (error.status === 404) {
$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);
console.error("Budget API error:", error);
}
})
.finally(function () {
$scope.loadSalaryDate();
$scope.loading = false;
setTimeout($scope.drawCategoryChart, 0);
});
};
$scope.saveCategory = function (category) {
if (category.newItemName && category.newItemAmount) {
const newItem = {
name: category.newItemName,
amount: parseFloat(category.newItemAmount),
isExpense: true,
includeInSummary: true,
budgetCategoryId: category.id,
};
$http.post("/api/budget/item", newItem)
.then(res => {
if (res.data && res.data.id) {
newItem.id = res.data.id;
category.items.push(newItem);
category.newItemName = "";
category.newItemAmount = "";
$scope.showToast("Post sparad!");
} else {
$scope.showToast("Fel vid sparande av ny post", true);
}
});
}
const payload = {
id: category.id,
name: category.name,
color: category.color,
budgetCategoryDefinitionId: category.budgetCategoryDefinitionId || null,
items: category.items.map((item, index) => ({
id: item.id,
name: item.name,
amount: item.amount,
isExpense: item.isExpense,
includeInSummary: item.includeInSummary,
budgetItemDefinitionId: item.budgetItemDefinitionId,
budgetCategoryId: category.id,
order: index
}))
};
$http.put(`/api/budget/category/${category.id}`, payload)
.then(() => {
$scope.showToast("Kategori sparad!");
category.editing = false; // ✅ flytta hit
})
.catch(() => {
$scope.showToast("Fel vid sparande av kategori", true);
});
};
$scope.deleteCategory = function (category) {
if (!confirm("Vill du verkligen ta bort kategorin och alla dess poster?")) return;
$http.delete(`/api/budget/category/${category.id}`)
.then(() => {
$scope.budget.categories = $scope.budget.categories.filter(c => c.id !== category.id);
$scope.showToast("Kategori borttagen!");
})
.catch(err => {
console.error("Fel vid borttagning av kategori:", err);
$scope.showToast("Kunde inte ta bort kategori", true);
});
};
$scope.copyPreviousMonthSafe = function () {
console.log("click OK");
$scope.menuOpen = false;
setTimeout(function () {
$scope.copyPreviousMonth();
}, 10);
};
$scope.deleteMonth = function () {
if (!confirm("Vill du verkligen ta bort alla data för denna månad?")) return;
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
$http.delete(`/api/budget/${year}/${month}`)
.then(() => {
$scope.showToast("Månad borttagen!");
$scope.loadBudget(); // tömmer sidan
})
.catch(err => {
console.error("Kunde inte ta bort månad:", err);
$scope.showToast("Fel vid borttagning av månad", true);
});
};
$scope.deleteBudget = function () {
if (!confirm("Vill du verkligen ta bort hela budgeten?")) return;
if ($scope.budget.name) {
// Namnbudget
$http.delete(`/api/budget/byname/${encodeURIComponent($scope.budget.name)}`)
.then(() => {
$scope.showToast("Budget borttagen!");
window.location.href = "/budget"; // gå tillbaka till startsida
})
.catch(err => {
console.error("Kunde inte ta bort budget:", err);
$scope.showToast("Fel vid borttagning", true);
});
} else {
// Månad
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
$http.delete(`/api/budget/${year}/${month}`)
.then(() => {
$scope.showToast("Månad borttagen!");
$scope.loadBudget();
})
.catch(err => {
console.error("Kunde inte ta bort månad:", err);
$scope.showToast("Fel vid borttagning", true);
});
}
};
$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) {
// 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 {
// Aktiv budget = månad/år
const year = $scope.selectedYear;
const month = $scope.selectedMonth;
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;
$http.post('/api/budget', { name: name })
.then(res => {
$scope.showToast("Ny budget skapad!");
window.location.href = `/budget/${encodeURIComponent(name)}`;
})
.catch(err => {
console.error("Fel vid skapande:", err);
if (err.status === 409) {
$scope.showToast("En budget med detta namn finns redan.", true);
} else {
$scope.showToast("Kunde inte skapa budget.", true);
}
});
};
$scope.copyPreviousMonth = function () {
if (!confirm("Vill du kopiera föregående månad till den aktuella?")) {
return;
}
console.log("Försöker kopiera föregående månad...");
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 föregående månad:", err);
if (err.status === 404) {
$scope.showToast("Ingen föregående månad att kopiera från", true);
} else if (err.status === 400) {
$scope.showToast("Data finns redan för denna månad", true);
} else {
$scope.showToast("Kunde inte kopiera", true);
}
});
};
$scope.cancelCategoryEdit = function (category) {
category.editing = false;
};
function getItemsFlat() {
if (!$scope.budget || !$scope.budget.categories) return [];
return $scope.budget.categories.flatMap(c => c.items || []);
}
$scope.getCategorySum = function (category) {
return (category.items || []).reduce((sum, i) => sum + i.amount, 0);
};
$scope.getTotalIncome = function () {
return getItemsFlat().filter(i => !i.isExpense && i.includeInSummary).reduce((sum, i) => sum + i.amount, 0);
};
$scope.getTotalExpense = function () {
return getItemsFlat().filter(i => i.isExpense && i.includeInSummary).reduce((sum, i) => sum + i.amount, 0);
};
$scope.getTotalSaving = function () {
return getItemsFlat().filter(i => !i.isExpense && !i.includeInSummary).reduce((sum, i) => sum + i.amount, 0);
};
$scope.getLeftover = function () {
return $scope.getTotalIncome() - $scope.getTotalExpense() - $scope.getTotalSaving();
};
function positionAddItemPopup(popup, triggerButton) {
const rect = popup.getBoundingClientRect();
const bottomSpace = window.innerHeight - rect.bottom;
// Om popupen sticker utanför skärmen, placera den ovanför
if (bottomSpace < 50) {
popup.classList.add('above');
} else {
popup.classList.remove('above');
}
}
$scope.applyMonthSelection = function () {
const monthIndex = $scope.monthNames.indexOf($scope.tempMonth);
if (monthIndex >= 0 && $scope.tempYear) {
$scope.selectedMonth = monthIndex + 1;
$scope.selectedYear = parseInt($scope.tempYear);
$scope.updateMonthAndUrl();
$scope.showMonthPicker = false;
}
};
$scope.previousMonth = function () {
if ($scope.selectedMonth === 1) {
$scope.selectedMonth = 12;
$scope.selectedYear--;
} else {
$scope.selectedMonth--;
}
$scope.updateMonthAndUrl();
};
$scope.nextMonth = function () {
if ($scope.selectedMonth === 12) {
$scope.selectedMonth = 1;
$scope.selectedYear++;
} else {
$scope.selectedMonth++;
}
$scope.updateMonthAndUrl();
};
$scope.handleCategoryDrop = function (data, targetCategory) {
if (data.type !== 'category') return; // ⛔ stoppa om det är ett item-drag
const categories = $scope.budget.categories;
const draggedIndex = categories.findIndex(c => c.id === data.categoryId);
const targetIndex = categories.findIndex(c => c.id === targetCategory.id);
if (draggedIndex === -1 || targetIndex === -1 || draggedIndex === targetIndex) return;
const moved = categories.splice(draggedIndex, 1)[0];
categories.splice(targetIndex, 0, moved);
categories.forEach((cat, i) => {
cat.order = i;
});
const payload = $scope.budget.categories.map(cat => ({
id: cat.id,
name: cat.name,
color: cat.color,
order: cat.order,
year: $scope.selectedYear,
month: $scope.selectedMonth,
items: [] // tom lista backend kräver den, men vi använder den inte här
}));
$http.put("/api/budget/category/order", payload)
.then(() => $scope.showToast("Kategorier omordnade!"))
.catch(() => $scope.showToast("Fel vid uppdatering av ordning", true));
};
$scope.handleItemDrop = function ({ data, targetCategory, targetIndex }) {
const sourceCategory = $scope.budget.categories.find(c => c.id === data.fromCategoryId);
const draggedItem = sourceCategory?.items.find(i => i.id === data.itemId);
if (!draggedItem || !targetCategory) return;
// Ta bort från ursprung
const draggedIndex = sourceCategory.items.findIndex(i => i.id === draggedItem.id);
if (draggedIndex !== -1) {
sourceCategory.items.splice(draggedIndex, 1);
}
// Uppdatera värden
draggedItem.budgetCategoryId = targetCategory.id;
const indexToInsert = typeof targetIndex === 'number' ? targetIndex : targetCategory.items.length;
targetCategory.items.splice(indexToInsert, 0, draggedItem);
// Uppdatera ordning
targetCategory.items.forEach((item, i) => {
item.order = i;
});
// ✅ Skicka PUT för det flyttade itemet
const payload = {
id: draggedItem.id,
name: draggedItem.name,
amount: draggedItem.amount,
isExpense: draggedItem.isExpense,
includeInSummary: draggedItem.includeInSummary,
order: draggedItem.order,
budgetCategoryId: draggedItem.budgetCategoryId
};
$http.put(`/api/budget/item/${draggedItem.id}`, payload)
.then(() => {
console.log(">>> Sparad!");
$scope.showToast("Post omplacerad!");
})
.catch(err => {
console.error("Kunde inte uppdatera post:", err);
$scope.showToast("Fel vid omplacering", true);
});
};
$scope.handleItemPreciseDrop = function (data, targetCategory, targetIndex) {
if (data.type !== 'item') return;
const sourceCategory = $scope.budget.categories.find(c => c.id === data.fromCategoryId);
const draggedItem = sourceCategory?.items.find(i => i.id === data.itemId);
if (!draggedItem || !targetCategory) return;
// Ta bort från ursprung
const draggedIndex = sourceCategory.items.findIndex(i => i.id === draggedItem.id);
if (draggedIndex !== -1) {
sourceCategory.items.splice(draggedIndex, 1);
}
// Uppdatera kategori och lägg till på exakt plats
draggedItem.budgetCategoryId = targetCategory.id;
targetCategory.items.splice(targetIndex, 0, draggedItem);
// Uppdatera ordning
targetCategory.items.forEach((item, i) => {
item.order = i;
});
// ✅ PUT för det flyttade itemet
const payload = {
id: draggedItem.id,
name: draggedItem.name,
amount: draggedItem.amount,
isExpense: draggedItem.isExpense,
includeInSummary: draggedItem.includeInSummary,
order: draggedItem.order,
budgetCategoryId: draggedItem.budgetCategoryId
};
$http.put(`/api/budget/item/${draggedItem.id}`, payload)
.then(() => {
console.log(">>> Sparad!");
$scope.showToast("Post omplacerad!");
})
.catch(err => {
console.error("Kunde inte uppdatera post:", err);
$scope.showToast("Fel vid omplacering", true);
});
};
$scope.updateItemType = function (item) {
if (item.type === 'income') {
item.isExpense = false;
item.includeInSummary = true;
} else if (item.type === 'expense') {
item.isExpense = true;
item.includeInSummary = true;
} else if (item.type === 'saving') {
item.isExpense = false;
item.includeInSummary = false;
}
};
$scope.setItemType = function (item, type) {
if (type === 'income') {
item.isExpense = false;
item.includeInSummary = true;
} else if (type === 'expense') {
item.isExpense = true;
item.includeInSummary = true;
} else if (type === 'saving') {
item.isExpense = false;
item.includeInSummary = false;
}
};
$scope.createNewCategory = function () {
const defaultName = "Ny kategori";
const newOrder = $scope.budget.categories.length;
const newCategory = {
name: defaultName,
color: "#666666",
order: newOrder,
budgetPeriodId: $scope.budget.id // <- 💡 den viktiga raden
};
$http.post("/api/budget/category", newCategory)
.then(res => {
if (res.data && res.data.id) {
$scope.budget.categories.push({
id: res.data.id,
name: defaultName,
color: "#666666",
order: newOrder,
items: [],
editing: true,
allowDrag: false
});
$scope.showToast("Kategori skapad!");
} else {
$scope.showToast("Misslyckades med att skapa kategori", true);
}
})
.catch(err => {
console.error("Fel vid skapande av kategori:", err);
$scope.showToast("Fel vid skapande av kategori", true);
});
};
$scope.addItem = function (category) {
if (!category.newItemName || !category.newItemAmount) return;
const tempId = `temp-${Date.now()}-${Math.random()}`; // temporärt ID
const newItem = {
id: tempId,
name: category.newItemName,
amount: parseFloat(category.newItemAmount),
isExpense: true,
includeInSummary: true,
budgetCategoryId: category.id
};
category.items.push(newItem);
category.newItemName = "";
category.newItemAmount = "";
};
$scope.deleteItem = function (category, item) {
console.log("Försöker ta bort:", item);
if (!item.id || item.id.toString().startsWith("temp-")) {
// Ta bort direkt om det är ett nytt (osparat) item
category.items = category.items.filter(i => i !== item);
$scope.showToast("Ospard post togs bort");
return;
}
if (!confirm("Vill du ta bort denna post?")) return;
$http.delete(`/api/budget/item/${item.id}`)
.then(() => {
category.items = category.items.filter(i => i.id !== item.id);
$scope.showToast("Post borttagen!");
})
.catch(err => {
console.error("Fel vid borttagning av item:", err);
$scope.showToast("Kunde inte ta bort posten", true);
});
};
$scope.handleItemDrop = function (event, data, targetCategory) {
console.log("Item drop received:", data, "to", targetCategory);
// Hitta källkategorin
const sourceCat = $scope.budget.categories.find(c => c.id === data.fromCategoryId);
if (!sourceCat) return;
const itemIndex = sourceCat.items.findIndex(i => i.id === data.itemId);
if (itemIndex === -1) return;
const [movedItem] = sourceCat.items.splice(itemIndex, 1);
targetCategory.items.push(movedItem);
$scope.$applyAsync();
};
$scope.handleItemPreciseDrop = function (data, targetCategory, targetIndex) {
console.log("Precise drop at index", targetIndex);
const sourceCat = $scope.budget.categories.find(c => c.id === data.fromCategoryId);
if (!sourceCat) return;
const itemIndex = sourceCat.items.findIndex(i => i.id === data.itemId);
if (itemIndex === -1) return;
const [movedItem] = sourceCat.items.splice(itemIndex, 1);
targetCategory.items.splice(targetIndex, 0, movedItem);
$scope.$applyAsync();
};
$scope.itemDefinitions = [];
$scope.loadItemDefinitions = function () {
return $http.get("/api/budget/definitions/items")
.then(res => {
console.log("Definitioner laddade:", res.data);
$scope.itemDefinitions = res.data || [];
});
};
$scope.getDefinitionName = function (item) {
if (!item || !item.budgetItemDefinitionId) return null;
const def = $scope.itemDefinitions.find(d => d.id === item.budgetItemDefinitionId);
return def?.name || null;
};
$scope.addPopupAbove = false;
$scope.openItemPopup = function ($event, category) {
const trigger = $event.currentTarget;
const rect = trigger.getBoundingClientRect();
$scope.addPopupData = {
category: category,
newItemType: "expense",
newItemDefinition: "",
newItemLabel: "",
newItemAmount: null
};
$scope.filteredDefinitions = [];
$scope.addPopupVisible = true;
// Vänta tills popup finns i DOM
setTimeout(() => {
const popup = document.querySelector('.add-item-popup');
if (!popup) return;
const popupHeight = popup.offsetHeight;
const margin = 6;
const spaceBelow = window.innerHeight - rect.bottom - margin;
const spaceAbove = rect.top - margin;
let top;
let showAbove = false;
if (spaceBelow >= popupHeight) {
top = rect.bottom + margin;
} else if (spaceAbove >= popupHeight) {
top = rect.top - popupHeight - margin;
showAbove = true;
} else {
// Får inte plats helt välj bästa plats och justera top
showAbove = spaceAbove > spaceBelow;
top = showAbove
? Math.max(0, rect.top - popupHeight - margin)
: rect.bottom + margin;
}
$scope.$apply(() => {
$scope.addPopupStyle = {
position: "fixed",
top: `${top}px`,
left: `${rect.left}px`
};
$scope.addPopupAbove = showAbove;
});
}, 0);
if (!$scope.itemDefinitions || $scope.itemDefinitions.length === 0) {
$scope.loadItemDefinitions();
}
};
$scope.addPopupVisible = false;
$scope.addPopupStyle = {};
$scope.addPopupData = {};
$scope.filteredDefinitions = [];
$scope.showDefinitionSuggestions = false;
$scope.updateDefinitionSuggestions = function () {
const term = $scope.addPopupData.newItemDefinition?.toLowerCase() || '';
console.log("Sökterm:", term);
$scope.filteredDefinitions = $scope.itemDefinitions.filter(d =>
d.Name && d.Name.toLowerCase().includes(term)
);
$scope.showDefinitionSuggestions = true;
};
$scope.selectDefinitionSuggestion = function (name) {
$scope.addPopupData.newItemDefinition = name;
$scope.filteredDefinitions = [];
$scope.showDefinitionSuggestions = false;
};
// För att inte stänga direkt vid klick
let suggestionBlurTimeout;
$scope.hideSuggestionsDelayed = function () {
suggestionBlurTimeout = setTimeout(() => {
$scope.$apply(() => {
$scope.showDefinitionSuggestions = false;
});
}, 200);
};
document.addEventListener('click', function (e) {
const popup = document.querySelector('.add-item-popup');
const isInsidePopup = popup?.contains(e.target);
const isButton = e.target.closest('.add-post-btn');
if (!isInsidePopup && !isButton) {
$scope.$apply(() => {
$scope.addPopupVisible = false;
$scope.filteredDefinitions = [];
});
}
});
$scope.goToBudgetList = function () {
$window.location.href = '/budget/list'; // den route som visar listan
};
$scope.addItemFromDefinition = function (cat) {
const definitionName = cat.newItemDefinition?.trim();
const label = cat.newItemLabel?.trim();
const amount = parseFloat(cat.newItemAmount);
if (!definitionName || isNaN(amount)) return;
const matched = $scope.itemDefinitions.find(d => d.name.toLowerCase() === definitionName.toLowerCase());
const isExpense = cat.newItemType === "expense";
const includeInSummary = cat.newItemType !== "saving";
const item = {
name: label || definitionName,
amount: amount,
isExpense: isExpense,
includeInSummary: includeInSummary,
budgetCategoryId: cat.id,
budgetItemDefinitionId: matched?.id || null
};
$http.post("/api/budget/item", item).then(res => {
item.id = res.data.id;
cat.items.push(item);
$scope.showToast("Post tillagd!");
cat.addingItem = false;
// Om det var ny definition hämta listan på nytt
if (!matched) $scope.loadItemDefinitions();
});
};
$scope.addItemFromPopup = function () {
const cat = $scope.addPopupData.category;
const def = $scope.addPopupData.newItemDefinition?.trim();
const label = $scope.addPopupData.newItemLabel?.trim();
const amount = parseFloat($scope.addPopupData.newItemAmount);
const type = $scope.addPopupData.newItemType;
if (!def || isNaN(amount)) return;
const matched = $scope.itemDefinitions.find(d => d.Name && d.Name.toLowerCase() === def.toLowerCase());
const item = {
name: label || def,
amount: amount,
isExpense: type === "expense",
includeInSummary: type !== "saving",
budgetCategoryId: cat.id,
budgetItemDefinitionId: matched?.Id || null
};
$http.post("/api/budget/item", item).then(res => {
item.id = res.data.id;
cat.items.push(item);
$scope.showToast("Post tillagd!");
$scope.addPopupVisible = false;
if (!matched) $scope.loadItemDefinitions();
});
};
$scope.isExpenseCategory = function (cat) {
return cat.items.some(i => i.isExpense);
};
$scope.createEmptyBudget = function () {
if (!$scope.budget) {
$scope.showToast("Ogiltig budget.");
return;
}
let payload = {};
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.", true);
});
};
$scope.drawCategoryChart = function () {
const ctx = document.getElementById("expenseChart");
if (!ctx || !$scope.budget?.categories) return;
const labels = [];
const data = [];
const colors = [];
$scope.budget.categories.forEach(cat => {
const sum = cat.items
.filter(i => i.isExpense)
.reduce((acc, i) => acc + i.amount, 0);
if (sum > 0) {
labels.push(cat.name);
data.push(sum);
colors.push(cat.color || "#94a3b8");
}
});
const total = data.reduce((a, b) => a + b, 0);
const topIndex = data.indexOf(Math.max(...data));
$scope.topExpenseCategory = topIndex !== -1 ? {
name: labels[topIndex],
value: data[topIndex],
percent: (data[topIndex] / total) * 100
} : null;
if (window.expenseChart && typeof window.expenseChart.destroy === 'function') {
window.expenseChart.destroy();
}
window.expenseChart = new Chart(ctx, {
type: 'pie',
data: {
labels: labels,
datasets: [{
data: data,
backgroundColor: colors
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => `${ctx.label}: ${ctx.formattedValue} kr`
}
}
}
}
});
};
// Import
$scope.importing = false;
$scope.importText = '';
$scope.importPreview = [];
$scope.importTargetCategory = null;
$scope.openImportModule = function () {
// Välj första redigerbara kategori som standard (eller be om val senare)
const editable = $scope.budget.categories.find(c => c.editing);
if (!editable) {
alert("Redigera en kategori först!");
return;
}
$scope.importTargetCategory = editable;
$scope.importText = '';
$scope.importPreview = [];
$scope.importing = true;
};
$scope.cancelImport = function () {
$scope.importing = false;
$scope.importText = '';
$scope.importPreview = [];
};
$scope.parseImportText = function () {
const lines = $scope.importText.trim().split('\n');
$scope.importPreview = lines.map(l => {
const parts = l.trim().match(/^(.+?)\s+(-?\d+(?:[,.]\d+)?)/);
return {
name: parts?.[1] || '',
amount: parseFloat((parts?.[2] || '0').replace(',', '.'))
};
}).filter(item => item.name && !isNaN(item.amount));
};
$scope.applyImport = function () {
const cat = $scope.importTargetCategory;
$scope.importPreview.forEach(p => {
cat.items.push({
id: -1 * Math.floor(Math.random() * 1000000), // temporärt ID
name: p.name,
amount: p.amount,
isExpense: true,
includeInSummary: true,
paymentStatus: 0
});
});
$scope.importing = false;
$scope.importText = '';
$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;
$scope.loadItemDefinitions().then(() => {
$scope.loadBudget();
});
});