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:
@@ -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>
|
||||
}
|
||||
@@ -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; } = "";
|
||||
|
||||
Reference in New Issue
Block a user