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> </div>
} }
<!-- Duplicate Warnings --> <!-- Duplicate Warning Modal -->
@if (Model.DuplicateWarnings.Any()) @if (Model.ShowDuplicateModal && Model.DuplicateWarnings.Any())
{ {
<div class="alert alert-warning alert-dismissible fade show" role="alert"> <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">
<h5 class="alert-heading">⚠️ Potential Duplicates Detected</h5> <div class="modal-dialog modal-lg">
<p>The following receipt(s) may be duplicates of the one you just uploaded:</p> <div class="modal-content">
<ul class="mb-0"> <div class="modal-header bg-warning">
@foreach (var warning in Model.DuplicateWarnings) <h5 class="modal-title" id="duplicateWarningModalLabel">⚠️ Potential Duplicates Detected</h5>
{ </div>
<li> <div class="modal-body">
<strong>Receipt #@warning.ReceiptId</strong> - @warning.FileName <p class="fw-bold">The file "@Model.PendingUploadFileName" may be a duplicate of existing receipt(s):</p>
<br /> <div class="table-responsive">
<small class="text-muted"> <table class="table table-sm table-bordered">
Uploaded: @warning.UploadedAtUtc.ToLocalTime().ToString("yyyy-MM-dd HH:mm") | <thead class="table-light">
Reason: @warning.Reason <tr>
@if (warning.TransactionId.HasValue) <th>Receipt</th>
{ <th>Uploaded</th>
<span> | Mapped to: @warning.TransactionName (Transaction #@warning.TransactionId)</span> <th>Reason</th>
} <th>Transaction</th>
else <th>Action</th>
{ </tr>
<span> | Status: Unmapped</span> </thead>
} <tbody>
</small> @foreach (var warning in Model.DuplicateWarnings)
<br /> {
<a asp-page="/ViewReceipt" asp-route-id="@warning.ReceiptId" class="btn btn-sm btn-outline-primary mt-1">View Receipt #@warning.ReceiptId</a> <tr>
</li> <td>
} <strong>#@warning.ReceiptId</strong><br />
</ul> <small>@warning.FileName</small>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </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)
{
<div><strong>@warning.TransactionName</strong></div>
<div class="text-muted">Transaction #@warning.TransactionId</div>
}
else
{
<span class="badge bg-warning text-dark">Unmapped</span>
}
</td>
<td>
<a asp-page="/ViewReceipt" asp-route-id="@warning.ReceiptId" class="btn btn-sm btn-outline-primary" target="_blank">
View
</a>
</td>
</tr>
}
</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> </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 --> <!-- Upload Section -->

View File

@@ -26,25 +26,33 @@ namespace MoneyMap.Pages
[BindProperty] [BindProperty]
public IFormFile? UploadFile { get; set; } public IFormFile? UploadFile { get; set; }
[BindProperty]
public bool ConfirmDuplicateUpload { get; set; }
[TempData] [TempData]
public string? Message { get; set; } public string? Message { get; set; }
[TempData] [TempData]
public bool IsSuccess { get; set; } public bool IsSuccess { get; set; }
[TempData]
public string? PendingUploadFileName { get; set; }
[TempData] [TempData]
public string? DuplicateWarningsJson { get; set; } public string? DuplicateWarningsJson { get; set; }
public List<DuplicateWarning> DuplicateWarnings { get; set; } = new(); public List<DuplicateWarning> DuplicateWarnings { get; set; } = new();
public bool ShowDuplicateModal { get; set; } = false;
public async Task OnGetAsync() public async Task OnGetAsync()
{ {
await LoadReceiptsAsync(); await LoadReceiptsAsync();
// Deserialize duplicate warnings if present // Show duplicate modal if warnings present
if (!string.IsNullOrWhiteSpace(DuplicateWarningsJson)) if (!string.IsNullOrWhiteSpace(DuplicateWarningsJson))
{ {
DuplicateWarnings = System.Text.Json.JsonSerializer.Deserialize<List<DuplicateWarning>>(DuplicateWarningsJson) ?? new(); DuplicateWarnings = System.Text.Json.JsonSerializer.Deserialize<List<DuplicateWarning>>(DuplicateWarningsJson) ?? new();
ShowDuplicateModal = DuplicateWarnings.Any();
} }
} }
@@ -58,29 +66,57 @@ namespace MoneyMap.Pages
return Page(); return Page();
} }
var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile); // If not confirmed and duplicates exist, show modal
if (!ConfirmDuplicateUpload)
if (result.IsSuccess)
{ {
if (result.DuplicateWarnings.Any()) var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
if (result.IsSuccess)
{ {
Message = $"Receipt uploaded successfully, but {result.DuplicateWarnings.Count} potential duplicate(s) detected."; if (result.DuplicateWarnings.Any())
IsSuccess = true; {
DuplicateWarningsJson = System.Text.Json.JsonSerializer.Serialize(result.DuplicateWarnings); // 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 else
{ {
Message = "Receipt uploaded successfully!"; Message = result.ErrorMessage ?? "Failed to upload receipt.";
IsSuccess = true; IsSuccess = false;
await LoadReceiptsAsync();
return Page();
} }
return RedirectToPage();
} }
else else
{ {
Message = result.ErrorMessage ?? "Failed to upload receipt."; // User confirmed, upload anyway
IsSuccess = false; var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
await LoadReceiptsAsync();
return Page(); if (result.IsSuccess)
{
Message = "Receipt uploaded successfully!";
IsSuccess = true;
return RedirectToPage();
}
else
{
Message = result.ErrorMessage ?? "Failed to upload receipt.";
IsSuccess = false;
await LoadReceiptsAsync();
return Page();
}
} }
} }