using BudgetApp.Data.Entities; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; namespace BudgetApp.Components.Pages; public partial class Transactions { private List? _transactions; private List? _accountsList; private List? _categoriesList; private TransactionForm _formModel = new(); private string? _userId; private int? _filterAccountId; private string _searchQuery = string.Empty; private DateTime _windowStart = DateTime.Today; private DateTime _windowEnd = DateTime.Today.AddDays(30); private string? _windowPreset = "30d"; private int _currentPage = 1; private int _pageSize = 25; private static readonly int[] PageSizeOptions = [10, 25, 50, 100]; private int TotalPages => (int)Math.Ceiling(FilteredTransactions.Count / (double)_pageSize); private List PagedTransactions => FilteredTransactions .Skip((_currentPage - 1) * _pageSize) .Take(_pageSize) .ToList(); private string _sortColumn = "date"; private bool _sortAscending = true; private List FilteredTransactions { get { var list = (_transactions ?? []).Where(t => t.Date.Date >= _windowStart.Date && t.Date.Date <= _windowEnd.Date); if (_filterAccountId is not null) list = list.Where(t => t.AccountId == _filterAccountId); if (!string.IsNullOrWhiteSpace(_searchQuery)) { var q = _searchQuery.Trim(); list = list.Where(t => t.Description.Contains(q, StringComparison.OrdinalIgnoreCase) || t.Account.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || (t.Category?.Name?.Contains(q, StringComparison.OrdinalIgnoreCase) ?? false) || t.Amount.ToString("F2").Contains(q, StringComparison.OrdinalIgnoreCase)); } list = _sortColumn switch { "description" => _sortAscending ? list.OrderBy(t => t.Description, StringComparer.OrdinalIgnoreCase) : list.OrderByDescending(t => t.Description, StringComparer.OrdinalIgnoreCase), "amount" => _sortAscending ? list.OrderBy(t => t.Amount) : list.OrderByDescending(t => t.Amount), _ => _sortAscending ? list.OrderBy(t => t.Date) : list.OrderByDescending(t => t.Date), }; return list.ToList(); } } private void OnAccountFilterChanged(ChangeEventArgs e) { _filterAccountId = int.TryParse(e.Value?.ToString(), out var id) ? id : null; _currentPage = 1; } private void OnSearchChanged() => _currentPage = 1; private async Task SetWindow(int days) { _windowPreset = days switch { 7 => "7d", 30 => "30d", 90 => "90d", 365 => "1y", _ => null }; _windowStart = DateTime.Today; _windowEnd = DateTime.Today.AddDays(days); _currentPage = 1; await LoadData(); } private async Task OnWindowDateChanged() { _windowPreset = null; _currentPage = 1; await LoadData(); } private void OnPageSizeChanged(ChangeEventArgs e) { if (int.TryParse(e.Value?.ToString(), out var size)) { _pageSize = size; _currentPage = 1; } } private void SetSort(string column) { if (_sortColumn == column) _sortAscending = !_sortAscending; else { _sortColumn = column; _sortAscending = true; } _currentPage = 1; } private MarkupString SortIndicator(string column) { if (_sortColumn != column) return new MarkupString("⇅"); return new MarkupString(_sortAscending ? "▲" : "▼"); } protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); _userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; await LoadData(); } private async Task LoadData() { if (_userId is null) return; await SalaryService.MaterializeSalaryTransactionsAsync(_userId, _windowEnd); _transactions = await TransactionService.GetTransactionsAsync(_userId); _accountsList = await AccountService.GetAccountsAsync(_userId); _categoriesList = await CategoryService.GetCategoriesAsync(_userId); if (_accountsList.Count > 0 && _formModel.AccountId == 0) { _formModel.AccountId = _accountsList[0].Id; } } private async Task AddTransaction() { if (_userId is null) return; var transaction = new Transaction { Description = _formModel.Description, Amount = _formModel.Amount, Date = _formModel.Date, Type = _formModel.Type, AccountId = _formModel.AccountId, CategoryId = _formModel.CategoryId, RecurrenceType = _formModel.RecurrenceType, RecurrenceEndDate = _formModel.RecurrenceEndDate }; await TransactionService.CreateTransactionAsync(transaction); _formModel = new TransactionForm { AccountId = _accountsList?.FirstOrDefault()?.Id ?? 0 }; await LoadData(); } private readonly HashSet _selectedIds = []; private void ToggleSelect(int id) { var t = _transactions?.FirstOrDefault(x => x.Id == id); if (t?.SalaryId is not null) return; if (!_selectedIds.Remove(id)) _selectedIds.Add(id); } private bool IsAllPageSelected() { var selectable = PagedTransactions.Where(t => t.SalaryId is null).ToList(); return selectable.Count > 0 && selectable.All(t => _selectedIds.Contains(t.Id)); } private void ToggleSelectAll(ChangeEventArgs e) { var selectable = PagedTransactions.Where(t => t.SalaryId is null).ToList(); if (e.Value is true) { foreach (var t in selectable) _selectedIds.Add(t.Id); } else { foreach (var t in selectable) _selectedIds.Remove(t.Id); } } private bool _showDeleteModal; private Transaction? _pendingDeleteTransaction; private bool _pendingDeleteIsRecurring; private int _bulkDeleteCount; private string _deleteModalTitle = "Delete Transaction"; private bool _showEditModal; private int? _editingTransactionId; private Transaction? _editingTransaction; private bool _editIsPartOfSeries; private TransactionForm _editModel = new(); private void OpenEditModal(Transaction t) { _editingTransactionId = t.Id; _editingTransaction = t; _editIsPartOfSeries = t.RecurrenceType != RecurrenceType.None || t.ParentTransactionId is not null; _editModel = new TransactionForm { Description = t.Description, Amount = t.Amount, Date = t.Date, Type = t.Type, AccountId = t.AccountId, CategoryId = t.CategoryId, RecurrenceType = t.RecurrenceType, RecurrenceEndDate = t.RecurrenceEndDate }; _showEditModal = true; } private void CloseEditModal() { _showEditModal = false; _editingTransaction = null; } private async Task SaveEdit() { if (_userId is null || _editingTransactionId is null || _editingTransaction?.SalaryId is not null) return; await TransactionService.UpdateTransactionAsync( _editingTransactionId.Value, _userId, _editModel.Description, _editModel.Amount, _editModel.Date, _editModel.Type, _editModel.AccountId, _editModel.CategoryId, _editModel.RecurrenceType, _editModel.RecurrenceEndDate); _showEditModal = false; _editingTransactionId = null; await LoadData(); } private void ConfirmDelete(Transaction t) { _pendingDeleteTransaction = t; _pendingDeleteIsRecurring = t.RecurrenceType != RecurrenceType.None || t.ParentTransactionId is not null; _bulkDeleteCount = 0; _deleteModalTitle = _pendingDeleteIsRecurring ? "Delete Recurring Transaction" : "Delete Transaction"; _showDeleteModal = true; } private void ConfirmBulkDelete() { _pendingDeleteTransaction = null; _pendingDeleteIsRecurring = false; _bulkDeleteCount = _selectedIds.Count; _deleteModalTitle = $"Delete {_bulkDeleteCount} Transactions"; _showDeleteModal = true; } private async Task DeleteSingle() { if (_userId is null || _pendingDeleteTransaction is null) return; _showDeleteModal = false; await TransactionService.DeleteTransactionAsync(_pendingDeleteTransaction.Id, _userId); _selectedIds.Remove(_pendingDeleteTransaction.Id); _pendingDeleteTransaction = null; await LoadData(); } private async Task DeleteSeries() { if (_userId is null || _pendingDeleteTransaction is null) return; _showDeleteModal = false; await TransactionService.DeleteTransactionSeriesAsync(_pendingDeleteTransaction.Id, _userId); _selectedIds.Remove(_pendingDeleteTransaction.Id); _pendingDeleteTransaction = null; await LoadData(); } private async Task ExecuteBulkDelete() { if (_userId is null) return; _showDeleteModal = false; foreach (var id in _selectedIds.ToList()) { var t = _transactions?.FirstOrDefault(x => x.Id == id); if (t?.SalaryId is not null) continue; await TransactionService.DeleteTransactionAsync(id, _userId); } _selectedIds.Clear(); await LoadData(); } private void CancelDelete() { _showDeleteModal = false; _pendingDeleteTransaction = null; _bulkDeleteCount = 0; } private async Task TogglePaid(Transaction t) { if (_userId is null) return; await TransactionService.MarkAsPaidAsync(t.Id, _userId, !t.IsPaid); await LoadData(); } private async Task ExportCsv() { var sb = new System.Text.StringBuilder(); sb.AppendLine("Date,Description,Type,Amount,Account,Category,Recurrence"); foreach (var t in FilteredTransactions) { var category = t.Category?.Name ?? ""; var recurrence = t.RecurrenceType != RecurrenceType.None ? t.RecurrenceType.ToString() : ""; sb.AppendLine($"{t.Date:yyyy-MM-dd},{CsvEscape(t.Description)},{t.Type},{t.Amount:F2},{CsvEscape(t.Account.Name)},{CsvEscape(category)},{recurrence}"); } var filename = $"transactions_{DateTime.Today:yyyy-MM-dd}.csv"; await JS.InvokeVoidAsync("downloadCsv", filename, sb.ToString()); } private static string CsvEscape(string value) { if (value.Contains(',') || value.Contains('"') || value.Contains('\n')) return $"\"{value.Replace("\"", "\"\"\"")}\""; return value; } private sealed class TransactionForm { [System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Description is required")] [System.ComponentModel.DataAnnotations.MaxLength(200)] public string Description { get; set; } = string.Empty; [System.ComponentModel.DataAnnotations.Range(0.01, double.MaxValue, ErrorMessage = "Amount must be greater than zero")] public decimal Amount { get; set; } public DateTime Date { get; set; } = DateTime.Today; public TransactionType Type { get; set; } = TransactionType.Expense; public int AccountId { get; set; } public int? CategoryId { get; set; } public RecurrenceType RecurrenceType { get; set; } = RecurrenceType.None; public DateTime? RecurrenceEndDate { get; set; } public IEnumerable Validate( System.ComponentModel.DataAnnotations.ValidationContext validationContext) { if (RecurrenceType != RecurrenceType.None && RecurrenceEndDate.HasValue) { var maxEnd = Date.AddYears(1); if (RecurrenceEndDate.Value > maxEnd) { yield return new System.ComponentModel.DataAnnotations.ValidationResult( "Recurrence end date cannot be more than 1 year from the transaction date.", [nameof(RecurrenceEndDate)]); } if (RecurrenceEndDate.Value <= Date) { yield return new System.ComponentModel.DataAnnotations.ValidationResult( "Recurrence end date must be after the transaction date.", [nameof(RecurrenceEndDate)]); } } } } }