218 lines
7.8 KiB
C#
218 lines
7.8 KiB
C#
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;
|
|
}
|
|
} |