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 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 accounts, List transactions, DateTime startDate, DateTime endDate) { var today = DateTime.Today; var labels = new List(); var datasets = new List(); var dayTransactionLabels = new List>(); 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(); var runningBalance = account.CurrentBalance; if (startDate.Date <= today) { foreach (var t in accountTransactions .Where(t => t.Date.Date >= startDate.Date && t.Date.Date <= today) .OrderByDescending(t => t.Date)) { runningBalance -= t.Type == TransactionType.Income ? t.Amount : -t.Amount; } } else { // Future-only window: project forward from CurrentBalance using only unpaid transactions foreach (var t in accountTransactions .Where(t => t.Date.Date > today && t.Date.Date < startDate.Date && !t.IsPaid) .OrderBy(t => t.Date)) { runningBalance += 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 }; } }