Add autocomplete dropdowns for category and merchant in CategoryMappings

Improved UX for adding/editing category mappings:

**Backend Changes:**
- Added AvailableCategories list (distinct categories from existing mappings)
- Added AvailableMerchants list (all merchants from Merchants table)
- Updated LoadDataAsync() to populate both lists

**UI Changes:**
- Replaced plain text input with HTML5 datalist for Category field
- Replaced plain text input with HTML5 datalist for Merchant field
- Both fields support autocomplete while allowing new values
- Added "required" attribute to Category field for validation
- Added helper text to guide users
- Removed custom JavaScript dropdown logic (now using native datalist)

**Benefits:**
- Consistent category naming (autocomplete suggests existing categories)
- Consistent merchant naming (autocomplete suggests existing merchants)
- Better UX with native browser autocomplete behavior
- Still allows creating new categories/merchants on the fly
- Cleaner, simpler code without custom dropdown implementation

🤖 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 03:58:56 -04:00
parent b1143ad484
commit 45077a0029
2 changed files with 32 additions and 80 deletions

View File

@@ -157,20 +157,25 @@ else
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="addCategory" class="form-label">Category</label> <label for="addCategory" class="form-label">Category</label>
<div class="position-relative"> <input name="model.Category" id="addCategory" class="form-control" list="categoryList" placeholder="e.g., Online shopping" autocomplete="off" required />
<input name="model.Category" id="addCategory" class="form-control" placeholder="e.g., Online shopping" autocomplete="off" /> <datalist id="categoryList">
<div id="addCategoryDropdown" class="dropdown-menu" style="width: 100%; max-height: 200px; overflow-y: auto;"> @foreach (var category in Model.AvailableCategories)
@foreach (var group in Model.CategoryGroups) {
{ <option value="@category">@category</option>
<a class="dropdown-item" href="#" data-category="@group.Category">@group.Category</a> }
} </datalist>
</div> <div class="form-text">Select existing or type new category name</div>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="addMerchant" class="form-label">Merchant Name (Optional)</label> <label for="addMerchant" class="form-label">Merchant Name (Optional)</label>
<input name="model.Merchant" id="addMerchant" class="form-control" placeholder="e.g., Target" /> <input name="model.Merchant" id="addMerchant" class="form-control" list="merchantList" placeholder="e.g., Target" autocomplete="off" />
<datalist id="merchantList">
@foreach (var merchant in Model.AvailableMerchants)
{
<option value="@merchant">@merchant</option>
}
</datalist>
<div class="form-text">Friendly name to assign to matching transactions (e.g., "Walmart" instead of "WAL-MART #1234")</div> <div class="form-text">Friendly name to assign to matching transactions (e.g., "Walmart" instead of "WAL-MART #1234")</div>
</div> </div>
@@ -212,20 +217,13 @@ else
<div class="mb-3"> <div class="mb-3">
<label for="editCategory" class="form-label">Category</label> <label for="editCategory" class="form-label">Category</label>
<div class="position-relative"> <input name="model.Category" id="editCategory" class="form-control" list="categoryList" autocomplete="off" required />
<input name="model.Category" id="editCategory" class="form-control" autocomplete="off" /> <div class="form-text">Select existing or type new category name</div>
<div id="editCategoryDropdown" class="dropdown-menu" style="width: 100%; max-height: 200px; overflow-y: auto;">
@foreach (var group in Model.CategoryGroups)
{
<a class="dropdown-item" href="#" data-category="@group.Category">@group.Category</a>
}
</div>
</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="editMerchant" class="form-label">Merchant Name (Optional)</label> <label for="editMerchant" class="form-label">Merchant Name (Optional)</label>
<input name="model.Merchant" id="editMerchant" class="form-control" placeholder="e.g., Target" /> <input name="model.Merchant" id="editMerchant" class="form-control" list="merchantList" placeholder="e.g., Target" autocomplete="off" />
<div class="form-text">Friendly name to assign to matching transactions (e.g., "Walmart" instead of "WAL-MART #1234")</div> <div class="form-text">Friendly name to assign to matching transactions (e.g., "Walmart" instead of "WAL-MART #1234")</div>
</div> </div>
@@ -326,67 +324,7 @@ else
} }
} }
// Category dropdown functionality
function setupCategoryDropdown(inputId, dropdownId) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
if (!input || !dropdown) return;
const dropdownItems = dropdown.querySelectorAll('.dropdown-item');
// Show dropdown on focus/click
input.addEventListener('focus', function() {
filterDropdown('');
dropdown.classList.add('show');
});
input.addEventListener('click', function() {
filterDropdown('');
dropdown.classList.add('show');
});
// Filter dropdown as user types
input.addEventListener('input', function() {
filterDropdown(this.value);
dropdown.classList.add('show');
});
// Select category from dropdown
dropdownItems.forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const category = this.getAttribute('data-category');
input.value = category;
dropdown.classList.remove('show');
});
});
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Filter dropdown items based on search text
function filterDropdown(searchText) {
const search = searchText.toLowerCase();
dropdownItems.forEach(item => {
const category = item.getAttribute('data-category').toLowerCase();
if (category.includes(search)) {
item.style.display = '';
} else {
item.style.display = 'none';
}
});
}
}
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
setupCategoryDropdown('addCategory', 'addCategoryDropdown');
setupCategoryDropdown('editCategory', 'editCategoryDropdown');
// Debug: Log form values before submission // Debug: Log form values before submission
var addForm = document.querySelector('#addModal form'); var addForm = document.querySelector('#addModal form');

View File

@@ -29,6 +29,8 @@ namespace MoneyMap.Pages
public List<CategoryGroup> CategoryGroups { get; set; } = new(); public List<CategoryGroup> CategoryGroups { get; set; } = new();
public int TotalMappings { get; set; } public int TotalMappings { get; set; }
public int TotalCategories { get; set; } public int TotalCategories { get; set; }
public List<string> AvailableCategories { get; set; } = new();
public List<string> AvailableMerchants { get; set; } = new();
[TempData] [TempData]
public string? SuccessMessage { get; set; } public string? SuccessMessage { get; set; }
@@ -251,6 +253,18 @@ namespace MoneyMap.Pages
TotalMappings = mappings.Count; TotalMappings = mappings.Count;
TotalCategories = CategoryGroups.Count; TotalCategories = CategoryGroups.Count;
// Get distinct categories for dropdown
AvailableCategories = CategoryGroups
.Select(g => g.Category)
.OrderBy(c => c)
.ToList();
// Get all merchants for dropdown
AvailableMerchants = await _db.Merchants
.OrderBy(m => m.Name)
.Select(m => m.Name)
.ToListAsync();
} }
public class CategoryGroup public class CategoryGroup