832 lines
29 KiB
JavaScript
832 lines
29 KiB
JavaScript
var app = angular.module('budgetApp', []);
|
||
console.log("budget.js loaded");
|
||
app.controller('BudgetController', function ($scope, $http) {
|
||
$scope.budget = null;
|
||
$scope.loading = false;
|
||
$scope.error = null;
|
||
$scope.menuOpen = false;
|
||
$scope.chartMode = "pie";
|
||
|
||
const today = new Date();
|
||
$scope.selectedYear = today.getFullYear();
|
||
$scope.selectedMonth = today.getMonth() + 1;
|
||
$scope.tempMonth = $scope.monthNames?.[today.getMonth()] || "";
|
||
$scope.tempYear = $scope.selectedYear;
|
||
|
||
$scope.monthNames = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
|
||
|
||
$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.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;
|
||
});
|
||
});
|
||
|
||
$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;
|
||
|
||
$http.get(`/api/budget/${$scope.selectedYear}/${$scope.selectedMonth}`)
|
||
.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,
|
||
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
|
||
};
|
||
}).sort((a, b) => a.order - b.order)
|
||
|
||
}));
|
||
|
||
$scope.budget = {
|
||
id: raw.Id,
|
||
year: raw.Year,
|
||
month: raw.Month,
|
||
categories: categories.sort((a, b) => a.order - b.order)
|
||
};
|
||
} else {
|
||
$scope.budget = { categories: [] };
|
||
}
|
||
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
|
||
})
|
||
.catch(function (error) {
|
||
if (error.status === 404) {
|
||
$scope.budget = { categories: [] };
|
||
} else {
|
||
$scope.error = "Kunde inte ladda budgetdata.";
|
||
$scope.showToast("Fel vid laddning av budgetdata", true);
|
||
console.error("Budget API error:", error);
|
||
}
|
||
})
|
||
.finally(function () {
|
||
$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,
|
||
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.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();
|
||
};
|
||
|
||
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.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
|
||
$scope.showMonthPicker = false;
|
||
$scope.loadBudget();
|
||
}
|
||
};
|
||
|
||
$scope.previousMonth = function () {
|
||
if ($scope.selectedMonth === 1) {
|
||
$scope.selectedMonth = 12;
|
||
$scope.selectedYear--;
|
||
} else {
|
||
$scope.selectedMonth--;
|
||
}
|
||
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth);
|
||
$scope.tempYear = $scope.selectedYear;
|
||
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
|
||
$scope.loadBudget();
|
||
};
|
||
|
||
$scope.nextMonth = function () {
|
||
if ($scope.selectedMonth === 12) {
|
||
$scope.selectedMonth = 1;
|
||
$scope.selectedYear++;
|
||
} else {
|
||
$scope.selectedMonth++;
|
||
}
|
||
$scope.tempMonth = $scope.getMonthName($scope.selectedMonth);
|
||
$scope.tempYear = $scope.selectedYear;
|
||
$scope.selectedMonthName = $scope.getMonthName($scope.selectedMonth);
|
||
$scope.loadBudget();
|
||
};
|
||
|
||
$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; // sist i listan
|
||
|
||
const newCategory = {
|
||
name: defaultName,
|
||
color: "#666666",
|
||
year: $scope.selectedYear,
|
||
month: $scope.selectedMonth,
|
||
order: newOrder
|
||
};
|
||
|
||
$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.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.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`
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
|
||
$scope.loadItemDefinitions().then(() => {
|
||
$scope.loadBudget();
|
||
});
|
||
|
||
});
|