Add merchant selection to EditTransaction page

Allow users to set or change the merchant for a transaction from the
EditTransaction page. Users can select from existing merchants or
create new ones on the fly.

Changes:
- Add merchant dropdown with existing merchants
- Support creating new merchants via custom input
- Update transaction merchant when saving
- Rename "Merchant Name" label to "Transaction Name" for clarity

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
AJ
2025-10-12 10:08:31 -04:00
parent 1e060c10f5
commit 6f307be026
2 changed files with 100 additions and 3 deletions

View File

@@ -52,7 +52,7 @@
</div>
<div class="mb-3">
<label class="form-label fw-bold">Merchant Name</label>
<label class="form-label fw-bold">Transaction Name</label>
<div class="form-control-plaintext">@Model.Transaction.Name</div>
</div>
@@ -98,6 +98,29 @@
<span asp-validation-for="Transaction.Category" class="text-danger"></span>
</div>
<div class="mb-3">
<label class="form-label fw-bold">Merchant</label>
<select class="form-select mb-2" id="merchantSelect" onchange="handleMerchantChange()">
<option value="">(no merchant)</option>
@foreach (var merchant in Model.AvailableMerchants)
{
<option value="@merchant.Id" selected="@(Model.Transaction.MerchantId == merchant.Id)">@merchant.Name</option>
}
<option value="__custom__" selected="@Model.UseCustomMerchant">+ Enter custom merchant</option>
</select>
<div id="customMerchantInput" style="display: @(Model.UseCustomMerchant ? "block" : "none")">
<input asp-for="Transaction.MerchantName"
class="form-control"
placeholder="Enter merchant name" />
</div>
<input type="hidden" asp-for="Transaction.MerchantId" id="merchantHidden" />
<div class="form-text">Select a merchant or create a new one</div>
<span asp-validation-for="Transaction.MerchantId" class="text-danger"></span>
</div>
<div class="mb-3">
<label asp-for="Transaction.Notes" class="form-label fw-bold">Notes</label>
<textarea asp-for="Transaction.Notes"
@@ -249,14 +272,32 @@
}
}
function handleMerchantChange() {
const select = document.getElementById('merchantSelect');
const customInput = document.getElementById('customMerchantInput');
const hiddenInput = document.getElementById('merchantHidden');
const merchantNameInput = document.querySelector('input[name="Transaction.MerchantName"]');
if (select.value === '__custom__') {
customInput.style.display = 'block';
hiddenInput.value = '';
merchantNameInput.value = '';
merchantNameInput.focus();
} else {
customInput.style.display = 'none';
hiddenInput.value = select.value;
merchantNameInput.value = '';
}
}
// Update hidden field when custom input changes
document.addEventListener('DOMContentLoaded', function() {
const categoryInput = document.querySelector('input[name="Transaction.Category"]');
const select = document.getElementById('categorySelect');
const categorySelect = document.getElementById('categorySelect');
if (categoryInput) {
categoryInput.addEventListener('input', function() {
if (select.value === '__custom__') {
if (categorySelect.value === '__custom__') {
// Keep custom selected when typing
}
});
@@ -264,6 +305,7 @@
// Initialize on page load
handleCategoryChange();
handleMerchantChange();
});
</script>
}

View File

@@ -33,7 +33,11 @@ namespace MoneyMap.Pages
[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]
@@ -49,6 +53,7 @@ namespace MoneyMap.Pages
.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);
@@ -64,6 +69,7 @@ namespace MoneyMap.Pages
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"
@@ -76,11 +82,16 @@ namespace MoneyMap.Pages
}).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();
}
@@ -92,6 +103,9 @@ namespace MoneyMap.Pages
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();
@@ -111,6 +125,37 @@ namespace MoneyMap.Pages
? ""
: Transaction.Notes.Trim();
// Update merchant
if (!string.IsNullOrWhiteSpace(Transaction.MerchantName))
{
// Create new merchant if custom name was entered
var merchantName = Transaction.MerchantName.Trim();
var existingMerchant = await _db.Merchants
.FirstOrDefaultAsync(m => m.Name == merchantName);
if (existingMerchant != null)
{
transaction.MerchantId = existingMerchant.Id;
}
else
{
var newMerchant = new Merchant { Name = merchantName };
_db.Merchants.Add(newMerchant);
await _db.SaveChangesAsync();
transaction.MerchantId = newMerchant.Id;
}
}
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!";
@@ -175,6 +220,7 @@ namespace MoneyMap.Pages
private async Task LoadDataAsync()
{
await LoadAvailableCategoriesAsync();
await LoadAvailableMerchantsAsync();
var transaction = await _db.Transactions
.Include(t => t.Receipts)
@@ -201,6 +247,13 @@ namespace MoneyMap.Pages
.ToListAsync();
}
private async Task LoadAvailableMerchantsAsync()
{
AvailableMerchants = await _db.Merchants
.OrderBy(m => m.Name)
.ToListAsync();
}
public class TransactionEditModel
{
public long Id { get; set; }
@@ -209,6 +262,8 @@ namespace MoneyMap.Pages
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; } = "";