WIP
This commit is contained in:
1 parent
2cf6128a85
commit
45a5e3fc5e
11 files changed
+428
-40
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user