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:
@@ -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 -->
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user