Files
Budgeteer/BudgetApp/Services/DashboardService.cs
T
2026-03-06 11:24:20 +00:00

114 lines
4.8 KiB
C#

using BudgetApp.Data.Entities;
using BudgetApp.Data.Interfaces;
using BudgetApp.Services.Models;
namespace BudgetApp.Services;
public class DashboardService(ITransactionRepository transactionRepository, IAccountRepository accountRepository, ICategoryRepository categoryRepository)
{
public async Task<DashboardData> GetDashboardDataAsync(string userId, DateTime? from = null, DateTime? to = null)
{
await categoryRepository.EnsureDefaultCategoriesAsync(userId);
var windowEnd = to ?? DateTime.Today.AddDays(30);
await transactionRepository.MaterializeRecurringTransactionsAsync(userId, windowEnd);
var accounts = await accountRepository.GetAccountsAsync(userId);
var categories = await categoryRepository.GetCategoriesAsync(userId);
var windowStart = from ?? DateTime.Today.AddDays(-30);
var fetchStart = windowStart < DateTime.Today ? windowStart : DateTime.Today;
var fetchEnd = windowEnd > DateTime.Today ? windowEnd : DateTime.Today;
var allTransactions = await transactionRepository.GetTransactionsInRangeAsync(userId, fetchStart, fetchEnd);
var recent = allTransactions
.Where(t => (t.Date.Date >= windowStart.Date && t.Date.Date <= DateTime.Today)
|| (t.IsPaid && t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date))
.OrderByDescending(t => t.Date)
.ToList();
var upcoming = allTransactions
.Where(t => t.Date.Date > DateTime.Today && t.Date.Date <= windowEnd.Date && !t.IsPaid)
.OrderBy(t => t.Date)
.ToList();
var chartData = BuildChartData(accounts, allTransactions, windowStart, windowEnd);
return new DashboardData
{
Accounts = accounts,
Categories = categories,
RecentTransactions = recent,
UpcomingTransactions = upcoming,
ChartData = chartData,
CurrencySymbol = accounts.FirstOrDefault()?.GetSymbol() ?? "£"
};
}
internal static ChartData BuildChartData(List<Account> accounts, List<Transaction> transactions, DateTime startDate, DateTime endDate)
{
var today = DateTime.Today;
var labels = new List<string>();
var datasets = new List<ChartDataset>();
var dayTransactionLabels = new List<List<string>>();
for (var d = startDate; d <= endDate; d = d.AddDays(1))
{
labels.Add(d.ToString("MMM dd"));
// Paid future transactions are excluded — their effect is already in CurrentBalance
var dayTxns = transactions
.Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today))
.Select(t => $"{(t.Type == TransactionType.Income ? "+" : "-")}{t.Account.FormatAmount(t.Amount)} {t.Description} ({t.Account.Name})")
.ToList();
dayTransactionLabels.Add(dayTxns);
}
foreach (var account in accounts)
{
var accountTransactions = transactions
.Where(t => t.AccountId == account.Id)
.OrderBy(t => t.Date)
.ToList();
var dataPoints = new List<decimal>();
var runningBalance = account.CurrentBalance;
if (startDate.Date <= today)
{
runningBalance = accountTransactions.Where(t => t.Date.Date >= startDate.Date && t.Date.Date <= today)
.OrderByDescending(t => t.Date)
.Aggregate(runningBalance, (current, t) => current - (t.Type == TransactionType.Income ? t.Amount : -t.Amount));
}
else
{
// Future-only window: project forward from CurrentBalance using only unpaid transactions
runningBalance += accountTransactions.Where(t => t.Date.Date > today && t.Date.Date < startDate.Date && !t.IsPaid)
.OrderBy(t => t.Date)
.Sum(t => t.Type == TransactionType.Income ? t.Amount : -t.Amount);
}
for (var d = startDate; d <= endDate; d = d.AddDays(1))
{
var dayTxns = accountTransactions.Where(t => t.Date.Date == d.Date && !(t.IsPaid && t.Date.Date > today));
runningBalance += dayTxns.Sum(t => t.Type == TransactionType.Income ? t.Amount : -t.Amount);
dataPoints.Add(runningBalance);
}
datasets.Add(new ChartDataset
{
Label = account.Name,
Color = account.Color,
Data = dataPoints,
CurrencySymbol = account.GetSymbol()
});
}
return new ChartData
{
Labels = labels,
Datasets = datasets,
DayTransactionLabels = dayTransactionLabels
};
}
}