Convert duplicate warnings to blocking modal dialog

Changed duplicate detection from informational alert to a modal dialog
that requires user decision before proceeding with upload.

Upload Flow Changes:
1. Initial upload detects duplicates and uploads temporarily
2. If duplicates found:
   - Deletes the uploaded file
   - Stores warnings and filename in TempData
   - Redirects to show modal
3. Modal blocks with two options:
   - Cancel: Don't upload (returns to page)
   - Upload Anyway: Re-upload with confirmation flag

Modal Features:
- Non-dismissible (static backdrop, no keyboard close)
- Yellow warning header
- Table showing all potential duplicates with:
  - Receipt ID and filename
  - Upload timestamp
  - Reason for match (hash/name+size)
  - Transaction mapping status
  - View button to compare
- Clear explanation of options
- Prompts user to re-select file for confirmed upload

Benefits:
- Prevents accidental duplicate uploads
- Forces user acknowledgment
- Provides context for decision-making
- Links to view existing receipts for comparison
- Better UX than passive alert banner

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-12 13:39:43 -04:00
parent 7ac80ab8d0
commit 123367c7bf
2 changed files with 136 additions and 44 deletions

View File

@@ -18,37 +18,93 @@
</div>
}
<!-- Duplicate Warnings -->
@if (Model.DuplicateWarnings.Any())
<!-- Duplicate Warning Modal -->
@if (Model.ShowDuplicateModal && Model.DuplicateWarnings.Any())
{
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<h5 class="alert-heading">⚠️ Potential Duplicates Detected</h5>
<p>The following receipt(s) may be duplicates of the one you just uploaded:</p>
<ul class="mb-0">
<div class="modal fade show" id="duplicateWarningModal" tabindex="-1" aria-labelledby="duplicateWarningModalLabel" style="display: block; background-color: rgba(0,0,0,0.5);" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="duplicateWarningModalLabel">⚠️ Potential Duplicates Detected</h5>
</div>
<div class="modal-body">
<p class="fw-bold">The file "@Model.PendingUploadFileName" may be a duplicate of existing receipt(s):</p>
<div class="table-responsive">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>Receipt</th>
<th>Uploaded</th>
<th>Reason</th>
<th>Transaction</th>
<th>Action</th>
</tr>
</thead>
<tbody>
@foreach (var warning in Model.DuplicateWarnings)
{
<li>
<strong>Receipt #@warning.ReceiptId</strong> - @warning.FileName
<br />
<small class="text-muted">
Uploaded: @warning.UploadedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm") |
Reason: @warning.Reason
<tr>
<td>
<strong>#@warning.ReceiptId</strong><br />
<small>@warning.FileName</small>
</td>
<td class="small">@warning.UploadedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm")</td>
<td class="small">@warning.Reason</td>
<td class="small">
@if (warning.TransactionId.HasValue)
{
<span> | Mapped to: @warning.TransactionName (Transaction #@warning.TransactionId)</span>
<div><strong>@warning.TransactionName</strong></div>
<div class="text-muted">Transaction #@warning.TransactionId</div>
}
else
{
<span> | Status: Unmapped</span>
<span class="badge bg-warning text-dark">Unmapped</span>
}
</small>
<br />
<a asp-page="/ViewReceipt" asp-route-id="@warning.ReceiptId" class="btn btn-sm btn-outline-primary mt-1">View Receipt #@warning.ReceiptId</a>
</li>
</td>
<td>
<a asp-page="/ViewReceipt" asp-route-id="@warning.ReceiptId" class="btn btn-sm btn-outline-primary" target="_blank">
View
</a>
</td>
</tr>
}
</ul>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</tbody>
</table>
</div>
<div class="alert alert-info mb-0">
<strong>What would you like to do?</strong>
<ul class="mb-0">
<li><strong>Cancel:</strong> Don't upload this receipt</li>
<li><strong>Upload Anyway:</strong> Continue with upload despite potential duplicates</li>
</ul>
</div>
</div>
<div class="modal-footer">
<form method="get" style="display: inline;">
<button type="submit" class="btn btn-secondary">Cancel</button>
</form>
<form method="post" asp-page-handler="Upload" enctype="multipart/form-data" style="display: inline;">
<input type="file" asp-for="UploadFile" accept=".jpg,.jpeg,.png,.pdf,.gif,.heic" style="display:none;" id="confirmUploadFile" required />
<input type="hidden" asp-for="ConfirmDuplicateUpload" value="true" />
<button type="submit" class="btn btn-warning" onclick="return copyFileInput();">Upload Anyway</button>
</form>
</div>
</div>
</div>
</div>
<script>
// Auto-show modal on page load
document.addEventListener('DOMContentLoaded', function() {
var modal = new bootstrap.Modal(document.getElementById('duplicateWarningModal'));
modal.show();
});
function copyFileInput() {
alert('Please re-select the file to upload.');
document.getElementById('confirmUploadFile').click();
return false;
}
</script>
}
<!-- Upload Section -->

View File

@@ -26,25 +26,33 @@ namespace MoneyMap.Pages
[BindProperty]
public IFormFile? UploadFile { get; set; }
[BindProperty]
public bool ConfirmDuplicateUpload { get; set; }
[TempData]
public string? Message { get; set; }
[TempData]
public bool IsSuccess { get; set; }
[TempData]
public string? PendingUploadFileName { get; set; }
[TempData]
public string? DuplicateWarningsJson { get; set; }
public List<DuplicateWarning> DuplicateWarnings { get; set; } = new();
public bool ShowDuplicateModal { get; set; } = false;
public async Task OnGetAsync()
{
await LoadReceiptsAsync();
// Deserialize duplicate warnings if present
// Show duplicate modal if warnings present
if (!string.IsNullOrWhiteSpace(DuplicateWarningsJson))
{
DuplicateWarnings = System.Text.Json.JsonSerializer.Deserialize<List<DuplicateWarning>>(DuplicateWarningsJson) ?? new();
ShowDuplicateModal = DuplicateWarnings.Any();
}
}
@@ -58,21 +66,48 @@ namespace MoneyMap.Pages
return Page();
}
// If not confirmed and duplicates exist, show modal
if (!ConfirmDuplicateUpload)
{
var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
if (result.IsSuccess)
{
if (result.DuplicateWarnings.Any())
{
Message = $"Receipt uploaded successfully, but {result.DuplicateWarnings.Count} potential duplicate(s) detected.";
IsSuccess = true;
// Store warnings and redirect to show modal
PendingUploadFileName = UploadFile.FileName;
DuplicateWarningsJson = System.Text.Json.JsonSerializer.Serialize(result.DuplicateWarnings);
// Delete the uploaded file since user needs to confirm
await _receiptManager.DeleteReceiptAsync(result.Receipt!.Id);
return RedirectToPage();
}
else
{
Message = "Receipt uploaded successfully!";
IsSuccess = true;
return RedirectToPage();
}
}
else
{
Message = result.ErrorMessage ?? "Failed to upload receipt.";
IsSuccess = false;
await LoadReceiptsAsync();
return Page();
}
}
else
{
// User confirmed, upload anyway
var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
if (result.IsSuccess)
{
Message = "Receipt uploaded successfully!";
IsSuccess = true;
return RedirectToPage();
}
else
@@ -83,6 +118,7 @@ namespace MoneyMap.Pages
return Page();
}
}
}
public async Task<IActionResult> OnPostDeleteAsync(long receiptId)
{