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:
@@ -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>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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; } = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user