Files
MoneyMap/MoneyMap/Pages/EditTransaction.cshtml.cs
AJ 56089cc437 Fix: improve EditTransaction form handling and validation
- Add hidden fields for immutable transaction properties to preserve values during form submission
- Make category field optional by removing validation for empty values
- Simplify category input handling by removing duplicate hidden field
- Clean up JavaScript by using proper element IDs instead of querySelector

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 01:59:50 -04:00

281 lines
9.8 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 EditTransactionModel : PageModel
{
private readonly MoneyMapContext _db;
private readonly IReceiptManager _receiptManager;
private readonly IReceiptParser _receiptParser;
private readonly IReferenceDataService _referenceDataService;
private readonly IMerchantService _merchantService;
public EditTransactionModel(MoneyMapContext db, IReceiptManager receiptManager, IReceiptParser receiptParser, IReferenceDataService referenceDataService, IMerchantService merchantService)
{
_db = db;
_receiptManager = receiptManager;
_receiptParser = receiptParser;
_referenceDataService = referenceDataService;
_merchantService = merchantService;
}
[BindProperty]
public TransactionEditModel Transaction { get; set; } = new();
[BindProperty]
public IFormFile? ReceiptFile { get; set; }
[BindProperty]
public bool UseCustomCategory { get; set; }
[BindProperty]
public bool UseCustomMerchant { get; set; }
public List<string> AvailableCategories { get; set; } = new();
public List<Merchant> AvailableMerchants { get; set; } = new();
public List<ReceiptWithItems> Receipts { get; set; } = new();
[TempData]
public string? SuccessMessage { get; set; }
[TempData]
public string? ErrorMessage { get; set; }
public async Task<IActionResult> OnGetAsync(long id)
{
var transaction = await _db.Transactions
.Include(t => t.Card)
.ThenInclude(c => c!.Account)
.Include(t => t.Account)
.Include(t => t.TransferToAccount)
.Include(t => t.Merchant)
.Include(t => t.Receipts)
.ThenInclude(r => r.LineItems)
.FirstOrDefaultAsync(t => t.Id == id);
if (transaction == null)
return NotFound();
Transaction = new TransactionEditModel
{
Id = transaction.Id,
Date = transaction.Date,
Name = transaction.Name,
Memo = transaction.Memo,
Amount = transaction.Amount,
Category = transaction.Category ?? "",
MerchantId = transaction.MerchantId,
Notes = transaction.Notes ?? "",
CardLabel = transaction.PaymentMethodLabel,
AccountLabel = transaction.Card?.Account?.DisplayLabel ?? transaction.Account?.DisplayLabel ?? "None"
};
Receipts = transaction.Receipts?.Select(r => new ReceiptWithItems
{
Receipt = r,
LineItems = r.LineItems?.OrderBy(li => li.LineNumber).ToList() ?? new List<ReceiptLineItem>()
}).ToList() ?? new List<ReceiptWithItems>();
await LoadAvailableCategoriesAsync();
await LoadAvailableMerchantsAsync();
// Check if current category exists in list
UseCustomCategory = !string.IsNullOrWhiteSpace(Transaction.Category)
&& !AvailableCategories.Contains(Transaction.Category);
// Check if current merchant exists in list
UseCustomMerchant = Transaction.MerchantId.HasValue
&& !AvailableMerchants.Any(m => m.Id == Transaction.MerchantId.Value);
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
// Remove Category from model state validation if it's empty (category is optional)
if (string.IsNullOrWhiteSpace(Transaction.Category))
{
ModelState.Remove("Transaction.Category");
}
// Remove Notes from model state validation if it's empty
if (string.IsNullOrWhiteSpace(Transaction.Notes))
{
ModelState.Remove("Transaction.Notes");
}
// Remove MerchantName from validation (it's only used when creating new merchant)
ModelState.Remove("Transaction.MerchantName");
if (!ModelState.IsValid)
{
await LoadDataAsync();
return Page();
}
var transaction = await _db.Transactions.FindAsync(Transaction.Id);
if (transaction == null)
return NotFound();
// Update category and notes
transaction.Category = string.IsNullOrWhiteSpace(Transaction.Category)
? ""
: Transaction.Category.Trim();
transaction.Notes = string.IsNullOrWhiteSpace(Transaction.Notes)
? ""
: Transaction.Notes.Trim();
// Update merchant
if (!string.IsNullOrWhiteSpace(Transaction.MerchantName))
{
// Create or get merchant if custom name was entered
var merchantId = await _merchantService.GetOrCreateIdAsync(Transaction.MerchantName);
transaction.MerchantId = merchantId;
}
else if (Transaction.MerchantId.HasValue && Transaction.MerchantId.Value > 0)
{
// Existing merchant was selected
transaction.MerchantId = Transaction.MerchantId.Value;
}
else
{
// No merchant selected
transaction.MerchantId = null;
}
await _db.SaveChangesAsync();
SuccessMessage = "Transaction updated successfully!";
return RedirectToPage(new { id = Transaction.Id });
}
public async Task<IActionResult> OnPostUploadReceiptAsync()
{
if (ReceiptFile == null)
{
ErrorMessage = "Please select a file to upload.";
await LoadDataAsync();
return Page();
}
var result = await _receiptManager.UploadReceiptAsync(Transaction.Id, ReceiptFile);
if (result.IsSuccess)
{
SuccessMessage = "Receipt uploaded successfully!";
}
else
{
ErrorMessage = result.ErrorMessage;
}
return RedirectToPage(new { id = Transaction.Id });
}
public async Task<IActionResult> OnPostUnmapReceiptAsync(long receiptId)
{
var receipt = await _db.Receipts.FindAsync(receiptId);
if (receipt != null)
{
receipt.TransactionId = null;
await _db.SaveChangesAsync();
SuccessMessage = "Receipt unmapped successfully! You can now find it on the Receipts page.";
}
else
{
ErrorMessage = "Receipt not found.";
}
return RedirectToPage(new { id = Transaction.Id });
}
public async Task<IActionResult> OnPostDeleteReceiptAsync(long receiptId)
{
var success = await _receiptManager.DeleteReceiptAsync(receiptId);
if (success)
{
SuccessMessage = "Receipt deleted permanently!";
}
else
{
ErrorMessage = "Failed to delete receipt.";
}
return RedirectToPage(new { id = Transaction.Id });
}
public async Task<IActionResult> OnPostParseReceiptAsync(long receiptId)
{
var result = await _receiptParser.ParseReceiptAsync(receiptId);
if (result.IsSuccess)
{
SuccessMessage = result.Message;
}
else
{
ErrorMessage = result.Message;
}
return RedirectToPage(new { id = Transaction.Id });
}
private async Task LoadDataAsync()
{
await LoadAvailableCategoriesAsync();
await LoadAvailableMerchantsAsync();
var transaction = await _db.Transactions
.Include(t => t.Receipts)
.ThenInclude(r => r.LineItems)
.FirstOrDefaultAsync(t => t.Id == Transaction.Id);
if (transaction != null)
{
Receipts = transaction.Receipts?.Select(r => new ReceiptWithItems
{
Receipt = r,
LineItems = r.LineItems?.OrderBy(li => li.LineNumber).ToList() ?? new List<ReceiptLineItem>()
}).ToList() ?? new List<ReceiptWithItems>();
}
}
private async Task LoadAvailableCategoriesAsync()
{
AvailableCategories = await _referenceDataService.GetAvailableCategoriesAsync();
}
private async Task LoadAvailableMerchantsAsync()
{
AvailableMerchants = await _referenceDataService.GetAvailableMerchantsAsync();
}
public class TransactionEditModel
{
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 int? MerchantId { get; set; }
public string? MerchantName { get; set; } = "";
public string? Notes { get; set; } = "";
public string CardLabel { get; set; } = "";
public string AccountLabel { get; set; } = "";
}
public class ReceiptWithItems
{
public Receipt Receipt { get; set; } = null!;
public List<ReceiptLineItem> LineItems { get; set; } = new();
}
}
}