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>
|
||||
}
|
||||
|
||||
<!-- 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 -->
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Category))
|
||||
{
|
||||
<strong>@Model.Category</strong>
|
||||
<span class="text-muted">- @Model.Stats.Count transactions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<strong>All Transactions</strong>
|
||||
<span class="text-muted">- @Model.Stats.Count total</span>
|
||||
}
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Category))
|
||||
{
|
||||
<strong>@Model.Category</strong>
|
||||
<span class="text-muted">- @Model.Stats.Count transactions</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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 class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 40px;"></th>
|
||||
<th style="width: 70px;">ID</th>
|
||||
<th style="width: 110px;">Date</th>
|
||||
<th>Name</th>
|
||||
@@ -166,12 +192,16 @@
|
||||
<tbody>
|
||||
@foreach (var t in Model.Transactions)
|
||||
{
|
||||
<tr style="cursor: pointer;" title="Open details" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||
<td class="small text-muted">#@t.Id</td>
|
||||
<td>@t.Date.ToString("yyyy-MM-dd")</td>
|
||||
<td>
|
||||
<tr>
|
||||
<td onclick="event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input txn-checkbox"
|
||||
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">
|
||||
<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)
|
||||
{
|
||||
<span class="badge bg-success" title="@t.ReceiptCount receipt(s) attached">
|
||||
@@ -187,11 +217,11 @@
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-truncate" style="max-width:320px">@t.Memo</td>
|
||||
<td class="text-end @(t.Amount >= 0 ? "text-success" : "")">
|
||||
<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" : "")" style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||
@t.Amount.ToString("C")
|
||||
</td>
|
||||
<td>
|
||||
<td style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||
@if (string.IsNullOrWhiteSpace(t.Category))
|
||||
{
|
||||
<span class="text-muted">(uncategorized)</span>
|
||||
@@ -201,10 +231,9 @@
|
||||
@t.Category
|
||||
}
|
||||
</td>
|
||||
<td class="small">
|
||||
<td class="small" style="cursor: pointer;" onclick="window.location.href='@Url.Page("/EditTransaction", new { id = t.Id })'">
|
||||
@t.CardLabel
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
}
|
||||
</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>
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user