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:
@@ -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)
|
|
||||||
{
|
{
|
||||||
<a class="dropdown-item" href="#" data-category="@group.Category">@group.Category</a>
|
<option value="@category">@category</option>
|
||||||
}
|
}
|
||||||
</div>
|
</datalist>
|
||||||
</div>
|
<div class="form-text">Select existing or type new category name</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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user