diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5d1f860..a5e5b83 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,15 +2,10 @@ "permissions": { "allow": [ "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(dotnet ef migrations add:*)", - "Bash(dotnet build)", - "Bash(dotnet ef database:*)", - "Bash(dotnet ef migrations:*)", - "Bash(git reset:*)", - "Bash(already contains a definition for 'MultipleMatches'\".\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude \nEOF\n)\")" + "Bash(git commit:*)" ], "deny": [], "ask": [] - } + }, + "spinnerTipsEnabled": false } diff --git a/MoneyMap/Pages/Receipts.cshtml b/MoneyMap/Pages/Receipts.cshtml index 476368a..2a84a16 100644 --- a/MoneyMap/Pages/Receipts.cshtml +++ b/MoneyMap/Pages/Receipts.cshtml @@ -229,22 +229,74 @@ }
- - -
- Showing last 100 transactions without receipts. - Can't find it? Open Transactions page to find the ID and enter it manually below. + else + { + Showing recent transactions without receipts. + } + Rows highlighted in green have matching amounts. +
+ +
+ + + + + + + + + + + + + @foreach (var txn in matches) + { + var rowClass = txn.IsAmountMatch ? "table-success" : ""; + + + + + + + + + } + +
SelectDateAmountNameMerchantPayment
+ + @txn.Date.ToString("yyyy-MM-dd")@txn.Amount.ToString("C")@txn.Name@(txn.MerchantName ?? "-")@txn.PaymentMethod
+
+ } + else + { +
+ No matching transactions found. + @if (r.ReceiptDate.HasValue) + { + Try searching within ±3 days of @r.ReceiptDate.Value.ToString("yyyy-MM-dd"). + } +
+ } +
+ Can't find it? Open Transactions page to search, then enter the ID manually below.
diff --git a/MoneyMap/Pages/Receipts.cshtml.cs b/MoneyMap/Pages/Receipts.cshtml.cs index 15d0b6f..b94b907 100644 --- a/MoneyMap/Pages/Receipts.cshtml.cs +++ b/MoneyMap/Pages/Receipts.cshtml.cs @@ -21,7 +21,7 @@ namespace MoneyMap.Pages } public List Receipts { get; set; } = new(); - public List RecentTransactions { get; set; } = new(); + public Dictionary> ReceiptTransactionMatches { get; set; } = new(); [BindProperty] public IFormFile? UploadFile { get; set; } @@ -170,30 +170,103 @@ namespace MoneyMap.Pages StoragePath = r.StoragePath }).ToList(); - // Load recent transactions without receipts for mapping dropdown + // Load matching transactions for each unmapped receipt var transactionsWithReceipts = await _db.Receipts .Where(r => r.TransactionId != null) .Select(r => r.TransactionId!.Value) - .ToListAsync(); + .ToHashSet(); - RecentTransactions = await _db.Transactions + var unmappedReceipts = Receipts.Where(r => !r.TransactionId.HasValue).ToList(); + + foreach (var receipt in unmappedReceipts) + { + var matches = await FindMatchingTransactionsForReceipt(receipt, transactionsWithReceipts); + ReceiptTransactionMatches[receipt.Id] = matches; + } + } + + private async Task> FindMatchingTransactionsForReceipt(ReceiptRow receipt, HashSet transactionsWithReceipts) + { + var query = _db.Transactions .Include(t => t.Card) .Include(t => t.Account) .Include(t => t.Merchant) .Where(t => !transactionsWithReceipts.Contains(t.Id)) + .AsQueryable(); + + // If receipt has a date, filter by +/- 3 days + if (receipt.ReceiptDate.HasValue) + { + var minDate = receipt.ReceiptDate.Value.AddDays(-3); + var maxDate = receipt.ReceiptDate.Value.AddDays(3); + query = query.Where(t => t.Date >= minDate && t.Date <= maxDate); + } + + // If receipt has merchant, filter by merchant name + if (!string.IsNullOrWhiteSpace(receipt.Merchant)) + { + query = query.Where(t => + (t.Merchant != null && t.Merchant.Name.Contains(receipt.Merchant)) || + t.Name.Contains(receipt.Merchant)); + } + + var candidates = await query .OrderByDescending(t => t.Date) .ThenByDescending(t => t.Id) - .Take(100) // Last 100 transactions without receipts - .Select(t => new TransactionOption + .Take(50) // Limit to 50 matches + .ToListAsync(); + + // Calculate match scores and mark close amount matches + var options = candidates.Select(t => + { + var option = new TransactionOption { Id = t.Id, Date = t.Date, Name = t.Name, Amount = t.Amount, - MerchantName = t.Merchant != null ? t.Merchant.Name : null, - PaymentMethod = t.PaymentMethodLabel - }) - .ToListAsync(); + MerchantName = t.Merchant?.Name, + PaymentMethod = t.PaymentMethodLabel, + IsAmountMatch = false + }; + + // Check if amount matches within tolerance + if (receipt.Total.HasValue) + { + var receiptTotal = Math.Abs(receipt.Total.Value); + var transactionAmount = Math.Abs(t.Amount); + option.IsAmountMatch = Math.Abs(transactionAmount - receiptTotal) <= 0.10m; + } + + return option; + }).ToList(); + + // If no date-filtered matches, fall back to recent transactions + if (!options.Any() && !receipt.ReceiptDate.HasValue) + { + var fallback = await _db.Transactions + .Include(t => t.Card) + .Include(t => t.Account) + .Include(t => t.Merchant) + .Where(t => !transactionsWithReceipts.Contains(t.Id)) + .OrderByDescending(t => t.Date) + .ThenByDescending(t => t.Id) + .Take(50) + .Select(t => new TransactionOption + { + Id = t.Id, + Date = t.Date, + Name = t.Name, + Amount = t.Amount, + MerchantName = t.Merchant != null ? t.Merchant.Name : null, + PaymentMethod = t.PaymentMethodLabel + }) + .ToListAsync(); + + options = fallback; + } + + return options; } public class ReceiptRow @@ -221,6 +294,7 @@ namespace MoneyMap.Pages public decimal Amount { get; set; } public string? MerchantName { get; set; } public string PaymentMethod { get; set; } = ""; + public bool IsAmountMatch { get; set; } } } }