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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<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 class="form-control-plaintext">@Model.Transaction.Name</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,6 +98,29 @@
|
|||||||
<span asp-validation-for="Transaction.Category" class="text-danger"></span>
|
<span asp-validation-for="Transaction.Category" class="text-danger"></span>
|
||||||
</div>
|
</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">
|
<div class="mb-3">
|
||||||
<label asp-for="Transaction.Notes" class="form-label fw-bold">Notes</label>
|
<label asp-for="Transaction.Notes" class="form-label fw-bold">Notes</label>
|
||||||
<textarea asp-for="Transaction.Notes"
|
<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
|
// Update hidden field when custom input changes
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const categoryInput = document.querySelector('input[name="Transaction.Category"]');
|
const categoryInput = document.querySelector('input[name="Transaction.Category"]');
|
||||||
const select = document.getElementById('categorySelect');
|
const categorySelect = document.getElementById('categorySelect');
|
||||||
|
|
||||||
if (categoryInput) {
|
if (categoryInput) {
|
||||||
categoryInput.addEventListener('input', function() {
|
categoryInput.addEventListener('input', function() {
|
||||||
if (select.value === '__custom__') {
|
if (categorySelect.value === '__custom__') {
|
||||||
// Keep custom selected when typing
|
// Keep custom selected when typing
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -264,6 +305,7 @@
|
|||||||
|
|
||||||
// Initialize on page load
|
// Initialize on page load
|
||||||
handleCategoryChange();
|
handleCategoryChange();
|
||||||
|
handleMerchantChange();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,11 @@ namespace MoneyMap.Pages
|
|||||||
[BindProperty]
|
[BindProperty]
|
||||||
public bool UseCustomCategory { get; set; }
|
public bool UseCustomCategory { get; set; }
|
||||||
|
|
||||||
|
[BindProperty]
|
||||||
|
public bool UseCustomMerchant { get; set; }
|
||||||
|
|
||||||
public List<string> AvailableCategories { get; set; } = new();
|
public List<string> AvailableCategories { get; set; } = new();
|
||||||
|
public List<Merchant> AvailableMerchants { get; set; } = new();
|
||||||
public List<ReceiptWithItems> Receipts { get; set; } = new();
|
public List<ReceiptWithItems> Receipts { get; set; } = new();
|
||||||
|
|
||||||
[TempData]
|
[TempData]
|
||||||
@@ -49,6 +53,7 @@ namespace MoneyMap.Pages
|
|||||||
.ThenInclude(c => c!.Account)
|
.ThenInclude(c => c!.Account)
|
||||||
.Include(t => t.Account)
|
.Include(t => t.Account)
|
||||||
.Include(t => t.TransferToAccount)
|
.Include(t => t.TransferToAccount)
|
||||||
|
.Include(t => t.Merchant)
|
||||||
.Include(t => t.Receipts)
|
.Include(t => t.Receipts)
|
||||||
.ThenInclude(r => r.LineItems)
|
.ThenInclude(r => r.LineItems)
|
||||||
.FirstOrDefaultAsync(t => t.Id == id);
|
.FirstOrDefaultAsync(t => t.Id == id);
|
||||||
@@ -64,6 +69,7 @@ namespace MoneyMap.Pages
|
|||||||
Memo = transaction.Memo,
|
Memo = transaction.Memo,
|
||||||
Amount = transaction.Amount,
|
Amount = transaction.Amount,
|
||||||
Category = transaction.Category ?? "",
|
Category = transaction.Category ?? "",
|
||||||
|
MerchantId = transaction.MerchantId,
|
||||||
Notes = transaction.Notes ?? "",
|
Notes = transaction.Notes ?? "",
|
||||||
CardLabel = transaction.PaymentMethodLabel,
|
CardLabel = transaction.PaymentMethodLabel,
|
||||||
AccountLabel = transaction.Card?.Account?.DisplayLabel ?? transaction.Account?.DisplayLabel ?? "None"
|
AccountLabel = transaction.Card?.Account?.DisplayLabel ?? transaction.Account?.DisplayLabel ?? "None"
|
||||||
@@ -76,11 +82,16 @@ namespace MoneyMap.Pages
|
|||||||
}).ToList() ?? new List<ReceiptWithItems>();
|
}).ToList() ?? new List<ReceiptWithItems>();
|
||||||
|
|
||||||
await LoadAvailableCategoriesAsync();
|
await LoadAvailableCategoriesAsync();
|
||||||
|
await LoadAvailableMerchantsAsync();
|
||||||
|
|
||||||
// Check if current category exists in list
|
// Check if current category exists in list
|
||||||
UseCustomCategory = !string.IsNullOrWhiteSpace(Transaction.Category)
|
UseCustomCategory = !string.IsNullOrWhiteSpace(Transaction.Category)
|
||||||
&& !AvailableCategories.Contains(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();
|
return Page();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +103,9 @@ namespace MoneyMap.Pages
|
|||||||
ModelState.Remove("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)
|
if (!ModelState.IsValid)
|
||||||
{
|
{
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
@@ -111,6 +125,37 @@ namespace MoneyMap.Pages
|
|||||||
? ""
|
? ""
|
||||||
: Transaction.Notes.Trim();
|
: 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();
|
await _db.SaveChangesAsync();
|
||||||
|
|
||||||
SuccessMessage = "Transaction updated successfully!";
|
SuccessMessage = "Transaction updated successfully!";
|
||||||
@@ -175,6 +220,7 @@ namespace MoneyMap.Pages
|
|||||||
private async Task LoadDataAsync()
|
private async Task LoadDataAsync()
|
||||||
{
|
{
|
||||||
await LoadAvailableCategoriesAsync();
|
await LoadAvailableCategoriesAsync();
|
||||||
|
await LoadAvailableMerchantsAsync();
|
||||||
|
|
||||||
var transaction = await _db.Transactions
|
var transaction = await _db.Transactions
|
||||||
.Include(t => t.Receipts)
|
.Include(t => t.Receipts)
|
||||||
@@ -201,6 +247,13 @@ namespace MoneyMap.Pages
|
|||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadAvailableMerchantsAsync()
|
||||||
|
{
|
||||||
|
AvailableMerchants = await _db.Merchants
|
||||||
|
.OrderBy(m => m.Name)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public class TransactionEditModel
|
public class TransactionEditModel
|
||||||
{
|
{
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
@@ -209,6 +262,8 @@ namespace MoneyMap.Pages
|
|||||||
public string Memo { get; set; } = "";
|
public string Memo { get; set; } = "";
|
||||||
public decimal Amount { get; set; }
|
public decimal Amount { get; set; }
|
||||||
public string Category { get; set; } = "";
|
public string Category { get; set; } = "";
|
||||||
|
public int? MerchantId { get; set; }
|
||||||
|
public string? MerchantName { get; set; } = "";
|
||||||
public string? Notes { get; set; } = "";
|
public string? Notes { get; set; } = "";
|
||||||
public string CardLabel { get; set; } = "";
|
public string CardLabel { get; set; } = "";
|
||||||
public string AccountLabel { get; set; } = "";
|
public string AccountLabel { get; set; } = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user