From ef88d03c953aca1cfcc6259a192f0e5264dc1201 Mon Sep 17 00:00:00 2001 From: AJ Date: Sun, 19 Oct 2025 00:22:18 -0400 Subject: [PATCH] Dashboard: add graphs (Chart.js) with category donut and 30-day cash flow; add SpendTrends provider and DI registration --- MoneyMap/Pages/Index.cshtml | 63 +++++++++++++++++++++-- MoneyMap/Pages/Index.cshtml.cs | 94 +++++++++++++++++++++++++++++++++- MoneyMap/Program.cs | 3 +- 3 files changed, 152 insertions(+), 8 deletions(-) diff --git a/MoneyMap/Pages/Index.cshtml b/MoneyMap/Pages/Index.cshtml index 6d8efc7..8da2108 100644 --- a/MoneyMap/Pages/Index.cshtml +++ b/MoneyMap/Pages/Index.cshtml @@ -1,4 +1,4 @@ -@page +@page @model MoneyMap.Pages.IndexModel @{ ViewData["Title"] = "MoneyMap"; @@ -10,7 +10,7 @@
Transactions
@Model.Stats.TotalTransactions
-
Credits: @Model.Stats.Credits · Debits: @Model.Stats.Debits
+
Credits: @Model.Stats.Credits · Debits: @Model.Stats.Debits
@@ -22,7 +22,7 @@
@if (Model.Stats.Uncategorized > 0) { - View uncategorized → + View uncategorized ? } else { @@ -63,7 +63,7 @@
Top expense categories (last 90 days) - · excludes transfers + · excludes transfers
@@ -125,7 +125,7 @@ @if (t.ReceiptCount > 0) { - 📄 @t.ReceiptCount + ?? @t.ReceiptCount } @@ -152,3 +152,56 @@
+ + + + + diff --git a/MoneyMap/Pages/Index.cshtml.cs b/MoneyMap/Pages/Index.cshtml.cs index 7d52af5..5d7658d 100644 --- a/MoneyMap/Pages/Index.cshtml.cs +++ b/MoneyMap/Pages/Index.cshtml.cs @@ -21,6 +21,10 @@ namespace MoneyMap.Pages public DashboardStats Stats { get; set; } = new(); public List TopCategories { get; set; } = new(); public List Recent { get; set; } = new(); + public List TrendLabels { get; set; } = new(); + public List TrendDebitsAbs { get; set; } = new(); + public List TrendCredits { get; set; } = new(); + public List TrendNet { get; set; } = new(); public async Task OnGet() { @@ -29,6 +33,10 @@ namespace MoneyMap.Pages Stats = dashboard.Stats; TopCategories = dashboard.TopCategories; Recent = dashboard.RecentTransactions; + TrendLabels = dashboard.Trends.Labels; + TrendDebitsAbs = dashboard.Trends.DebitsAbs; + TrendCredits = dashboard.Trends.Credits; + TrendNet = dashboard.Trends.Net; } public record DashboardStats( @@ -74,17 +82,20 @@ namespace MoneyMap.Pages private readonly IDashboardStatsCalculator _statsCalculator; private readonly ITopCategoriesProvider _topCategoriesProvider; private readonly IRecentTransactionsProvider _recentTransactionsProvider; + private readonly ISpendTrendsProvider _spendTrendsProvider; public DashboardService( MoneyMapContext db, IDashboardStatsCalculator statsCalculator, ITopCategoriesProvider topCategoriesProvider, - IRecentTransactionsProvider recentTransactionsProvider) + IRecentTransactionsProvider recentTransactionsProvider, + ISpendTrendsProvider spendTrendsProvider) { _db = db; _statsCalculator = statsCalculator; _topCategoriesProvider = topCategoriesProvider; _recentTransactionsProvider = recentTransactionsProvider; + _spendTrendsProvider = spendTrendsProvider; } public async Task GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20) @@ -92,12 +103,14 @@ namespace MoneyMap.Pages var stats = await _statsCalculator.CalculateAsync(); var topCategories = await _topCategoriesProvider.GetTopCategoriesAsync(topCategoriesCount); var recent = await _recentTransactionsProvider.GetRecentTransactionsAsync(recentTransactionsCount); + var trends = await _spendTrendsProvider.GetDailyTrendsAsync(30); return new DashboardData { Stats = stats, TopCategories = topCategories, - RecentTransactions = recent + RecentTransactions = recent, + Trends = trends }; } } @@ -264,5 +277,82 @@ namespace MoneyMap.Pages public required IndexModel.DashboardStats Stats { get; init; } public required List TopCategories { get; init; } public required List RecentTransactions { get; init; } + public required SpendTrends Trends { get; init; } + } + + // ===== Spend Trends Provider ===== + + public interface ISpendTrendsProvider + { + Task GetDailyTrendsAsync(int lastDays = 30); + } + + public class SpendTrendsProvider : ISpendTrendsProvider + { + private readonly MoneyMapContext _db; + + public SpendTrendsProvider(MoneyMapContext db) + { + _db = db; + } + + public async Task GetDailyTrendsAsync(int lastDays = 30) + { + var today = DateTime.UtcNow.Date; + var since = today.AddDays(-(lastDays - 1)); + + var raw = await _db.Transactions + .Where(t => t.Date >= since) + .ExcludeTransfers() + .GroupBy(t => t.Date.Date) + .Select(g => new + { + Date = g.Key, + Debits = g.Where(t => t.Amount < 0).Sum(t => t.Amount), + Credits = g.Where(t => t.Amount > 0).Sum(t => t.Amount) + }) + .ToListAsync(); + + var dict = raw.ToDictionary(x => x.Date, x => x); + var labels = new List(); + var debitsAbs = new List(); + var credits = new List(); + var net = new List(); + + for (var d = since; d <= today; d = d.AddDays(1)) + { + labels.Add(d.ToString("yyyy-MM-dd")); + if (dict.TryGetValue(d, out var v)) + { + var debit = v.Debits; + var credit = v.Credits; + debitsAbs.Add(Math.Abs(debit)); + credits.Add(credit); + net.Add(credit + debit); + } + else + { + debitsAbs.Add(0); + credits.Add(0); + net.Add(0); + } + } + + return new SpendTrends + { + Labels = labels, + DebitsAbs = debitsAbs, + Credits = credits, + Net = net + }; + } + } + + public class SpendTrends + { + public List Labels { get; set; } = new(); + public List DebitsAbs { get; set; } = new(); + public List Credits { get; set; } = new(); + public List Net { get; set; } = new(); } } diff --git a/MoneyMap/Program.cs b/MoneyMap/Program.cs index 54c9b80..d5cc43b 100644 --- a/MoneyMap/Program.cs +++ b/MoneyMap/Program.cs @@ -31,6 +31,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); @@ -65,4 +66,4 @@ app.UseAuthorization(); app.MapRazorPages(); -app.Run(); \ No newline at end of file +app.Run();