151 lines
5.9 KiB
C#
151 lines
5.9 KiB
C#
using BudgetApp.Data;
|
|
|
|
namespace BudgetApp.Services;
|
|
|
|
public class DashboardService(TransactionService transactionService, AccountService accountService, CategoryService categoryService)
|
|
{
|
|
private readonly TransactionService _transactionService = transactionService;
|
|
private readonly AccountService _accountService = accountService;
|
|
private readonly CategoryService _categoryService = categoryService;
|
|
|
|
public async Task<DashboardData> GetDashboardDataAsync(string userId, DateTime? from = null, DateTime? to = null)
|
|
{
|
|
await _categoryService.EnsureDefaultCategoriesAsync(userId);
|
|
var windowEnd = to ?? DateTime.Today.AddDays(30);
|
|
await _transactionService.MaterializeRecurringTransactionsAsync(userId, windowEnd);
|
|
|
|
var accounts = await _accountService.GetAccountsAsync(userId);
|
|
var categories = await _categoryService.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 _transactionService.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)
|
|
{
|
|
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));
|
|
foreach (var t in dayTxns)
|
|
{
|
|
runningBalance += 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
|
|
};
|
|
}
|
|
}
|
|
|
|
public class DashboardData
|
|
{
|
|
public List<Account> Accounts { get; set; } = [];
|
|
public List<Category> Categories { get; set; } = [];
|
|
public List<Transaction> RecentTransactions { get; set; } = [];
|
|
public List<Transaction> UpcomingTransactions { get; set; } = [];
|
|
public ChartData ChartData { get; set; } = new();
|
|
public string CurrencySymbol { get; set; } = "$";
|
|
}
|
|
|
|
public class ChartData
|
|
{
|
|
public List<string> Labels { get; set; } = [];
|
|
public List<ChartDataset> Datasets { get; set; } = [];
|
|
public List<List<string>> DayTransactionLabels { get; set; } = [];
|
|
}
|
|
|
|
public class ChartDataset
|
|
{
|
|
public string Label { get; set; } = string.Empty;
|
|
public string Color { get; set; } = "#1b6ec2";
|
|
public List<decimal> Data { get; set; } = [];
|
|
public string CurrencySymbol { get; set; } = "$";
|
|
}
|