Feature: Add transaction multi-select for batch AI review
Add selection UI to Transactions page: - Add checkbox column for selecting transactions - Add sticky selection bar showing selected count - Add Select All / Clear Selection controls - Wire up form to send selected IDs to AICategorizePreview page - Preserve row click navigation for unselected areas Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,27 +133,53 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Selection Action Bar (hidden by default) -->
|
||||||
|
<div id="selectionBar" class="alert alert-primary d-none sticky-top" style="top: 10px; z-index: 1020;">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<strong><span id="selectedCount">0</span> transaction(s) selected</strong>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="selectAllVisible()">Select All Visible</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSelection()">Clear Selection</button>
|
||||||
|
<form id="aiReviewForm" method="post" asp-page="/AICategorizePreview" asp-page-handler="StoreIds" class="d-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<button type="submit" class="btn btn-sm btn-success">
|
||||||
|
AI Review Categories
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Transactions Table -->
|
<!-- Transactions Table -->
|
||||||
@if (Model.Transactions.Any())
|
@if (Model.Transactions.Any())
|
||||||
{
|
{
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.Category))
|
<div>
|
||||||
{
|
@if (!string.IsNullOrWhiteSpace(Model.Category))
|
||||||
<strong>@Model.Category</strong>
|
{
|
||||||
<span class="text-muted">- @Model.Stats.Count transactions</span>
|
<strong>@Model.Category</strong>
|
||||||
}
|
<span class="text-muted">- @Model.Stats.Count transactions</span>
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
<strong>All Transactions</strong>
|
{
|
||||||
<span class="text-muted">- @Model.Stats.Count total</span>
|
<strong>All Transactions</strong>
|
||||||
}
|
<span class="text-muted">- @Model.Stats.Count total</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="selectAllCheckbox" onchange="toggleSelectAll(this.checked)">
|
||||||
|
<label class="form-check-label small" for="selectAllCheckbox">Select all on page</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover table-sm mb-0">
|
<table class="table table-hover table-sm mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 40px;"></th>
|
||||||
<th style="width: 70px;">ID</th>
|
<th style="width: 70px;">ID</th>
|
||||||
<th style="width: 110px;">Date</th>
|
<th style="width: 110px;">Date</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@@ -166,12 +192,16 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var t in Model.Transactions)
|
@foreach (var t in Model.Transactions)
|
||||||
{
|
{
|
||||||
<tr style="cursor: pointer;" title="Open details" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
<tr>
|
||||||
<td class="small text-muted">#@t.Id</td>
|
<td onclick="event.stopPropagation();">
|
||||||
<td>@t.Date.ToString("yyyy-MM-dd")</td>
|
<input type="checkbox" class="form-check-input txn-checkbox"
|
||||||
<td>
|
value="@t.Id" onchange="updateSelection()">
|
||||||
|
</td>
|
||||||
|
<td class="small text-muted" style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">#@t.Id</td>
|
||||||
|
<td style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">@t.Date.ToString("yyyy-MM-dd")</td>
|
||||||
|
<td style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<a asp-page="/EditTransaction" asp-route-id="@t.Id" class="text-decoration-none text-body">@t.Name</a>
|
<span class="text-body">@t.Name</span>
|
||||||
@if (t.ReceiptCount > 0)
|
@if (t.ReceiptCount > 0)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success" title="@t.ReceiptCount receipt(s) attached">
|
<span class="badge bg-success" title="@t.ReceiptCount receipt(s) attached">
|
||||||
@@ -187,11 +217,11 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-truncate" style="max-width:320px">@t.Memo</td>
|
<td class="text-truncate" style="max-width:320px; cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">@t.Memo</td>
|
||||||
<td class="text-end @(t.Amount >= 0 ? "text-success" : "")">
|
<td class="text-end @(t.Amount >= 0 ? "text-success" : "")" style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||||
@t.Amount.ToString("C")
|
@t.Amount.ToString("C")
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||||
@if (string.IsNullOrWhiteSpace(t.Category))
|
@if (string.IsNullOrWhiteSpace(t.Category))
|
||||||
{
|
{
|
||||||
<span class="text-muted">(uncategorized)</span>
|
<span class="text-muted">(uncategorized)</span>
|
||||||
@@ -201,10 +231,9 @@
|
|||||||
@t.Category
|
@t.Category
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td class="small">
|
<td class="small" style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||||
@t.CardLabel
|
@t.CardLabel
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -340,6 +369,56 @@ else
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// Transaction selection handling
|
||||||
|
function updateSelection() {
|
||||||
|
const checkboxes = document.querySelectorAll('.txn-checkbox:checked');
|
||||||
|
const count = checkboxes.length;
|
||||||
|
const selectionBar = document.getElementById('selectionBar');
|
||||||
|
const selectedCount = document.getElementById('selectedCount');
|
||||||
|
const form = document.getElementById('aiReviewForm');
|
||||||
|
|
||||||
|
selectedCount.textContent = count;
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
selectionBar.classList.remove('d-none');
|
||||||
|
// Update form with selected IDs
|
||||||
|
const existingInputs = form.querySelectorAll('input[name="transactionIds"]');
|
||||||
|
existingInputs.forEach(input => input.remove());
|
||||||
|
|
||||||
|
checkboxes.forEach(cb => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'hidden';
|
||||||
|
input.name = 'transactionIds';
|
||||||
|
input.value = cb.value;
|
||||||
|
form.appendChild(input);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectionBar.classList.add('d-none');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update select all checkbox state
|
||||||
|
const allCheckboxes = document.querySelectorAll('.txn-checkbox');
|
||||||
|
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
|
||||||
|
selectAllCheckbox.checked = allCheckboxes.length > 0 && checkboxes.length === allCheckboxes.length;
|
||||||
|
selectAllCheckbox.indeterminate = checkboxes.length > 0 && checkboxes.length < allCheckboxes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelectAll(checked) {
|
||||||
|
document.querySelectorAll('.txn-checkbox').forEach(cb => cb.checked = checked);
|
||||||
|
updateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllVisible() {
|
||||||
|
document.querySelectorAll('.txn-checkbox').forEach(cb => cb.checked = true);
|
||||||
|
updateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
document.querySelectorAll('.txn-checkbox').forEach(cb => cb.checked = false);
|
||||||
|
document.getElementById('selectAllCheckbox').checked = false;
|
||||||
|
updateSelection();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user