Receipts: fix manual mapping (mirror manual ID, submit guard), allow remapping, and add clearer duplicate message; add Edit link in map modal and show mapped ID link

Co-authored-by:Codex-CLI-Agent
codex-cli@users.noreply.local
This commit is contained in:
AJ
2025-10-19 00:07:45 -04:00
parent eb31039bc8
commit bfe9ee5f08
3 changed files with 68 additions and 12 deletions

View File

@@ -212,6 +212,7 @@
<div class="text-muted">
@r.TransactionDate?.ToString("yyyy-MM-dd") - @r.TransactionAmount?.ToString("C")
</div>
<div class="text-muted"><a asp-page="/EditTransaction" asp-route-id="@r.TransactionId" target="_blank">ID #@r.TransactionId</a></div>
</div>
}
else
@@ -266,8 +267,9 @@
<h5 class="modal-title" id="mapModalLabel@(r.Id)">Map Receipt to Transaction</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" asp-page-handler="MapToTransaction" asp-route-receiptId="@r.Id">
<form method="post" asp-page-handler="MapToTransaction" asp-route-receiptId="@r.Id" data-mapform="1">
<div class="modal-body">
<input type="hidden" id="transactionId@(r.Id)" name="transactionId" />
<div class="mb-3">
<label class="form-label fw-bold">Receipt: @r.FileName</label>
@if (!string.IsNullOrWhiteSpace(r.Merchant) || r.ReceiptDate.HasValue || r.DueDate.HasValue || r.Total.HasValue)
@@ -316,7 +318,7 @@
}
Rows highlighted in <span class="badge bg-success">green</span> have matching amounts (within ±2%). Only showing transactions within ±10% of receipt total.
</div>
<input type="hidden" id="transactionId@(r.Id)" name="transactionId" required />
<div id="transactionListContainer@(r.Id)" style="max-height: 400px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 0.25rem;">
<table class="table table-sm table-hover mb-0" style="font-size: 0.85rem;">
<thead style="position: sticky; top: 0; background-color: white; z-index: 1;">
@@ -327,6 +329,7 @@
<th>Name</th>
<th style="width: 150px;">Merchant</th>
<th style="width: 100px;">Payment</th>
<th style="width: 70px;">Edit</th>
</tr>
</thead>
<tbody>
@@ -353,6 +356,7 @@
<td>@txn.Name</td>
<td>@(txn.MerchantName ?? "-")</td>
<td class="small">@txn.PaymentMethod</td>
<td><a asp-page="/EditTransaction" asp-route-id="@txn.Id" target="_blank" class="btn btn-sm btn-outline-secondary">Open</a></td>
</tr>
}
</tbody>
@@ -398,7 +402,7 @@
</div>
<div class="mb-3">
<label for="manualTransactionId@(r.Id)" class="form-label">Or Enter Transaction ID Manually</label>
<input type="number" class="form-control" id="manualTransactionId@(r.Id)" placeholder="Enter transaction ID..."
<input type="number" class="form-control" id="manualTransactionId@(r.Id)" oninput="document.getElementById('transactionId@(r.Id)').value = this.value" placeholder="Enter transaction ID..."
onchange="document.getElementById('transactionId@(r.Id)').value = this.value" />
</div>
</div>
@@ -411,3 +415,22 @@
</div>
</div>
}
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('form[data-mapform="1"]').forEach(function(form){
form.addEventListener('submit', function(e){
var hidden = form.querySelector('input[type="hidden"][name="transactionId"]');
var manual = form.querySelector('input[id^="manualTransactionId"]');
if (manual && manual.value) { hidden.value = manual.value; }
if (!hidden || !hidden.value) {
e.preventDefault();
alert('Please select a transaction or enter an ID.');
}
});
});
});
</script>

View File

@@ -140,6 +140,41 @@ namespace MoneyMap.Pages
public async Task<IActionResult> OnPostMapToTransactionAsync(long receiptId, long transactionId)
{
if (transactionId <= 0)
{
Message = "Please select a transaction or enter a valid ID.";
IsSuccess = false;
return RedirectToPage();
}
var receipt = await _db.Receipts.FirstOrDefaultAsync(r => r.Id == receiptId);
if (receipt == null)
{
Message = $"Receipt not found (ID {receiptId}).";
IsSuccess = false;
return RedirectToPage();
}
var transactionExists = await _db.Transactions.AnyAsync(t => t.Id == transactionId);
if (!transactionExists)
{
Message = $"Transaction not found (ID {transactionId}).";
IsSuccess = false;
return RedirectToPage();
}
// Friendly duplicate check: same file already mapped to this transaction
if (!string.IsNullOrWhiteSpace(receipt.FileHashSha256))
{
var duplicateExists = await _db.Receipts.AnyAsync(r => r.Id != receiptId && r.TransactionId == transactionId && r.FileHashSha256 == receipt.FileHashSha256);
if (duplicateExists)
{
Message = "This transaction already has a receipt with the same file (duplicate prevented).";
IsSuccess = false;
return RedirectToPage();
}
}
var success = await _receiptManager.MapReceiptToTransactionAsync(receiptId, transactionId);
if (success)
@@ -393,3 +428,4 @@ namespace MoneyMap.Pages
}
}
}

View File

@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
@@ -206,12 +206,9 @@ namespace MoneyMap.Services
if (transaction == null)
return false;
// Check if this receipt is already mapped to another transaction
if (receipt.TransactionId.HasValue && receipt.TransactionId.Value != transactionId)
{
// Could return a more specific error, but for now just return false
return false;
}
// Allow remapping: simply update the TransactionId
if (receipt.TransactionId == transactionId)
return true;
receipt.TransactionId = transactionId;
await _db.SaveChangesAsync();
@@ -228,7 +225,7 @@ namespace MoneyMap.Services
var sanitized = new StringBuilder();
foreach (var c in fileName)
{
if (c == '®' || c == '™' || c == '©')
if (c == '®' || c == '™' || c == '©')
{
// Skip trademark/copyright symbols
continue;
@@ -313,4 +310,4 @@ namespace MoneyMap.Services
public string? TransactionName { get; set; }
public string Reason { get; set; } = "";
}
}
}