Budget complete!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Elias Jansson
2025-05-28 10:29:56 +02:00
parent 0f0eaad7b1
commit eed1ce166f
16 changed files with 1490 additions and 44 deletions

View File

@@ -149,6 +149,7 @@ body {
padding: 2px 8px;
display: flex;
flex-direction: column;
position: relative;
}
@@ -158,10 +159,6 @@ body {
}
.card-header {
border-radius: 12px 12px 0 0;
}
.total-row {
padding: 2px 8px;
font-weight: bold;
@@ -173,6 +170,7 @@ body {
}
.card-header {
border-radius: 12px 12px 0 0;
position: sticky;
top: 0;
z-index: 3; /* högre än total-row */
@@ -186,6 +184,13 @@ body {
flex-wrap: wrap;
gap: 6px;
}
.header-edit {
padding: 4px 8px;
border-radius: 6px;
border: 1px solid var(--border-color);
flex: 1;
min-width: 120px; /* om du vill att den ska vara större */
}
.card-header.income {
background-color: var(--card-income);
@@ -424,3 +429,109 @@ color: var(--btn-check);
pointer-events: none;
font-style: italic;
}
.add-item-popup {
position: absolute; /* ← Ändrat från fixed */
z-index: 9999;
background-color: #1F2C3C;
color: white;
border: 1px solid #444;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
padding: 14px;
width: 280px;
font-size: 14px;
margin-top: 6px;
}
.add-item-popup.above {
margin-top: unset;
margin-bottom: 6px;
}
.add-item-popup label {
display: block;
margin-top: 8px;
font-weight: 600;
font-size: 13px;
color: #cbd5e1;
}
.add-item-popup input,
.add-item-popup select {
width: 100%;
padding: 6px 8px;
border: none;
border-radius: 6px;
margin-top: 4px;
font-size: 13px;
box-sizing: border-box;
background-color: #334155;
color: white;
}
.add-item-popup input::placeholder {
color: #94a3b8;
}
.add-item-popup button {
margin-top: 12px;
margin-right: 8px;
padding: 6px 12px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s;
}
.add-item-popup button:hover {
background-color: #2563eb;
}
.add-post-btn {
margin: 10px 0 6px 0;
padding: 6px 10px;
background-color: #3b82f6;
color: white;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: background-color 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.add-post-btn:hover {
background-color: #2563eb;
}
.suggestion-list {
position: absolute;
background: #1F2C3C;
color: white;
border: 1px solid #555;
border-radius: 4px;
margin-top: 4px;
padding: 0;
list-style: none;
width: 100%;
max-height: 150px;
overflow-y: auto;
z-index: 10000; /* 👈 ovanför resten */
}
.suggestion-list li {
padding: 6px 10px;
cursor: pointer;
}
.suggestion-list li:hover {
background: #334155;
}

View File

@@ -0,0 +1,108 @@
/* report.css */
body {
font-family: 'Segoe UI', sans-serif;
background-color: #f9fafb;
color: #1e293b;
padding: 24px;
}
.report-page {
max-width: 1000px;
margin: auto;
}
h1 {
font-size: 28px;
margin-bottom: 20px;
}
.report-controls {
background: #ffffff;
border: 1px solid #e5e7eb;
padding: 16px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
margin-bottom: 24px;
}
.date-select label,
.definition-select label {
font-weight: 600;
margin-right: 8px;
display: inline-block;
margin-bottom: 8px;
}
.date-select select {
margin-right: 8px;
padding: 4px 6px;
border-radius: 6px;
border: 1px solid #cbd5e1;
font-size: 14px;
}
.definition-select {
margin-top: 16px;
}
.checkbox-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 6px 12px;
margin-top: 6px;
}
.btn-generate {
margin-top: 16px;
background-color: #3b82f6;
color: white;
border: none;
padding: 8px 16px;
font-size: 14px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-generate:hover {
background-color: #2563eb;
}
.report-results h2 {
font-size: 22px;
margin-bottom: 12px;
}
.report-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
font-size: 14px;
background-color: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
border-radius: 6px;
overflow: hidden;
}
.report-table th, .report-table td {
padding: 10px 8px;
border: 1px solid #e5e7eb;
text-align: right;
}
.report-table th {
background-color: #f1f5f9;
font-weight: 600;
text-align: center;
}
.report-table td:first-child,
.report-table td:nth-child(2) {
text-align: left;
}
canvas#reportChart {
width: 100% !important;
max-height: 400px;
margin-top: 24px;
}

View File

@@ -102,14 +102,20 @@ app.controller('BudgetController', function ($scope, $http) {
color: cat.Color,
editing: false,
allowDrag: false,
items: (cat.Items || []).map((item, index) => ({
id: item.Id,
name: item.Name,
amount: parseFloat(item.Amount),
isExpense: item.IsExpense === true,
includeInSummary: item.IncludeInSummary === true,
order: item.Order ?? index
})).sort((a, b) => a.order - b.order)
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 = {
@@ -165,6 +171,7 @@ app.controller('BudgetController', function ($scope, $http) {
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,
@@ -280,6 +287,19 @@ app.controller('BudgetController', function ($scope, $http) {
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) {
@@ -558,6 +578,190 @@ app.controller('BudgetController', function ($scope, $http) {
$scope.$applyAsync();
};
$scope.itemDefinitions = [];
$scope.loadBudget();
$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.loadItemDefinitions().then(() => {
$scope.loadBudget();
});
});

View File

@@ -0,0 +1,84 @@
var app = angular.module('reportApp', []);
app.controller('ReportController', function ($scope, $http) {
$scope.definitions = [];
$scope.results = [];
$scope.years = [];
$scope.months = [
{ value: 1, label: 'Januari' },
{ value: 2, label: 'Februari' },
{ value: 3, label: 'Mars' },
{ value: 4, label: 'April' },
{ value: 5, label: 'Maj' },
{ value: 6, label: 'Juni' },
{ value: 7, label: 'Juli' },
{ value: 8, label: 'Augusti' },
{ value: 9, label: 'September' },
{ value: 10, label: 'Oktober' },
{ value: 11, label: 'November' },
{ value: 12, label: 'December' }
];
$scope.init = function () {
const now = new Date();
$scope.endYear = now.getFullYear();
$scope.endMonth = now.getMonth() + 1;
$scope.startYear = $scope.endYear - 1;
$scope.startMonth = $scope.endMonth;
const baseYear = 2022;
const thisYear = new Date().getFullYear();
for (let y = baseYear; y <= thisYear + 1; y++) {
$scope.years.push(y);
}
$http.get('/api/budget/definitions/items')
.then(res => {
$scope.definitions = res.data.map(d => {
d.Selected = true;
return d;
});
});
};
$scope.monthName = function (month) {
const match = $scope.months.find(m => m.value === month);
return match ? match.label : month;
};
$scope.loadReport = function () {
const selectedDefs = $scope.definitions.filter(d => d.Selected);
if (selectedDefs.length === 0) {
alert("Välj minst en post att visa.");
return;
}
const payload = {
startYear: $scope.startYear,
startMonth: $scope.startMonth,
endYear: $scope.endYear,
endMonth: $scope.endMonth,
definitionIds: selectedDefs.map(d => d.Id)
};
$http.post('/api/report/report', payload)
.then(res => {
$scope.results = res.data;
$scope.activeDefinitions = selectedDefs;
})
.catch(err => {
console.error("Fel vid hämtning av rapport:", err);
alert("Kunde inte ladda rapporten.");
});
};
$scope.getAmount = function (row, defId) {
const match = row.Definitions.find(d => d.DefinitionId === defId);
return match ? match.TotalAmount : 0;
};
$scope.getRowTotal = function (row) {
return row.Definitions.reduce((sum, d) => sum + d.TotalAmount, 0);
};
});