This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
108
Aberwyn/wwwroot/css/report.css
Normal file
108
Aberwyn/wwwroot/css/report.css
Normal 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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
84
Aberwyn/wwwroot/js/report.js
Normal file
84
Aberwyn/wwwroot/js/report.js
Normal 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);
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user