From e3eb2dc7cbc96902936b75cac1348d41d586bbdc Mon Sep 17 00:00:00 2001 From: Elias Jansson Date: Mon, 9 Jun 2025 16:11:59 +0200 Subject: [PATCH] Budget page improvements --- Aberwyn/Models/MenuViewModel.cs | 40 +++--- Aberwyn/Views/Budget/Index.cshtml | 55 +++++++- Aberwyn/wwwroot/css/budget.css | 200 +++++++++++++++++++++++++----- Aberwyn/wwwroot/js/budget.js | 64 ++++++++++ 4 files changed, 301 insertions(+), 58 deletions(-) diff --git a/Aberwyn/Models/MenuViewModel.cs b/Aberwyn/Models/MenuViewModel.cs index d5b8299..335cd0e 100644 --- a/Aberwyn/Models/MenuViewModel.cs +++ b/Aberwyn/Models/MenuViewModel.cs @@ -60,31 +60,31 @@ public class WeeklyMenu public string LunchMealName { get; set; } public string DinnerMealName { get; set; } } -public class Meal -{ - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + public class Meal + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } - public string Name { get; set; } // Behåll som obligatorisk + public string Name { get; set; } // Behåll som obligatorisk - public string? Description { get; set; } - public string? ProteinType { get; set; } - public string? Category { get; set; } - public string? CarbType { get; set; } - public string? RecipeUrl { get; set; } - public string? ImageUrl { get; set; } + public string? Description { get; set; } + public string? ProteinType { get; set; } + public string? Category { get; set; } + public string? CarbType { get; set; } + public string? RecipeUrl { get; set; } + public string? ImageUrl { get; set; } - public bool IsAvailable { get; set; } - public DateTime CreatedAt { get; set; } - public byte[]? ThumbnailData { get; set; } + public bool IsAvailable { get; set; } + public DateTime CreatedAt { get; set; } + public byte[]? ThumbnailData { get; set; } - public byte[]? ImageData { get; set; } - public string? ImageMimeType { get; set; } - public string? Instructions { get; set; } + public byte[]? ImageData { get; set; } + public string? ImageMimeType { get; set; } + public string? Instructions { get; set; } - public List Ingredients { get; set; } = new(); -} + public List Ingredients { get; set; } = new(); + } public class Ingredient { diff --git a/Aberwyn/Views/Budget/Index.cshtml b/Aberwyn/Views/Budget/Index.cshtml index 48586a8..303cf8b 100644 --- a/Aberwyn/Views/Budget/Index.cshtml +++ b/Aberwyn/Views/Budget/Index.cshtml @@ -34,13 +34,57 @@ -
-
Totalt inkomst
{{ getTotalIncome() | number:0 }}
-
Total utgift
({{ getTotalExpense() | number:0 }})
-
Sparande
{{ getTotalSaving() | number:0 }}
-
Pengar kvar
{{ getLeftover() | number:0 }}
+
+ +
+

Sammanställning

+
    +
  • 💰 Inkomst: {{ getTotalIncome() | number:0 }} kr
  • +
  • 💸 Utgift: {{ getTotalExpense() | number:0 }} kr
  • +
  • 🏦 Sparande: {{ getTotalSaving() | number:0 }} kr
  • +

    + 📈 Kvar: + + {{ getLeftover() | number:0 }} kr + +

    + +
+
+
    +
  • + {{ cat.name }} + {{ getCategorySum(cat) | number:0 }} kr +
  • +
+
+ + +
+
+
+

Utgifter

+ +

+ Totalt: {{ getTotalExpense() | number:0 }} kr +

+ +

+ Största kategori:
+ {{ topExpenseCategory.name }} ({{ topExpenseCategory.percent | number:1 }}%) +

