Files
MoneyMap/MoneyMap/Pages/Transactions.cshtml.cs
AJ Isaacs 2be9990dbc Improve: Default transaction date filter and LLM response viewer
Transactions page now defaults to last 30 days when no date filters are
set. ViewReceipt page adds collapsible raw LLM response payload on
parse logs for debugging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 19:14:27 -05:00

213 lines
8.1 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Models;
using MoneyMap.Services;
namespace MoneyMap.Pages
{
public class TransactionsModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly ITransactionStatisticsService _statsService;
private readonly IReferenceDataService _referenceDataService;
public TransactionsModel(MoneyMapContext db, ITransactionStatisticsService statsService, IReferenceDataService referenceDataService)
{
_db = db;
_statsService = statsService;
_referenceDataService = referenceDataService;
}
[BindProperty(SupportsGet = true)]
public string? Search { get; set; }
[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 List<CategoryBreakdown> CategoryBreakdowns { get; set; } = new();
public async Task OnGetAsync()
{
// Default to last 30 days if no date filters provided
if (!StartDate.HasValue && !EndDate.HasValue)
{
StartDate = DateTime.Today.AddDays(-30);
EndDate = DateTime.Today;
}
var query = _db.Transactions
.Include(t => t.Card)
.ThenInclude(c => c!.Account)
.Include(t => t.Account)
.Include(t => t.TransferToAccount)
.Include(t => t.Merchant)
.AsQueryable();
// Apply filters (case-insensitive search using EF.Functions.Like)
if (!string.IsNullOrWhiteSpace(Search))
{
var searchPattern = $"%{Search}%";
query = query.Where(t =>
EF.Functions.Like(t.Name, searchPattern) ||
(t.Memo != null && EF.Functions.Like(t.Memo, searchPattern)) ||
(t.Category != null && EF.Functions.Like(t.Category, searchPattern)) ||
(t.Notes != null && EF.Functions.Like(t.Notes, searchPattern)) ||
(t.Merchant != null && EF.Functions.Like(t.Merchant.Name, searchPattern)));
}
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 => t.MerchantId == null);
}
else
{
query = query.Where(t => t.Merchant != null && t.Merchant.Name == 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 => r.TransactionId != null && transactionIds.Contains(r.TransactionId.Value))
.GroupBy(r => r.TransactionId)
.Select(g => new { TransactionId = g.Key!.Value, 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)
Stats = await _statsService.CalculateStatsAsync(query);
// Calculate category breakdown for pie chart (only expenses)
var expenseQuery = query.Where(t => t.Amount < 0).ExcludeTransfers();
var categoryGroups = await expenseQuery
.GroupBy(t => t.Category ?? "")
.Select(g => new CategoryBreakdown
{
Category = g.Key,
TotalSpend = g.Sum(x => -x.Amount),
Count = g.Count()
})
.OrderByDescending(x => x.TotalSpend)
.ToListAsync();
CategoryBreakdowns = categoryGroups;
// Get available categories for filter dropdown
AvailableCategories = await _referenceDataService.GetAvailableCategoriesAsync();
// Get available merchants for filter dropdown
var merchants = await _referenceDataService.GetAvailableMerchantsAsync();
AvailableMerchants = merchants.Select(m => m.Name).ToList();
// Get available cards for filter dropdown
AvailableCards = await _referenceDataService.GetAvailableCardsAsync(includeAccount: false);
}
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 CategoryBreakdown
{
public string Category { get; set; } = "";
public decimal TotalSpend { get; set; }
public int Count { get; set; }
}
}
}