This commit is contained in:
rossmill76 committed 2026-03-02 11:50:08 +00:00
1 parent 2cf6128a85
commit 45a5e3fc5e
11 files changed
+428 -40

No files matched your search

+125 -18
View File
@@ -119,6 +119,8 @@ else
}
}
</select>
<input type="search" class="form-control form-control-sm" style="max-width: 220px;"
placeholder="Search..." @bind="searchQuery" @bind:event="oninput" @bind:after="OnSearchChanged" />
<div class="ms-auto d-flex align-items-center gap-2">
<label class="form-label mb-0 text-nowrap">Show:</label>
<select class="form-select form-select-sm" style="max-width: 90px;" @onchange="OnPageSizeChanged">
@@ -133,6 +135,9 @@ else
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 2rem;">
<input type="checkbox" class="form-check-input" checked="@IsAllPageSelected()" @onchange="ToggleSelectAll" />
</th>
<th role="button" class="user-select-none" @onclick='() => SetSort("date")'>Date @SortIndicator("date")</th>
<th role="button" class="user-select-none" @onclick='() => SetSort("description")'>Description @SortIndicator("description")</th>
<th>Account</th>
@@ -146,6 +151,9 @@ else
@foreach (var t in pagedTransactions)
{
<tr>
<td>
<input type="checkbox" class="form-check-input" checked="@selectedIds.Contains(t.Id)" @onchange="() => ToggleSelect(t.Id)" />
</td>
<td>@t.Date.ToString("MMM dd, yyyy")</td>
<td>@t.Description</td>
<td>
@@ -197,37 +205,75 @@ else
{
<div class="card-footer d-flex justify-content-between align-items-center">
<button class="btn btn-sm btn-outline-secondary" disabled="@(currentPage <= 1 ? true : (bool?)null)" @onclick="() => currentPage--">← Prev</button>
<small class="text-muted">Page @currentPage of @totalPages (@filteredTransactions.Count transactions)</small>
<div class="d-flex align-items-center gap-3">
@if (selectedIds.Count > 0)
{
<button class="btn btn-sm btn-danger" @onclick="ConfirmBulkDelete">
Delete @selectedIds.Count selected
</button>
}
<small class="text-muted">Page @currentPage of @totalPages (@filteredTransactions.Count transactions)</small>
</div>
<button class="btn btn-sm btn-outline-secondary" disabled="@(currentPage >= totalPages ? true : (bool?)null)" @onclick="() => currentPage++">Next →</button>
</div>
}
else
{
<div class="card-footer">
<div class="card-footer d-flex justify-content-between align-items-center">
<small class="text-muted">@filteredTransactions.Count transactions</small>
@if (selectedIds.Count > 0)
{
<button class="btn btn-sm btn-danger" @onclick="ConfirmBulkDelete">
Delete @selectedIds.Count selected
</button>
}
</div>
}
</div>
@if (showDeleteModal && pendingDeleteTransaction is not null)
@if (showDeleteModal)
{
<div class="modal d-block" tabindex="-1" style="background: rgba(0,0,0,.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Recurring Transaction</h5>
<h5 class="modal-title">@deleteModalTitle</h5>
<button type="button" class="btn-close" @onclick="CancelDelete"></button>
</div>
<div class="modal-body">
<p>
<strong>@pendingDeleteTransaction.Description</strong> is part of a recurring series.
</p>
<p>Would you like to delete just this transaction, or all future transactions in the series?</p>
@if (bulkDeleteCount > 0)
{
<p>Are you sure you want to delete <strong>@bulkDeleteCount</strong> selected transactions?</p>
<p class="text-danger mb-0">This action cannot be undone.</p>
}
else if (pendingDeleteTransaction is not null)
{
<p>Are you sure you want to delete <strong>@pendingDeleteTransaction.Description</strong>?</p>
@if (pendingDeleteIsRecurring)
{
<p>This transaction is part of a recurring series. Would you like to delete just this one, or all future transactions in the series?</p>
}
else
{
<p class="text-danger mb-0">This action cannot be undone.</p>
}
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @onclick="CancelDelete">Cancel</button>
<button type="button" class="btn btn-outline-danger" @onclick="DeleteSingle">This One Only</button>
<button type="button" class="btn btn-danger" @onclick="DeleteSeries">All Future in Series</button>
@if (bulkDeleteCount > 0)
{
<button type="button" class="btn btn-danger" @onclick="ExecuteBulkDelete">Delete All Selected</button>
}
else if (pendingDeleteIsRecurring)
{
<button type="button" class="btn btn-outline-danger" @onclick="DeleteSingle">This One Only</button>
<button type="button" class="btn btn-danger" @onclick="DeleteSeries">All Future in Series</button>
}
else
{
<button type="button" class="btn btn-danger" @onclick="DeleteSingle">Delete</button>
}
</div>
</div>
</div>
@@ -315,6 +361,7 @@ else
private TransactionForm formModel = new();
private string? userId;
private int? filterAccountId;
private string searchQuery = string.Empty;
// Pagination state
private int currentPage = 1;
@@ -338,6 +385,16 @@ else
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
@@ -361,6 +418,8 @@ else
currentPage = 1;
}
private void OnSearchChanged() => currentPage = 1;
private void OnPageSizeChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var size))
@@ -433,9 +492,37 @@ else
await LoadData();
}
// Selection state
private HashSet<int> selectedIds = [];
private void ToggleSelect(int id)
{
if (!selectedIds.Remove(id))
selectedIds.Add(id);
}
private bool IsAllPageSelected() => pagedTransactions.Count > 0 && pagedTransactions.All(t => selectedIds.Contains(t.Id));
private void ToggleSelectAll(ChangeEventArgs e)
{
if (e.Value is true)
{
foreach (var t in pagedTransactions)
selectedIds.Add(t.Id);
}
else
{
foreach (var t in pagedTransactions)
selectedIds.Remove(t.Id);
}
}
// Delete confirmation state
private bool showDeleteModal;
private Transaction? pendingDeleteTransaction;
private bool pendingDeleteIsRecurring;
private int bulkDeleteCount;
private string deleteModalTitle = "Delete Transaction";
// Edit modal state
private bool showEditModal;
@@ -481,14 +568,19 @@ else
private void ConfirmDelete(Transaction t)
{
pendingDeleteTransaction = t;
if (t.RecurrenceType != RecurrenceType.None || t.ParentTransactionId is not null)
{
showDeleteModal = true;
}
else
{
_ = DeleteSingle();
}
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()
@@ -496,6 +588,7 @@ else
if (userId is null || pendingDeleteTransaction is null) return;
showDeleteModal = false;
await TransactionService.DeleteTransactionAsync(pendingDeleteTransaction.Id, userId);
selectedIds.Remove(pendingDeleteTransaction.Id);
pendingDeleteTransaction = null;
await LoadData();
}
@@ -505,14 +598,28 @@ else
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)
{
await TransactionService.DeleteTransactionAsync(id, userId);
}
selectedIds.Clear();
await LoadData();
}
private void CancelDelete()
{
showDeleteModal = false;
pendingDeleteTransaction = null;
bulkDeleteCount = 0;
}
private sealed class TransactionForm : System.ComponentModel.DataAnnotations.IValidatableObject