@page "/transactions" @attribute [Authorize] @inject TransactionService TransactionService @inject AccountService AccountService @inject CategoryService CategoryService @inject AuthenticationStateProvider AuthStateProvider @rendermode InteractiveServer Transactions

Transactions

Add Transaction
@if (accountsList is not null && accountsList.Count == 0) {
You need to create an account before adding transactions.
} else {
@if (accountsList is not null) { @foreach (var account in accountsList) { } }
@if (categoriesList is not null) { @foreach (var category in categoriesList) { } }
@if (formModel.RecurrenceType != RecurrenceType.None) {
}
}
@if (transactions is null) {

Loading...

} else if (transactions.Count == 0) {
No transactions yet.
} else {
@foreach (var t in pagedTransactions) { }
Date @SortIndicator("date") Description @SortIndicator("description") Account Category Recurrence Amount @SortIndicator("amount")
@t.Date.ToString("MMM dd, yyyy") @t.Description @t.Account.Name @if (t.Category is not null) { @if (!string.IsNullOrEmpty(t.Category.Icon)) { @t.Category.Icon } @t.Category.Name } else { — } @if (t.RecurrenceType != RecurrenceType.None) { 🔁 @t.RecurrenceType } else if (t.ParentTransactionId is not null) { 🔁 @(t.ParentTransaction?.RecurrenceType.ToString() ?? "Recurring") } @(t.Type == TransactionType.Income ? "+" : "-")@t.Account.FormatAmount(t.Amount)
@if (totalPages > 1) { } else { }
@if (showDeleteModal && pendingDeleteTransaction is not null) { } @if (showEditModal) { } } @code { private List? transactions; private List? accountsList; private List? categoriesList; private TransactionForm formModel = new(); private string? userId; private int? filterAccountId; // Pagination state 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(); // Sort state private string sortColumn = "date"; private bool sortAscending = true; private List filteredTransactions { get { var list = (transactions ?? []).Where(t => t.Date.Date >= DateTime.Today); if (filterAccountId is not null) list = list.Where(t => t.AccountId == filterAccountId); 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 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; 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(); } // Delete confirmation state private bool showDeleteModal; private Transaction? pendingDeleteTransaction; // Edit modal state private bool showEditModal; private int? editingTransactionId; private bool editIsPartOfSeries; private TransactionForm editModel = new(); private void OpenEditModal(Transaction t) { editingTransactionId = t.Id; 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; private async Task SaveEdit() { if (userId is null || editingTransactionId is 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; if (t.RecurrenceType != RecurrenceType.None || t.ParentTransactionId is not null) { showDeleteModal = true; } else { _ = DeleteSingle(); } } private async Task DeleteSingle() { if (userId is null || pendingDeleteTransaction is null) return; showDeleteModal = false; await TransactionService.DeleteTransactionAsync(pendingDeleteTransaction.Id, userId); pendingDeleteTransaction = null; await LoadData(); } private async Task DeleteSeries() { if (userId is null || pendingDeleteTransaction is null) return; showDeleteModal = false; await TransactionService.DeleteTransactionSeriesAsync(pendingDeleteTransaction.Id, userId); pendingDeleteTransaction = null; await LoadData(); } private void CancelDelete() { showDeleteModal = false; pendingDeleteTransaction = null; } private sealed class TransactionForm : System.ComponentModel.DataAnnotations.IValidatableObject { [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)]); } } } } }