Refactor. Add data layer

This commit is contained in:
rossmill76 committed 2026-03-06 11:05:01 +00:00
1 parent 3b7e4e6ec5
commit b6b794f0b4
82 files changed
+2930 -2811

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
using BudgetApp.Data.Entities;
using Microsoft.JSInterop;
namespace BudgetApp.Components.Pages;
public partial class Reports
{
private bool _loading = true;
private string? _userId;
private DateTime _dateFrom = DateTime.Today;
private DateTime _dateTo = DateTime.Today.AddDays(30);
private string? _preset = "30d";
private List<Account> _accounts = [];
private HashSet<int> _selectedAccountIds = [];
private List<Category> _categories = [];
private string _currencySymbol = "£";
private List<CategoryAmount> _categoryBreakdown = [];
private List<CategoryAmount> _incomeBreakdown = [];
private List<CategorySummaryRow> _categorySummary = [];
private List<string> _monthlyLabels = [];
private List<decimal> _monthlyIncome = [];
private List<decimal> _monthlyExpenses = [];
private static readonly string[] ChartColors =
[
"#4dc9f6", "#f67019", "#f53794", "#537bc4", "#acc236",
"#166a8f", "#00a950", "#58595b", "#8549ba", "#e6194b",
"#3cb44b", "#ffe119", "#4363d8", "#f58231", "#911eb4",
"#42d4f4", "#f032e6", "#bfef45", "#fabed4", "#469990"
];
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
_userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (_userId is null) return;
_accounts = await AccountService.GetAccountsAsync(_userId);
_categories = await CategoryService.GetCategoriesAsync(_userId);
_selectedAccountIds = _accounts.Select(a => a.Id).ToHashSet();
_currencySymbol = _accounts.FirstOrDefault()?.GetSymbol() ?? "£";
await LoadReport();
_loading = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || _loading) return;
await RenderCharts();
}
private async Task LoadReport()
{
if (_userId is null) return;
await SalaryService.MaterializeSalaryTransactionsAsync(_userId, _dateTo);
var transactions = await TransactionService.GetTransactionsInRangeAsync(_userId, _dateFrom, _dateTo);
var filtered = transactions
.Where(t => _selectedAccountIds.Contains(t.AccountId))
.ToList();
_categoryBreakdown = filtered
.Where(t => t.Type == TransactionType.Expense)
.GroupBy(t => t.CategoryId)
.Select(g =>
{
var cat = _categories.FirstOrDefault(c => c.Id == g.Key);
return new CategoryAmount
{
Name = cat?.Name ?? "Uncategorised",
Icon = cat?.Icon ?? "",
Total = g.Sum(t => t.Amount)
};
})
.OrderByDescending(c => c.Total)
.ToList();
_incomeBreakdown = filtered
.Where(t => t.Type == TransactionType.Income)
.GroupBy(t => t.CategoryId)
.Select(g =>
{
var cat = _categories.FirstOrDefault(c => c.Id == g.Key);
return new CategoryAmount
{
Name = cat?.Name ?? "Uncategorised",
Icon = cat?.Icon ?? "",
Total = g.Sum(t => t.Amount)
};
})
.OrderByDescending(c => c.Total)
.ToList();
var allCategoryKeys = filtered
.Select(t => t.CategoryId)
.Distinct()
.ToList();
_categorySummary = allCategoryKeys.Select(key =>
{
var cat = _categories.FirstOrDefault(c => c.Id == key);
var income = filtered.Where(t => t.CategoryId == key && t.Type == TransactionType.Income)
.Sum(t => t.Amount);
var expense = filtered.Where(t => t.CategoryId == key && t.Type == TransactionType.Expense)
.Sum(t => t.Amount);
return new CategorySummaryRow
{
Name = cat?.Name ?? "Uncategorised",
Icon = cat?.Icon ?? "",
Income = income,
Expense = expense
};
})
.OrderByDescending(r => r.Expense)
.ToList();
// Monthly data
var monthly = filtered
.GroupBy(t => new { t.Date.Year, t.Date.Month })
.OrderBy(g => g.Key.Year).ThenBy(g => g.Key.Month)
.ToList();
_monthlyLabels = monthly.Select(g => $"{g.Key.Year}-{g.Key.Month:D2}").ToList();
_monthlyIncome = monthly.Select(g => g.Where(t => t.Type == TransactionType.Income).Sum(t => t.Amount))
.ToList();
_monthlyExpenses = monthly.Select(g => g.Where(t => t.Type == TransactionType.Expense).Sum(t => t.Amount))
.ToList();
}
private async Task RenderCharts()
{
if (_categoryBreakdown.Count > 0)
{
var labels = _categoryBreakdown.Select(c => $"{c.Icon} {c.Name}".Trim()).ToArray();
var data = _categoryBreakdown.Select(c => c.Total).ToArray();
var colors = Enumerable.Range(0, _categoryBreakdown.Count).Select(i => ChartColors[i % ChartColors.Length])
.ToArray();
await Js.InvokeVoidAsync("chartInterop.createOrUpdatePieChart", "expensePieChart", labels, data, colors,
_currencySymbol, "doughnut");
}
if (_incomeBreakdown.Count > 0)
{
var labels = _incomeBreakdown.Select(c => $"{c.Icon} {c.Name}".Trim()).ToArray();
var data = _incomeBreakdown.Select(c => c.Total).ToArray();
var colors = Enumerable.Range(0, _incomeBreakdown.Count).Select(i => ChartColors[i % ChartColors.Length])
.ToArray();
await Js.InvokeVoidAsync("chartInterop.createOrUpdatePieChart", "incomePieChart", labels, data, colors,
_currencySymbol, "doughnut");
}
if (_monthlyLabels.Count > 0)
{
await Js.InvokeVoidAsync("chartInterop.createOrUpdateBarChart", "monthlyBarChart", _monthlyLabels.ToArray(),
_monthlyIncome.ToArray(), _monthlyExpenses.ToArray(), _currencySymbol);
}
}
private async Task SetPreset(int days)
{
_preset = days switch { 7 => "7d", 30 => "30d", 90 => "90d", 365 => "1y", _ => null };
_dateFrom = DateTime.Today;
_dateTo = DateTime.Today.AddDays(days);
await Refresh();
}
private async Task OnDateChanged()
{
_preset = null;
await Refresh();
}
private async Task ToggleAccount(int accountId)
{
if (!_selectedAccountIds.Remove(accountId))
_selectedAccountIds.Add(accountId);
await Refresh();
}
private async Task Refresh()
{
await LoadReport();
StateHasChanged();
await Task.Yield();
await RenderCharts();
}
public async ValueTask DisposeAsync()
{
try
{
await Js.InvokeVoidAsync("chartInterop.destroyChart", "expensePieChart");
await Js.InvokeVoidAsync("chartInterop.destroyChart", "incomePieChart");
await Js.InvokeVoidAsync("chartInterop.destroyChart", "monthlyBarChart");
}
catch (JSDisconnectedException)
{
}
}
private sealed class CategoryAmount
{
public string Name { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public decimal Total { get; set; }
}
private sealed class CategorySummaryRow
{
public string Name { get; set; } = string.Empty;
public string Icon { get; set; } = string.Empty;
public decimal Income { get; set; }
public decimal Expense { get; set; }
public decimal Net => Income - Expense;
}
}