Files
MoneyMap/MoneyMap/Pages/Receipts.cshtml.cs
AJ Isaacs 7b0853b74c Feature: Add pagination to Receipts page
- Add PageNumber and PageSize query parameters (default 25 per page)
- Implement server-side pagination with Skip/Take
- Display "Showing X-Y of Z" count in header
- Add Bootstrap pagination controls with sliding page window

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-05 21:33:05 -05:00

351 lines
12 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using MoneyMap.Data;
using MoneyMap.Services;
namespace MoneyMap.Pages
{
public class ReceiptsModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly IReceiptManager _receiptManager;
private readonly IReceiptAutoMapper _autoMapper;
private readonly IReceiptMatchingService _receiptMatchingService;
public ReceiptsModel(MoneyMapContext db, IReceiptManager receiptManager, IReceiptAutoMapper autoMapper, IReceiptMatchingService receiptMatchingService)
{
_db = db;
_receiptManager = receiptManager;
_autoMapper = autoMapper;
_receiptMatchingService = receiptMatchingService;
}
public List<ReceiptRow> Receipts { get; set; } = new();
public Dictionary<long, List<TransactionOption>> ReceiptTransactionMatches { get; set; } = new();
[BindProperty(SupportsGet = true)]
public int PageNumber { get; set; } = 1;
[BindProperty(SupportsGet = true)]
public int PageSize { get; set; } = 25;
public int TotalCount { get; set; }
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
[BindProperty]
public IFormFile? UploadFile { get; set; }
[BindProperty]
public bool ConfirmDuplicateUpload { get; set; }
[TempData]
public string? Message { get; set; }
[TempData]
public bool IsSuccess { get; set; }
[TempData]
public string? PendingUploadFileName { get; set; }
[TempData]
public string? DuplicateWarningsJson { get; set; }
public List<DuplicateWarning> DuplicateWarnings { get; set; } = new();
public bool ShowDuplicateModal { get; set; } = false;
public async Task OnGetAsync()
{
await LoadReceiptsAsync();
// Show duplicate modal if warnings present
if (!string.IsNullOrWhiteSpace(DuplicateWarningsJson))
{
DuplicateWarnings = System.Text.Json.JsonSerializer.Deserialize<List<DuplicateWarning>>(DuplicateWarningsJson) ?? new();
ShowDuplicateModal = DuplicateWarnings.Any();
}
}
public async Task<IActionResult> OnPostUploadAsync()
{
if (UploadFile == null)
{
Message = "Please select a file to upload.";
IsSuccess = false;
await LoadReceiptsAsync();
return Page();
}
// If not confirmed and duplicates exist, show modal
if (!ConfirmDuplicateUpload)
{
var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
if (result.IsSuccess)
{
if (result.DuplicateWarnings.Any())
{
// Store warnings and redirect to show modal
PendingUploadFileName = UploadFile.FileName;
DuplicateWarningsJson = System.Text.Json.JsonSerializer.Serialize(result.DuplicateWarnings);
// Delete the uploaded file since user needs to confirm
await _receiptManager.DeleteReceiptAsync(result.Receipt!.Id);
return RedirectToPage();
}
else
{
Message = "Receipt uploaded successfully!";
IsSuccess = true;
return RedirectToPage();
}
}
else
{
Message = result.ErrorMessage ?? "Failed to upload receipt.";
IsSuccess = false;
await LoadReceiptsAsync();
return Page();
}
}
else
{
// User confirmed, upload anyway
var result = await _receiptManager.UploadUnmappedReceiptAsync(UploadFile);
if (result.IsSuccess)
{
Message = "Receipt uploaded successfully!";
IsSuccess = true;
return RedirectToPage();
}
else
{
Message = result.ErrorMessage ?? "Failed to upload receipt.";
IsSuccess = false;
await LoadReceiptsAsync();
return Page();
}
}
}
public async Task<IActionResult> OnPostDeleteAsync(long receiptId)
{
var success = await _receiptManager.DeleteReceiptAsync(receiptId);
if (success)
{
Message = "Receipt deleted successfully.";
IsSuccess = true;
}
else
{
Message = "Failed to delete receipt.";
IsSuccess = false;
}
return RedirectToPage();
}
public async Task<IActionResult> OnPostMapToTransactionAsync(long receiptId, long transactionId)
{
if (transactionId <= 0)
{
Message = "Please select a transaction or enter a valid ID.";
IsSuccess = false;
return RedirectToPage();
}
var receipt = await _db.Receipts.FirstOrDefaultAsync(r => r.Id == receiptId);
if (receipt == null)
{
Message = $"Receipt not found (ID {receiptId}).";
IsSuccess = false;
return RedirectToPage();
}
var transactionExists = await _db.Transactions.AnyAsync(t => t.Id == transactionId);
if (!transactionExists)
{
Message = $"Transaction not found (ID {transactionId}).";
IsSuccess = false;
return RedirectToPage();
}
// Friendly duplicate check: same file already mapped to this transaction
if (!string.IsNullOrWhiteSpace(receipt.FileHashSha256))
{
var duplicateExists = await _db.Receipts.AnyAsync(r => r.Id != receiptId && r.TransactionId == transactionId && r.FileHashSha256 == receipt.FileHashSha256);
if (duplicateExists)
{
Message = "This transaction already has a receipt with the same file (duplicate prevented).";
IsSuccess = false;
return RedirectToPage();
}
}
var success = await _receiptManager.MapReceiptToTransactionAsync(receiptId, transactionId);
if (success)
{
Message = "Receipt mapped to transaction successfully.";
IsSuccess = true;
}
else
{
Message = "Failed to map receipt to transaction.";
IsSuccess = false;
}
return RedirectToPage();
}
public async Task<IActionResult> OnPostAutoMapUnmappedAsync()
{
var result = await _autoMapper.AutoMapUnmappedReceiptsAsync();
if (result.MappedCount > 0)
{
Message = $"Successfully auto-mapped {result.MappedCount} receipt(s). " +
$"{result.NoMatchCount} had no matching transaction. " +
$"{result.MultipleMatchesCount} had multiple potential matches.";
IsSuccess = true;
}
else if (result.TotalProcessed == 0)
{
Message = "No unmapped parsed receipts found to process.";
IsSuccess = false;
}
else
{
Message = $"Unable to auto-map any receipts. {result.NoMatchCount} had no matching transaction. " +
$"{result.MultipleMatchesCount} had multiple potential matches.";
IsSuccess = false;
}
return RedirectToPage();
}
public async Task<IActionResult> OnPostUnmapAsync(long receiptId)
{
var success = await _receiptManager.UnmapReceiptAsync(receiptId);
if (success)
{
Message = "Receipt unmapped successfully.";
IsSuccess = true;
}
else
{
Message = "Failed to unmap receipt.";
IsSuccess = false;
}
return RedirectToPage();
}
private async Task LoadReceiptsAsync()
{
if (PageNumber < 1) PageNumber = 1;
if (PageSize < 1) PageSize = 25;
if (PageSize > 100) PageSize = 100;
var query = _db.Receipts
.Include(r => r.Transaction)
.OrderByDescending(r => r.UploadedAtUtc);
TotalCount = await query.CountAsync();
var receipts = await query
.Skip((PageNumber - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
Receipts = receipts.Select(r => new ReceiptRow
{
Id = r.Id,
FileName = r.FileName,
ContentType = r.ContentType,
FileSizeBytes = r.FileSizeBytes,
UploadedAtUtc = r.UploadedAtUtc,
TransactionId = r.TransactionId,
TransactionName = r.Transaction?.Name,
TransactionDate = r.Transaction?.Date,
TransactionAmount = r.Transaction?.Amount,
Merchant = r.Merchant,
ReceiptDate = r.ReceiptDate,
DueDate = r.DueDate,
Total = r.Total,
StoragePath = r.StoragePath
}).ToList();
// Load matching transactions for each unmapped receipt
var transactionsWithReceipts = await _receiptMatchingService.GetTransactionIdsWithReceiptsAsync();
var unmappedReceipts = Receipts.Where(r => !r.TransactionId.HasValue).ToList();
foreach (var receipt in unmappedReceipts)
{
var criteria = new ReceiptMatchCriteria
{
ReceiptDate = receipt.ReceiptDate,
DueDate = receipt.DueDate,
Total = receipt.Total,
MerchantName = receipt.Merchant,
ExcludeTransactionIds = transactionsWithReceipts
};
var matches = await _receiptMatchingService.FindMatchingTransactionsAsync(criteria);
// Convert TransactionMatch to TransactionOption
ReceiptTransactionMatches[receipt.Id] = matches.Select(m => new TransactionOption
{
Id = m.Id,
Date = m.Date,
Name = m.Name,
Amount = m.Amount,
MerchantName = m.MerchantName,
PaymentMethod = m.PaymentMethod,
IsExactAmount = m.IsExactAmount,
IsCloseAmount = m.IsCloseAmount
}).ToList();
}
}
public class ReceiptRow
{
public long Id { get; set; }
public string FileName { get; set; } = "";
public string ContentType { get; set; } = "";
public long FileSizeBytes { get; set; }
public DateTime UploadedAtUtc { get; set; }
public long? TransactionId { get; set; }
public string? TransactionName { get; set; }
public DateTime? TransactionDate { get; set; }
public decimal? TransactionAmount { get; set; }
public string? Merchant { get; set; }
public DateTime? ReceiptDate { get; set; }
public DateTime? DueDate { get; set; }
public decimal? Total { get; set; }
public string StoragePath { get; set; } = "";
}
public class TransactionOption
{
public long Id { get; set; }
public DateTime Date { get; set; }
public string Name { get; set; } = "";
public decimal Amount { get; set; }
public string? MerchantName { get; set; }
public string PaymentMethod { get; set; } = "";
public bool IsExactAmount { get; set; }
public bool IsCloseAmount { get; set; }
}
}
}