+
+ +
+ +
+
+
+ + +
+ diff --git a/Aberwyn/wwwroot/css/budget.css b/Aberwyn/wwwroot/css/budget.css index abf628c..75785f7 100644 --- a/Aberwyn/wwwroot/css/budget.css +++ b/Aberwyn/wwwroot/css/budget.css @@ -88,40 +88,7 @@ body { background-color: #cbd5e1; } -.summary-cards { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - margin: 16px 0 12px; -} -.summary-card { - background-color: var(--bg-card); - padding: 8px 10px; - border-radius: 10px; - text-align: center; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - font-weight: 600; - font-size: 13px; - min-width: 130px; -} - - .summary-card.income { - border-top: 3px solid var(--card-income); - } - - .summary-card.expense { - border-top: 3px solid var(--card-expense); - color: var(--card-expense); - } - - .summary-card.savings { - border-top: 3px solid var(--card-savings); - } - - .summary-card.leftover { - border-top: 3px solid var(--card-leftover); - } .budget-grid { display: grid; @@ -535,3 +502,170 @@ color: var(--btn-check); .suggestion-list li:hover { background: #334155; } + + + + +/* Summary */ +.budget-overview-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 8px 0 16px; +} + +.budget-summary-box, +.budget-chart-box { + flex: 1 1 250px; + background-color: var(--bg-card); + padding: 8px 12px; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.04); + min-width: 220px; +} + + .budget-summary-box h3, + .budget-chart-box h3 { + margin: 0 0 6px; + font-size: 13px; + font-weight: 600; + color: var(--text-main); + } + +.summary-list, +.category-summary-list { + list-style: none; + padding: 0; + margin: 0; +} + + .summary-list li, + .category-summary-list li { + display: flex; + justify-content: space-between; + padding: 1px 0; + font-size: 12px; + font-weight: 500; + line-height: 1.4; + color: var(--text-main); + } + + .summary-list li span:first-child { + font-weight: 600; + color: var(--text-sub); + } + + .category-summary-list li { + border-top: 1px dashed var(--border-color); + padding: 3px 0; + font-size: 11.5px; + } + +.highlight { + font-weight: 700; +} + + .highlight.leftover.plus { + color: #15803d; + } + + .highlight.leftover.minus { + color: #dc2626; + } + + + + +/* chart */ +.chart-area { + flex: 1; + display: flex; + align-items: center; + justify-content: center; +} + + .chart-legend h4 { + margin: 0 0 6px; + font-size: 13px; + font-weight: 600; + } + +.chart-area { + flex: 1; + min-width: 200px; + max-width: 340px; +} + +canvas { + max-width: 100%; + height: auto; +} + +@media (min-width: 769px) { + .budget-overview-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + align-items: start; + } + + .budget-chart-box { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + min-height: 300px; + text-align: center; + } + + .budget-chart-box .chart-row { + display: flex; + justify-content: center; + align-items: center; + } + + .budget-chart-box .chart-area { + flex: 1; + max-width: 420px; + width: 100%; + } + + .budget-chart-box canvas { + max-width: 100%; + height: auto; + } + +} + +@media (max-width: 768px) { + .budget-overview-row { + display: flex; + flex-direction: column; + gap: 12px; + } + + .budget-summary-box, + .budget-chart-box { + width: 100%; + } + + .chart-row { + flex-direction: column; + align-items: center; + } + + .chart-area { + max-width: 100%; + } + + .chart-legend { + width: 100%; + text-align: center; + padding-bottom: 10px; + } + + .chart-area canvas { + max-width: 240px; + height: auto; + } +} diff --git a/Aberwyn/wwwroot/js/budget.js b/Aberwyn/wwwroot/js/budget.js index 3203d86..8417002 100644 --- a/Aberwyn/wwwroot/js/budget.js +++ b/Aberwyn/wwwroot/js/budget.js @@ -5,6 +5,7 @@ app.controller('BudgetController', function ($scope, $http) { $scope.loading = false; $scope.error = null; $scope.menuOpen = false; + $scope.chartMode = "pie"; const today = new Date(); $scope.selectedYear = today.getFullYear(); @@ -140,6 +141,7 @@ app.controller('BudgetController', function ($scope, $http) { }) .finally(function () { $scope.loading = false; + setTimeout($scope.drawCategoryChart, 0); }); }; @@ -760,8 +762,70 @@ $scope.addItemFromDefinition = function (cat) { }); }; + $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(); }); + });