Feature: add ability to unmap receipts from transactions

Add unmap functionality to allow users to disassociate receipts from transactions without deleting them:

- ReceiptManager: Add UnmapReceiptAsync method
- Receipts page: Add OnPostUnmapAsync handler
- Receipts view: Add Unmap button for mapped receipts with confirmation dialog

This provides a non-destructive alternative to deleting receipts when they need to be remapped to different transactions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-26 00:01:11 -04:00
parent b4a91d8b7f
commit f8bb5d5a82
3 changed files with 41 additions and 1 deletions

View File

@@ -235,6 +235,14 @@
Map
</button>
}
else
{
<form method="post" asp-page-handler="Unmap" asp-route-receiptId="@r.Id" style="display: inline;" onsubmit="return confirm('Are you sure you want to unmap this receipt from the transaction?');">
<button type="submit" class="btn btn-outline-warning btn-sm" title="Unmap from Transaction">
Unmap
</button>
</form>
}
<form method="post" asp-page-handler="Delete" asp-route-receiptId="@r.Id" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this receipt?');">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Delete">
Delete

View File

@@ -218,6 +218,24 @@ namespace MoneyMap.Pages
return RedirectToPage();
}
public async Task<IActionResult> OnPostUnmapAsync(long receiptId)
{
var success = await _receiptManager.UnmapReceiptAsync(receiptId);
if (success)
{
Message = "Receipt unmapped successfully.";
IsSuccess = true;
}
else
{
Message = "Failed to unmap receipt.";
IsSuccess = false;
}
return RedirectToPage();
}
private async Task LoadReceiptsAsync()
{
var receipts = await _db.Receipts

View File

@@ -12,6 +12,7 @@ namespace MoneyMap.Services
Task<ReceiptUploadResult> UploadUnmappedReceiptAsync(IFormFile file);
Task<bool> DeleteReceiptAsync(long receiptId);
Task<bool> MapReceiptToTransactionAsync(long receiptId, long transactionId);
Task<bool> UnmapReceiptAsync(long receiptId);
string GetReceiptPhysicalPath(Receipt receipt);
Task<Receipt?> GetReceiptAsync(long receiptId);
}
@@ -210,6 +211,19 @@ namespace MoneyMap.Services
return true;
}
public async Task<bool> UnmapReceiptAsync(long receiptId)
{
var receipt = await _db.Receipts.FindAsync(receiptId);
if (receipt == null)
return false;
// Set TransactionId to null to unmap
receipt.TransactionId = null;
await _db.SaveChangesAsync();
return true;
}
private static string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
@@ -219,7 +233,7 @@ namespace MoneyMap.Services
var sanitized = new StringBuilder();
foreach (var c in fileName)
{
if (c == '®' || c == '' || c == '©')
if (c == '<EFBFBD>' || c == '<EFBFBD>' || c == '<EFBFBD>')
{
// Skip trademark/copyright symbols
continue;