Files
MoneyMap/MoneyMap/Pages/Transactions.cshtml.cs
AJ 675ffa6509 Add merchant field to transactions and category mappings
This feature enables easy filtering and identification of transactions by merchant name:
- Added Merchant column to Transaction model (nullable, max 100 chars)
- Added Merchant field to CategoryMapping model
- Modified ITransactionCategorizer to return CategorizationResult (category + merchant)
- Updated auto-categorization logic to assign merchant from category mappings
- Updated category mappings UI to include merchant field in add/edit forms
- Added merchant filter dropdown to transactions page with full pagination support
- Updated receipt parser to set transaction merchant from parsed receipt data
- Created two database migrations for the schema changes
- Updated helper methods to support merchant names in default mappings

Benefits:
- Consistent merchant naming across variant patterns (e.g., "Walmart" for all "WAL-MART*" patterns)
- Easy filtering by merchant on transactions page
- No CSV changes required - merchant is derived from category mapping patterns
- Receipt parsing can also populate merchant field automatically

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 03:21:31 -04:00

193 lines
6.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
namespace MoneyMap.Pages
{
public class TransactionsModel : PageModel
{
private readonly MoneyMapContext _db;
public TransactionsModel(MoneyMapContext db)
{
_db = db;
}
[BindProperty(SupportsGet = true)]
public string? Category { get; set; }
[BindProperty(SupportsGet = true)]
public string? Merchant { get; set; }
[BindProperty(SupportsGet = true)]
public string? CardId { get; set; }
[BindProperty(SupportsGet = true)]
public DateTime? StartDate { get; set; }
[BindProperty(SupportsGet = true)]
public DateTime? EndDate { get; set; }
[BindProperty(SupportsGet = true)]
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 50;
public int TotalPages { get; set; }
public int TotalCount { get; set; }
public List<TransactionRow> Transactions { get; set; } = new();
public List<string> AvailableCategories { get; set; } = new();
public List<string> AvailableMerchants { get; set; } = new();
public List<Card> AvailableCards { get; set; } = new();
public TransactionStats Stats { get; set; } = new();
public async Task OnGetAsync()
{
var query = _db.Transactions
.Include(t => t.Card)
.ThenInclude(c => c!.Account)
.Include(t => t.Account)
.Include(t => t.TransferToAccount)
.AsQueryable();
// Apply filters
if (!string.IsNullOrWhiteSpace(Category))
{
if (Category == "(blank)")
{
query = query.Where(t => string.IsNullOrWhiteSpace(t.Category));
}
else
{
query = query.Where(t => t.Category == Category);
}
}
if (!string.IsNullOrWhiteSpace(Merchant))
{
if (Merchant == "(blank)")
{
query = query.Where(t => string.IsNullOrWhiteSpace(t.Merchant));
}
else
{
query = query.Where(t => t.Merchant == Merchant);
}
}
if (!string.IsNullOrWhiteSpace(CardId) && int.TryParse(CardId, out int cardIdInt))
{
query = query.Where(t => t.CardId == cardIdInt);
}
if (StartDate.HasValue)
{
query = query.Where(t => t.Date >= StartDate.Value);
}
if (EndDate.HasValue)
{
query = query.Where(t => t.Date <= EndDate.Value);
}
// Get total count for pagination
TotalCount = await query.CountAsync();
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
// Ensure page number is valid
if (PageNumber < 1) PageNumber = 1;
if (PageNumber > TotalPages && TotalPages > 0) PageNumber = TotalPages;
// Get paginated transactions
var transactions = await query
.OrderByDescending(t => t.Date)
.ThenByDescending(t => t.Id)
.Skip((PageNumber - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
// Get receipt counts for the current page only
var transactionIds = transactions.Select(t => t.Id).ToList();
var receiptCounts = await _db.Receipts
.Where(r => transactionIds.Contains(r.TransactionId))
.GroupBy(r => r.TransactionId)
.Select(g => new { TransactionId = g.Key, Count = g.Count() })
.ToListAsync();
var receiptCountDict = receiptCounts.ToDictionary(x => x.TransactionId, x => x.Count);
Transactions = transactions.Select(t => new TransactionRow
{
Id = t.Id,
Date = t.Date,
Name = t.Name,
Memo = t.Memo,
Amount = t.Amount,
Category = t.Category ?? "",
Notes = t.Notes ?? "",
CardLabel = t.PaymentMethodLabel,
AccountLabel = t.Card?.Account?.DisplayLabel ?? t.Account?.DisplayLabel ?? "None",
ReceiptCount = receiptCountDict.ContainsKey(t.Id) ? receiptCountDict[t.Id] : 0
}).ToList();
// Calculate stats for filtered results (all pages, not just current)
var allFilteredTransactions = await query.ToListAsync();
Stats = new TransactionStats
{
Count = allFilteredTransactions.Count,
TotalDebits = allFilteredTransactions.Where(t => t.Amount < 0).Sum(t => t.Amount),
TotalCredits = allFilteredTransactions.Where(t => t.Amount > 0).Sum(t => t.Amount),
NetAmount = allFilteredTransactions.Sum(t => t.Amount)
};
// Get available categories for filter dropdown
AvailableCategories = await _db.Transactions
.Select(t => t.Category ?? "")
.Distinct()
.OrderBy(c => c)
.ToListAsync();
// Get available merchants for filter dropdown
AvailableMerchants = await _db.Transactions
.Where(t => !string.IsNullOrWhiteSpace(t.Merchant))
.Select(t => t.Merchant!)
.Distinct()
.OrderBy(m => m)
.ToListAsync();
// Get available cards for filter dropdown
AvailableCards = await _db.Cards
.OrderBy(c => c.Owner)
.ThenBy(c => c.Last4)
.ToListAsync();
}
public class TransactionRow
{
public long Id { get; set; }
public DateTime Date { get; set; }
public string Name { get; set; } = "";
public string Memo { get; set; } = "";
public decimal Amount { get; set; }
public string Category { get; set; } = "";
public string Notes { get; set; } = "";
public string CardLabel { get; set; } = "";
public string AccountLabel { get; set; } = "";
public int ReceiptCount { get; set; }
}
public class TransactionStats
{
public int Count { get; set; }
public decimal TotalDebits { get; set; }
public decimal TotalCredits { get; set; }
public decimal NetAmount { get; set; }
}
}
}