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>
213 lines
8.1 KiB
C#
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; }
|
|
}
|
|
}
|
|
} |