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 _accounts = []; private HashSet _selectedAccountIds = []; private List _categories = []; private string _currencySymbol = "£"; private List _categoryBreakdown = []; private List _incomeBreakdown = []; private List _categorySummary = []; private List _monthlyLabels = []; private List _monthlyIncome = []; private List _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; } }