Refactor: Extract dashboard services from Index page
Extract to separate files for better maintainability: - Models/Dashboard/DashboardModels.cs - Dashboard DTOs - Services/DashboardService.cs - All dashboard-related services - IDashboardService, IDashboardStatsCalculator - ITopCategoriesProvider, IRecentTransactionsProvider - ISpendTrendsProvider Reduces Index.cshtml.cs from 355 lines to 37 lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
62
MoneyMap/Models/Dashboard/DashboardModels.cs
Normal file
62
MoneyMap/Models/Dashboard/DashboardModels.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace MoneyMap.Models.Dashboard
|
||||
{
|
||||
/// <summary>
|
||||
/// Statistics displayed on the dashboard.
|
||||
/// </summary>
|
||||
public record DashboardStats(
|
||||
int TotalTransactions = 0,
|
||||
int Credits = 0,
|
||||
int Debits = 0,
|
||||
int Uncategorized = 0,
|
||||
int Receipts = 0,
|
||||
int Cards = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Row representing spending in a category.
|
||||
/// </summary>
|
||||
public class TopCategoryRow
|
||||
{
|
||||
public string Category { get; set; } = "";
|
||||
public decimal TotalSpend { get; set; }
|
||||
public int Count { get; set; }
|
||||
public decimal PercentageOfTotal { get; set; }
|
||||
public decimal AveragePerTransaction { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Row representing a recent transaction.
|
||||
/// </summary>
|
||||
public class RecentTransactionRow
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Memo { get; set; } = "";
|
||||
public decimal Amount { get; set; }
|
||||
public string Category { get; set; } = "";
|
||||
public string CardLabel { get; set; } = "";
|
||||
public int ReceiptCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spending trends over time.
|
||||
/// </summary>
|
||||
public class SpendTrends
|
||||
{
|
||||
public List<string> Labels { get; set; } = new();
|
||||
public List<decimal> DebitsAbs { get; set; } = new();
|
||||
public List<decimal> Credits { get; set; } = new();
|
||||
public List<decimal> Net { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Complete dashboard data package.
|
||||
/// </summary>
|
||||
public class DashboardData
|
||||
{
|
||||
public required DashboardStats Stats { get; init; }
|
||||
public required List<TopCategoryRow> TopCategories { get; init; }
|
||||
public required List<RecentTransactionRow> RecentTransactions { get; init; }
|
||||
public required SpendTrends Trends { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoneyMap.Data;
|
||||
using MoneyMap.Models;
|
||||
using MoneyMap.Models.Dashboard;
|
||||
using MoneyMap.Services;
|
||||
|
||||
namespace MoneyMap.Pages
|
||||
@@ -17,7 +15,7 @@ namespace MoneyMap.Pages
|
||||
|
||||
public DashboardStats Stats { get; set; } = new();
|
||||
public List<TopCategoryRow> TopCategories { get; set; } = new();
|
||||
public List<RecentTxnRow> Recent { get; set; } = new();
|
||||
public List<RecentTransactionRow> Recent { get; set; } = new();
|
||||
public List<string> TrendLabels { get; set; } = new();
|
||||
public List<decimal> TrendDebitsAbs { get; set; } = new();
|
||||
public List<decimal> TrendCredits { get; set; } = new();
|
||||
@@ -35,321 +33,5 @@ namespace MoneyMap.Pages
|
||||
TrendCredits = dashboard.Trends.Credits;
|
||||
TrendNet = dashboard.Trends.Net;
|
||||
}
|
||||
|
||||
public record DashboardStats(
|
||||
int TotalTransactions = 0,
|
||||
int Credits = 0,
|
||||
int Debits = 0,
|
||||
int Uncategorized = 0,
|
||||
int Receipts = 0,
|
||||
int Cards = 0);
|
||||
|
||||
public class TopCategoryRow
|
||||
{
|
||||
public string Category { get; set; } = "";
|
||||
public decimal TotalSpend { get; set; }
|
||||
public int Count { get; set; }
|
||||
public decimal PercentageOfTotal { get; set; }
|
||||
public decimal AveragePerTransaction { get; set; }
|
||||
}
|
||||
|
||||
public class RecentTxnRow
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Memo { get; set; } = "";
|
||||
public decimal Amount { get; set; }
|
||||
public string Category { get; set; } = "";
|
||||
public string CardLabel { get; set; } = "";
|
||||
public int ReceiptCount { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Service Layer =====
|
||||
|
||||
public interface IDashboardService
|
||||
{
|
||||
Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20);
|
||||
}
|
||||
|
||||
public class DashboardService : IDashboardService
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
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,
|
||||
ISpendTrendsProvider spendTrendsProvider)
|
||||
{
|
||||
_db = db;
|
||||
_statsCalculator = statsCalculator;
|
||||
_topCategoriesProvider = topCategoriesProvider;
|
||||
_recentTransactionsProvider = recentTransactionsProvider;
|
||||
_spendTrendsProvider = spendTrendsProvider;
|
||||
}
|
||||
|
||||
public async Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20)
|
||||
{
|
||||
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,
|
||||
Trends = trends
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Stats Calculator =====
|
||||
|
||||
public interface IDashboardStatsCalculator
|
||||
{
|
||||
Task<IndexModel.DashboardStats> CalculateAsync();
|
||||
}
|
||||
|
||||
public class DashboardStatsCalculator : IDashboardStatsCalculator
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public DashboardStatsCalculator(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<IndexModel.DashboardStats> CalculateAsync()
|
||||
{
|
||||
var transactionStats = await GetTransactionStatsAsync();
|
||||
var receiptsCount = await _db.Receipts.CountAsync();
|
||||
var cardsCount = await _db.Cards.CountAsync();
|
||||
|
||||
return new IndexModel.DashboardStats(
|
||||
transactionStats.Total,
|
||||
transactionStats.Credits,
|
||||
transactionStats.Debits,
|
||||
transactionStats.Uncategorized,
|
||||
receiptsCount,
|
||||
cardsCount
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<TransactionStats> GetTransactionStatsAsync()
|
||||
{
|
||||
var stats = await _db.Transactions
|
||||
.GroupBy(_ => 1)
|
||||
.Select(g => new TransactionStats
|
||||
{
|
||||
Total = g.Count(),
|
||||
Credits = g.Count(t => t.Amount > 0),
|
||||
Debits = g.Count(t => t.Amount < 0),
|
||||
Uncategorized = g.Count(t => t.Category == null || t.Category == "")
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return stats ?? new TransactionStats();
|
||||
}
|
||||
|
||||
private class TransactionStats
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Credits { get; set; }
|
||||
public int Debits { get; set; }
|
||||
public int Uncategorized { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Top Categories Provider =====
|
||||
|
||||
public interface ITopCategoriesProvider
|
||||
{
|
||||
Task<List<IndexModel.TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90);
|
||||
}
|
||||
|
||||
public class TopCategoriesProvider : ITopCategoriesProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public TopCategoriesProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<IndexModel.TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90)
|
||||
{
|
||||
var since = DateTime.UtcNow.Date.AddDays(-lastDays);
|
||||
|
||||
// Get all expense transactions for the period
|
||||
var expenseTransactions = await _db.Transactions
|
||||
.Where(t => t.Date >= since && t.Amount < 0)
|
||||
.ExcludeTransfers() // Exclude credit card payments and transfers
|
||||
.ToListAsync();
|
||||
|
||||
// Calculate total spend
|
||||
var totalSpend = expenseTransactions.Sum(t => -t.Amount);
|
||||
|
||||
// Group by category and calculate percentages
|
||||
var topCategories = expenseTransactions
|
||||
.GroupBy(t => t.Category ?? "")
|
||||
.Select(g => new IndexModel.TopCategoryRow
|
||||
{
|
||||
Category = g.Key,
|
||||
TotalSpend = g.Sum(x => -x.Amount),
|
||||
Count = g.Count(),
|
||||
PercentageOfTotal = totalSpend > 0 ? (g.Sum(x => -x.Amount) / totalSpend) * 100 : 0,
|
||||
AveragePerTransaction = g.Count() > 0 ? g.Sum(x => -x.Amount) / g.Count() : 0
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSpend)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topCategories;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Recent Transactions Provider =====
|
||||
|
||||
public interface IRecentTransactionsProvider
|
||||
{
|
||||
Task<List<IndexModel.RecentTxnRow>> GetRecentTransactionsAsync(int count = 20);
|
||||
}
|
||||
|
||||
public class RecentTransactionsProvider : IRecentTransactionsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public RecentTransactionsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<IndexModel.RecentTxnRow>> GetRecentTransactionsAsync(int count = 20)
|
||||
{
|
||||
return await _db.Transactions
|
||||
.Include(t => t.Card)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ThenByDescending(t => t.Id)
|
||||
.Select(t => new IndexModel.RecentTxnRow
|
||||
{
|
||||
Id = t.Id,
|
||||
Date = t.Date,
|
||||
Name = t.Name,
|
||||
Memo = t.Memo,
|
||||
Amount = t.Amount,
|
||||
Category = t.Category ?? "",
|
||||
CardLabel = t.PaymentMethodLabel,
|
||||
ReceiptCount = t.Receipts.Count()
|
||||
})
|
||||
.Take(count)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
private static string FormatCardLabel(Card? card, string? cardLast4)
|
||||
{
|
||||
if (card != null)
|
||||
return $"{card.Issuer} {card.Last4}";
|
||||
|
||||
if (string.IsNullOrEmpty(cardLast4))
|
||||
return "";
|
||||
|
||||
return $"<22><><EFBFBD><EFBFBD> {cardLast4}";
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Data Transfer Objects =====
|
||||
|
||||
public class DashboardData
|
||||
{
|
||||
public required IndexModel.DashboardStats Stats { get; init; }
|
||||
public required List<IndexModel.TopCategoryRow> TopCategories { get; init; }
|
||||
public required List<IndexModel.RecentTxnRow> RecentTransactions { get; init; }
|
||||
public required SpendTrends Trends { get; init; }
|
||||
}
|
||||
|
||||
// ===== Spend Trends Provider =====
|
||||
|
||||
public interface ISpendTrendsProvider
|
||||
{
|
||||
Task<SpendTrends> GetDailyTrendsAsync(int lastDays = 30);
|
||||
}
|
||||
|
||||
public class SpendTrendsProvider : ISpendTrendsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public SpendTrendsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<SpendTrends> 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<string>();
|
||||
var debitsAbs = new List<decimal>();
|
||||
var credits = new List<decimal>();
|
||||
var net = new List<decimal>();
|
||||
|
||||
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<string> Labels { get; set; } = new();
|
||||
public List<decimal> DebitsAbs { get; set; } = new();
|
||||
public List<decimal> Credits { get; set; } = new();
|
||||
public List<decimal> Net { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
263
MoneyMap/Services/DashboardService.cs
Normal file
263
MoneyMap/Services/DashboardService.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoneyMap.Data;
|
||||
using MoneyMap.Models.Dashboard;
|
||||
|
||||
namespace MoneyMap.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for retrieving dashboard data.
|
||||
/// </summary>
|
||||
public interface IDashboardService
|
||||
{
|
||||
Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20);
|
||||
}
|
||||
|
||||
public class DashboardService : IDashboardService
|
||||
{
|
||||
private readonly IDashboardStatsCalculator _statsCalculator;
|
||||
private readonly ITopCategoriesProvider _topCategoriesProvider;
|
||||
private readonly IRecentTransactionsProvider _recentTransactionsProvider;
|
||||
private readonly ISpendTrendsProvider _spendTrendsProvider;
|
||||
|
||||
public DashboardService(
|
||||
IDashboardStatsCalculator statsCalculator,
|
||||
ITopCategoriesProvider topCategoriesProvider,
|
||||
IRecentTransactionsProvider recentTransactionsProvider,
|
||||
ISpendTrendsProvider spendTrendsProvider)
|
||||
{
|
||||
_statsCalculator = statsCalculator;
|
||||
_topCategoriesProvider = topCategoriesProvider;
|
||||
_recentTransactionsProvider = recentTransactionsProvider;
|
||||
_spendTrendsProvider = spendTrendsProvider;
|
||||
}
|
||||
|
||||
public async Task<DashboardData> GetDashboardDataAsync(int topCategoriesCount = 8, int recentTransactionsCount = 20)
|
||||
{
|
||||
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,
|
||||
Trends = trends
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates dashboard statistics.
|
||||
/// </summary>
|
||||
public interface IDashboardStatsCalculator
|
||||
{
|
||||
Task<DashboardStats> CalculateAsync();
|
||||
}
|
||||
|
||||
public class DashboardStatsCalculator : IDashboardStatsCalculator
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public DashboardStatsCalculator(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<DashboardStats> CalculateAsync()
|
||||
{
|
||||
var transactionStats = await GetTransactionStatsAsync();
|
||||
var receiptsCount = await _db.Receipts.CountAsync();
|
||||
var cardsCount = await _db.Cards.CountAsync();
|
||||
|
||||
return new DashboardStats(
|
||||
transactionStats.Total,
|
||||
transactionStats.Credits,
|
||||
transactionStats.Debits,
|
||||
transactionStats.Uncategorized,
|
||||
receiptsCount,
|
||||
cardsCount
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<TransactionStatsInternal> GetTransactionStatsAsync()
|
||||
{
|
||||
var stats = await _db.Transactions
|
||||
.GroupBy(_ => 1)
|
||||
.Select(g => new TransactionStatsInternal
|
||||
{
|
||||
Total = g.Count(),
|
||||
Credits = g.Count(t => t.Amount > 0),
|
||||
Debits = g.Count(t => t.Amount < 0),
|
||||
Uncategorized = g.Count(t => t.Category == null || t.Category == "")
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
return stats ?? new TransactionStatsInternal();
|
||||
}
|
||||
|
||||
private class TransactionStatsInternal
|
||||
{
|
||||
public int Total { get; set; }
|
||||
public int Credits { get; set; }
|
||||
public int Debits { get; set; }
|
||||
public int Uncategorized { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides top spending categories.
|
||||
/// </summary>
|
||||
public interface ITopCategoriesProvider
|
||||
{
|
||||
Task<List<TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90);
|
||||
}
|
||||
|
||||
public class TopCategoriesProvider : ITopCategoriesProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public TopCategoriesProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<TopCategoryRow>> GetTopCategoriesAsync(int count = 8, int lastDays = 90)
|
||||
{
|
||||
var since = DateTime.UtcNow.Date.AddDays(-lastDays);
|
||||
|
||||
var expenseTransactions = await _db.Transactions
|
||||
.Where(t => t.Date >= since && t.Amount < 0)
|
||||
.ExcludeTransfers()
|
||||
.ToListAsync();
|
||||
|
||||
var totalSpend = expenseTransactions.Sum(t => -t.Amount);
|
||||
|
||||
var topCategories = expenseTransactions
|
||||
.GroupBy(t => t.Category ?? "")
|
||||
.Select(g => new TopCategoryRow
|
||||
{
|
||||
Category = g.Key,
|
||||
TotalSpend = g.Sum(x => -x.Amount),
|
||||
Count = g.Count(),
|
||||
PercentageOfTotal = totalSpend > 0 ? (g.Sum(x => -x.Amount) / totalSpend) * 100 : 0,
|
||||
AveragePerTransaction = g.Count() > 0 ? g.Sum(x => -x.Amount) / g.Count() : 0
|
||||
})
|
||||
.OrderByDescending(x => x.TotalSpend)
|
||||
.Take(count)
|
||||
.ToList();
|
||||
|
||||
return topCategories;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides recent transactions.
|
||||
/// </summary>
|
||||
public interface IRecentTransactionsProvider
|
||||
{
|
||||
Task<List<RecentTransactionRow>> GetRecentTransactionsAsync(int count = 20);
|
||||
}
|
||||
|
||||
public class RecentTransactionsProvider : IRecentTransactionsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public RecentTransactionsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<List<RecentTransactionRow>> GetRecentTransactionsAsync(int count = 20)
|
||||
{
|
||||
return await _db.Transactions
|
||||
.Include(t => t.Card)
|
||||
.OrderByDescending(t => t.Date)
|
||||
.ThenByDescending(t => t.Id)
|
||||
.Select(t => new RecentTransactionRow
|
||||
{
|
||||
Id = t.Id,
|
||||
Date = t.Date,
|
||||
Name = t.Name,
|
||||
Memo = t.Memo,
|
||||
Amount = t.Amount,
|
||||
Category = t.Category ?? "",
|
||||
CardLabel = t.PaymentMethodLabel,
|
||||
ReceiptCount = t.Receipts.Count()
|
||||
})
|
||||
.Take(count)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides spending trends over time.
|
||||
/// </summary>
|
||||
public interface ISpendTrendsProvider
|
||||
{
|
||||
Task<SpendTrends> GetDailyTrendsAsync(int lastDays = 30);
|
||||
}
|
||||
|
||||
public class SpendTrendsProvider : ISpendTrendsProvider
|
||||
{
|
||||
private readonly MoneyMapContext _db;
|
||||
|
||||
public SpendTrendsProvider(MoneyMapContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<SpendTrends> 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<string>();
|
||||
var debitsAbs = new List<decimal>();
|
||||
var credits = new List<decimal>();
|
||||
var net = new List<decimal>();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user