1185 lines
41 KiB
JavaScript
1185 lines
41 KiB
JavaScript
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();
|
||
});
|
||
|
||
});
|