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>
193 lines
6.8 KiB
C#
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; }
|
|
}
|
|
}
|
|
} |