Add client-side spell column sorting and improve inventory search
- Implement client-side sorting for all columns including spell_names - Add computed_spell_names CTE for server-side sort fallback - Add resizable columns with localStorage persistence - Add Cloak slot detection by name pattern - Increase items limit to 50000 for full inventory loading - Increase proxy timeout to 60s for large queries - Remove pagination (all items loaded at once for sorting) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
25980edf99
commit
8cae94d87d
6 changed files with 294 additions and 111 deletions
|
|
@ -2188,7 +2188,7 @@
|
||||||
"dictionaries": {
|
"dictionaries": {
|
||||||
"AttributeSetInfo": {
|
"AttributeSetInfo": {
|
||||||
"name": "AttributeSetInfo",
|
"name": "AttributeSetInfo",
|
||||||
"description": "Equipment set names",
|
"description": "Equipment set names (complete from Dictionaries.cs)",
|
||||||
"values": {
|
"values": {
|
||||||
"2": "Test",
|
"2": "Test",
|
||||||
"4": "Carraida's Benediction",
|
"4": "Carraida's Benediction",
|
||||||
|
|
@ -2210,7 +2210,7 @@
|
||||||
"20": "Dexterous Set",
|
"20": "Dexterous Set",
|
||||||
"21": "Wise Set",
|
"21": "Wise Set",
|
||||||
"22": "Swift Set",
|
"22": "Swift Set",
|
||||||
"23": "Hardenend Set",
|
"23": "Hardened Set",
|
||||||
"24": "Reinforced Set",
|
"24": "Reinforced Set",
|
||||||
"25": "Interlocking Set",
|
"25": "Interlocking Set",
|
||||||
"26": "Flame Proof Set",
|
"26": "Flame Proof Set",
|
||||||
|
|
@ -2232,15 +2232,102 @@
|
||||||
"42": "Olthoi Armor D Red",
|
"42": "Olthoi Armor D Red",
|
||||||
"43": "Olthoi Armor C Rat",
|
"43": "Olthoi Armor C Rat",
|
||||||
"44": "Olthoi Armor C Red",
|
"44": "Olthoi Armor C Red",
|
||||||
"45": "Olthoi Armor F Red",
|
"45": "Olthoi Armor D Rat",
|
||||||
"46": "Olthoi Armor K Red",
|
"46": "Upgraded Relic Alduressa Set",
|
||||||
"47": "Olthoi Armor M Red",
|
"47": "Upgraded Ancient Relic Set",
|
||||||
"48": "Olthoi Armor B Red",
|
"48": "Upgraded Noble Relic Set",
|
||||||
"49": "Olthoi Armor B Rat",
|
"49": "Weave of Alchemy",
|
||||||
"50": "Olthoi Armor K Rat",
|
"50": "Weave of Arcane Lore",
|
||||||
"51": "Olthoi Armor M Rat",
|
"51": "Weave of Armor Tinkering",
|
||||||
"52": "Olthoi Armor F Rat",
|
"52": "Weave of Assess Person",
|
||||||
"53": "Olthoi Armor D Rat"
|
"53": "Weave of Light Weapons",
|
||||||
|
"54": "Weave of Missile Weapons",
|
||||||
|
"55": "Weave of Cooking",
|
||||||
|
"56": "Weave of Creature Enchantment",
|
||||||
|
"57": "Weave of Missile Weapons",
|
||||||
|
"58": "Weave of Finesse",
|
||||||
|
"59": "Weave of Deception",
|
||||||
|
"60": "Weave of Fletching",
|
||||||
|
"61": "Weave of Healing",
|
||||||
|
"62": "Weave of Item Enchantment",
|
||||||
|
"63": "Weave of Item Tinkering",
|
||||||
|
"64": "Weave of Leadership",
|
||||||
|
"65": "Weave of Life Magic",
|
||||||
|
"66": "Weave of Loyalty",
|
||||||
|
"67": "Weave of Light Weapons",
|
||||||
|
"68": "Weave of Magic Defense",
|
||||||
|
"69": "Weave of Magic Item Tinkering",
|
||||||
|
"70": "Weave of Mana Conversion",
|
||||||
|
"71": "Weave of Melee Defense",
|
||||||
|
"72": "Weave of Missile Defense",
|
||||||
|
"73": "Weave of Salvaging",
|
||||||
|
"74": "Weave of Light Weapons",
|
||||||
|
"75": "Weave of Light Weapons",
|
||||||
|
"76": "Weave of Heavy Weapons",
|
||||||
|
"77": "Weave of Missile Weapons",
|
||||||
|
"78": "Weave of Two Handed Combat",
|
||||||
|
"79": "Weave of Light Weapons",
|
||||||
|
"80": "Weave of Void Magic",
|
||||||
|
"81": "Weave of War Magic",
|
||||||
|
"82": "Weave of Weapon Tinkering",
|
||||||
|
"83": "Weave of Assess Creature",
|
||||||
|
"84": "Weave of Dirty Fighting",
|
||||||
|
"85": "Weave of Dual Wield",
|
||||||
|
"86": "Weave of Recklessness",
|
||||||
|
"87": "Weave of Shield",
|
||||||
|
"88": "Weave of Sneak Attack",
|
||||||
|
"89": "Ninja_New",
|
||||||
|
"90": "Weave of Summoning",
|
||||||
|
"91": "Shrouded Soul",
|
||||||
|
"92": "Darkened Mind",
|
||||||
|
"93": "Clouded Spirit",
|
||||||
|
"94": "Minor Stinging Shrouded Soul",
|
||||||
|
"95": "Minor Sparking Shrouded Soul",
|
||||||
|
"96": "Minor Smoldering Shrouded Soul",
|
||||||
|
"97": "Minor Shivering Shrouded Soul",
|
||||||
|
"98": "Minor Stinging Darkened Mind",
|
||||||
|
"99": "Minor Sparking Darkened Mind",
|
||||||
|
"100": "Minor Smoldering Darkened Mind",
|
||||||
|
"101": "Minor Shivering Darkened Mind",
|
||||||
|
"102": "Minor Stinging Clouded Spirit",
|
||||||
|
"103": "Minor Sparking Clouded Spirit",
|
||||||
|
"104": "Minor Smoldering Clouded Spirit",
|
||||||
|
"105": "Minor Shivering Clouded Spirit",
|
||||||
|
"106": "Major Stinging Shrouded Soul",
|
||||||
|
"107": "Major Sparking Shrouded Soul",
|
||||||
|
"108": "Major Smoldering Shrouded Soul",
|
||||||
|
"109": "Major Shivering Shrouded Soul",
|
||||||
|
"110": "Major Stinging Darkened Mind",
|
||||||
|
"111": "Major Sparking Darkened Mind",
|
||||||
|
"112": "Major Smoldering Darkened Mind",
|
||||||
|
"113": "Major Shivering Darkened Mind",
|
||||||
|
"114": "Major Stinging Clouded Spirit",
|
||||||
|
"115": "Major Sparking Clouded Spirit",
|
||||||
|
"116": "Major Smoldering Clouded Spirit",
|
||||||
|
"117": "Major Shivering Clouded Spirit",
|
||||||
|
"118": "Blackfire Stinging Shrouded Soul",
|
||||||
|
"119": "Blackfire Sparking Shrouded Soul",
|
||||||
|
"120": "Blackfire Smoldering Shrouded Soul",
|
||||||
|
"121": "Blackfire Shivering Shrouded Soul",
|
||||||
|
"122": "Blackfire Stinging Darkened Mind",
|
||||||
|
"123": "Blackfire Sparking Darkened Mind",
|
||||||
|
"124": "Blackfire Smoldering Darkened Mind",
|
||||||
|
"125": "Blackfire Shivering Darkened Mind",
|
||||||
|
"126": "Blackfire Stinging Clouded Spirit",
|
||||||
|
"127": "Blackfire Sparking Clouded Spirit",
|
||||||
|
"128": "Blackfire Smoldering Clouded Spirit",
|
||||||
|
"129": "Blackfire Shivering Clouded Spirit",
|
||||||
|
"130": "Shimmering Shadows",
|
||||||
|
"131": "Brown Society Locket",
|
||||||
|
"132": "Yellow Society Locket",
|
||||||
|
"133": "Red Society Band",
|
||||||
|
"134": "Green Society Band",
|
||||||
|
"135": "Purple Society Band",
|
||||||
|
"136": "Blue Society Band",
|
||||||
|
"137": "Gauntlet Garb",
|
||||||
|
"138": "Paragon Missile Weapons",
|
||||||
|
"139": "Paragon Casters",
|
||||||
|
"140": "Paragon Melee Weapons"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2085,7 +2085,7 @@ async def search_items(
|
||||||
sort_by: str = Query("name", description="Sort field: name, value, damage, armor, workmanship, damage_rating, crit_damage_rating, level"),
|
sort_by: str = Query("name", description="Sort field: name, value, damage, armor, workmanship, damage_rating, crit_damage_rating, level"),
|
||||||
sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
|
sort_dir: str = Query("asc", description="Sort direction: asc or desc"),
|
||||||
page: int = Query(1, ge=1, description="Page number"),
|
page: int = Query(1, ge=1, description="Page number"),
|
||||||
limit: int = Query(200, ge=1, le=10000, description="Items per page")
|
limit: int = Query(200, ge=1, le=50000, description="Items per page")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Search items across characters with comprehensive filtering options.
|
Search items across characters with comprehensive filtering options.
|
||||||
|
|
@ -2236,9 +2236,19 @@ async def search_items(
|
||||||
-- Check wielded location for two-handed weapons
|
-- Check wielded location for two-handed weapons
|
||||||
WHEN i.current_wielded_location = 67108864 THEN 'Two-Handed'
|
WHEN i.current_wielded_location = 67108864 THEN 'Two-Handed'
|
||||||
|
|
||||||
|
-- CLOAKS: Identify by name pattern
|
||||||
|
WHEN i.name ILIKE '%cloak%' THEN 'Cloak'
|
||||||
|
|
||||||
-- DEFAULT
|
-- DEFAULT
|
||||||
ELSE '-'
|
ELSE '-'
|
||||||
END as computed_slot_name
|
END as computed_slot_name,
|
||||||
|
|
||||||
|
-- Compute spell_names (spell IDs for client-side name lookup)
|
||||||
|
COALESCE(
|
||||||
|
(SELECT STRING_AGG(CAST(sp_inner.spell_id AS VARCHAR), ',' ORDER BY sp_inner.spell_id)
|
||||||
|
FROM item_spells sp_inner WHERE sp_inner.item_id = i.id),
|
||||||
|
''
|
||||||
|
) as computed_spell_names
|
||||||
|
|
||||||
FROM items i
|
FROM items i
|
||||||
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
LEFT JOIN item_combat_stats cs ON i.id = cs.item_id
|
||||||
|
|
@ -2495,6 +2505,10 @@ async def search_items(
|
||||||
slot_approaches.append("(object_class = 11 AND name ILIKE '%trinket%')")
|
slot_approaches.append("(object_class = 11 AND name ILIKE '%trinket%')")
|
||||||
# 4. Jewelry fallback: items that don't match other jewelry patterns
|
# 4. Jewelry fallback: items that don't match other jewelry patterns
|
||||||
slot_approaches.append("(object_class = 4 AND name NOT ILIKE '%ring%' AND name NOT ILIKE '%bracelet%' AND name NOT ILIKE '%amulet%' AND name NOT ILIKE '%necklace%' AND name NOT ILIKE '%gorget%')")
|
slot_approaches.append("(object_class = 4 AND name NOT ILIKE '%ring%' AND name NOT ILIKE '%bracelet%' AND name NOT ILIKE '%amulet%' AND name NOT ILIKE '%necklace%' AND name NOT ILIKE '%gorget%')")
|
||||||
|
elif slot_name.lower() == 'cloak':
|
||||||
|
# For cloaks: identify by name pattern
|
||||||
|
slot_approaches.append("(name ILIKE '%cloak%')")
|
||||||
|
slot_approaches.append("(computed_slot_name = 'Cloak')")
|
||||||
|
|
||||||
# Combine approaches with OR (any approach can match)
|
# Combine approaches with OR (any approach can match)
|
||||||
if slot_approaches:
|
if slot_approaches:
|
||||||
|
|
@ -2663,9 +2677,11 @@ async def search_items(
|
||||||
# Add ORDER BY
|
# Add ORDER BY
|
||||||
sort_mapping = {
|
sort_mapping = {
|
||||||
"name": "name",
|
"name": "name",
|
||||||
|
"character_name": "character_name",
|
||||||
"value": "value",
|
"value": "value",
|
||||||
"damage": "max_damage",
|
"damage": "max_damage",
|
||||||
"armor": "armor_level",
|
"armor": "armor_level",
|
||||||
|
"armor_level": "armor_level",
|
||||||
"workmanship": "workmanship",
|
"workmanship": "workmanship",
|
||||||
"level": "wield_level",
|
"level": "wield_level",
|
||||||
"damage_rating": "damage_rating",
|
"damage_rating": "damage_rating",
|
||||||
|
|
@ -2673,11 +2689,20 @@ async def search_items(
|
||||||
"heal_boost_rating": "heal_boost_rating",
|
"heal_boost_rating": "heal_boost_rating",
|
||||||
"vitality_rating": "vitality_rating",
|
"vitality_rating": "vitality_rating",
|
||||||
"damage_resist_rating": "damage_resist_rating",
|
"damage_resist_rating": "damage_resist_rating",
|
||||||
"crit_damage_resist_rating": "crit_damage_resist_rating"
|
"crit_damage_resist_rating": "crit_damage_resist_rating",
|
||||||
|
"item_set": "item_set",
|
||||||
|
"slot_name": "computed_slot_name",
|
||||||
|
"coverage": "coverage_mask",
|
||||||
|
"item_type_name": "object_class",
|
||||||
|
"last_updated": "timestamp",
|
||||||
|
"spell_names": "computed_spell_names"
|
||||||
}
|
}
|
||||||
sort_field = sort_mapping.get(sort_by, "name")
|
sort_field = sort_mapping.get(sort_by, "name")
|
||||||
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
|
sort_direction = "DESC" if sort_dir.lower() == "desc" else "ASC"
|
||||||
query_parts.append(f"ORDER BY {sort_field} {sort_direction}")
|
|
||||||
|
# Handle NULLS for optional fields
|
||||||
|
nulls_clause = "NULLS LAST" if sort_direction == "ASC" else "NULLS FIRST"
|
||||||
|
query_parts.append(f"ORDER BY {sort_field} {sort_direction} {nulls_clause}")
|
||||||
|
|
||||||
# Add pagination
|
# Add pagination
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
|
||||||
4
main.py
4
main.py
|
|
@ -2275,8 +2275,8 @@ async def proxy_inventory_service(path: str, request: Request):
|
||||||
inventory_service_url = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-service:8000')
|
inventory_service_url = os.getenv('INVENTORY_SERVICE_URL', 'http://inventory-service:8000')
|
||||||
logger.info(f"Proxying to inventory service: {inventory_service_url}/{path}")
|
logger.info(f"Proxying to inventory service: {inventory_service_url}/{path}")
|
||||||
|
|
||||||
# Forward the request to inventory service
|
# Forward the request to inventory service (60s timeout for large queries)
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
response = await client.request(
|
response = await client.request(
|
||||||
method=request.method,
|
method=request.method,
|
||||||
url=f"{inventory_service_url}/{path}",
|
url=f"{inventory_service_url}/{path}",
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #000;
|
color: #000;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-table th {
|
.results-table th {
|
||||||
|
|
@ -276,11 +277,36 @@
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resizable column handle */
|
||||||
|
.results-table th .resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 5px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table th .resize-handle:hover,
|
||||||
|
.results-table th .resize-handle.resizing {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-table th.resizing {
|
||||||
|
background: #b0b0b0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-table td {
|
.results-table td {
|
||||||
padding: 1px 3px;
|
padding: 1px 3px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-table tbody tr:hover {
|
.results-table tbody tr:hover {
|
||||||
|
|
@ -542,6 +568,9 @@
|
||||||
<option value="Right Wrist">Right Wrist</option>
|
<option value="Right Wrist">Right Wrist</option>
|
||||||
<option value="Trinket">Trinket</option>
|
<option value="Trinket">Trinket</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
<optgroup label="Other Slots">
|
||||||
|
<option value="Cloak">Cloak</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -944,6 +973,10 @@
|
||||||
<input type="checkbox" id="slot_trinket" value="Trinket">
|
<input type="checkbox" id="slot_trinket" value="Trinket">
|
||||||
<label for="slot_trinket">Trinket</label>
|
<label for="slot_trinket">Trinket</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-item">
|
||||||
|
<input type="checkbox" id="slot_cloak" value="Cloak">
|
||||||
|
<label for="slot_cloak">Cloak</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,9 @@ let currentSort = {
|
||||||
// Store current search results for client-side sorting
|
// Store current search results for client-side sorting
|
||||||
let currentResultsData = null;
|
let currentResultsData = null;
|
||||||
|
|
||||||
// Pagination state
|
// Items limit - load all items for client-side sorting (backend max is 50000)
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let itemsPerPage = 5000; // 5k items per page for good performance
|
let itemsPerPage = 50000;
|
||||||
let totalPages = 1;
|
|
||||||
|
|
||||||
// Initialize the application
|
// Initialize the application
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
@ -44,7 +43,7 @@ function initializeEventListeners() {
|
||||||
// Form submission
|
// Form submission
|
||||||
searchForm.addEventListener('submit', async (e) => {
|
searchForm.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await performSearch(true); // Reset to page 1 on new search
|
await performSearch();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear button
|
// Clear button
|
||||||
|
|
@ -172,9 +171,8 @@ function clearAllFields() {
|
||||||
// Reset slot filter
|
// Reset slot filter
|
||||||
document.getElementById('slotFilter').value = '';
|
document.getElementById('slotFilter').value = '';
|
||||||
|
|
||||||
// Reset pagination
|
// Reset page
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
// Reset results and clear stored data
|
// Reset results and clear stored data
|
||||||
currentResultsData = null;
|
currentResultsData = null;
|
||||||
|
|
@ -205,12 +203,8 @@ function handleSlotFilterChange() {
|
||||||
/**
|
/**
|
||||||
* Perform the search based on form inputs
|
* Perform the search based on form inputs
|
||||||
*/
|
*/
|
||||||
async function performSearch(resetPage = false) {
|
async function performSearch() {
|
||||||
// Reset to page 1 if this is a new search (not pagination)
|
currentPage = 1;
|
||||||
if (resetPage) {
|
|
||||||
currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
|
searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -228,9 +222,6 @@ async function performSearch(resetPage = false) {
|
||||||
// Store results for client-side re-sorting
|
// Store results for client-side re-sorting
|
||||||
currentResultsData = data;
|
currentResultsData = data;
|
||||||
|
|
||||||
// Update pagination state
|
|
||||||
updatePaginationState(data);
|
|
||||||
|
|
||||||
// Apply client-side slot filtering
|
// Apply client-side slot filtering
|
||||||
applySlotFilter(data);
|
applySlotFilter(data);
|
||||||
|
|
||||||
|
|
@ -396,12 +387,8 @@ function getSelectedProtections() {
|
||||||
*/
|
*/
|
||||||
function getSelectedSlots() {
|
function getSelectedSlots() {
|
||||||
const selectedSlots = [];
|
const selectedSlots = [];
|
||||||
// Get armor slots
|
// Get all selected slot checkboxes from the all-slots container
|
||||||
document.querySelectorAll('#armor-slots input[type="checkbox"]:checked').forEach(cb => {
|
document.querySelectorAll('#all-slots input[type="checkbox"]:checked').forEach(cb => {
|
||||||
selectedSlots.push(cb.value);
|
|
||||||
});
|
|
||||||
// Get jewelry slots
|
|
||||||
document.querySelectorAll('#jewelry-slots input[type="checkbox"]:checked').forEach(cb => {
|
|
||||||
selectedSlots.push(cb.value);
|
selectedSlots.push(cb.value);
|
||||||
});
|
});
|
||||||
return selectedSlots;
|
return selectedSlots;
|
||||||
|
|
@ -505,12 +492,16 @@ function displayResults(data) {
|
||||||
// Get item type for display
|
// Get item type for display
|
||||||
const itemType = item.item_type_name || '-';
|
const itemType = item.item_type_name || '-';
|
||||||
|
|
||||||
// Format equipment set name
|
// Format equipment set name - prefer translated name
|
||||||
let setDisplay = '-';
|
let setDisplay = '-';
|
||||||
if (item.item_set) {
|
if (item.item_set_name) {
|
||||||
// Remove redundant "Set" prefix if present
|
// Use the translated set name from backend
|
||||||
|
setDisplay = item.item_set_name;
|
||||||
|
// Remove redundant "Set" suffix if present for cleaner display
|
||||||
|
setDisplay = setDisplay.replace(/\s+Set$/i, '');
|
||||||
|
} else if (item.item_set) {
|
||||||
|
// Fallback to raw set ID if name not available
|
||||||
setDisplay = item.item_set.replace(/^Set\s+/i, '');
|
setDisplay = item.item_set.replace(/^Set\s+/i, '');
|
||||||
// Also handle if it ends with " Set"
|
|
||||||
setDisplay = setDisplay.replace(/\s+Set$/i, '');
|
setDisplay = setDisplay.replace(/\s+Set$/i, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -540,22 +531,6 @@ function displayResults(data) {
|
||||||
</table>
|
</table>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add pagination controls if needed
|
|
||||||
if (totalPages > 1) {
|
|
||||||
const isFirstPage = currentPage === 1;
|
|
||||||
const isLastPage = currentPage === totalPages;
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div class="pagination-controls">
|
|
||||||
<button onclick="goToPage(1)" ${isFirstPage ? 'disabled' : ''}>First</button>
|
|
||||||
<button onclick="previousPage()" ${isFirstPage ? 'disabled' : ''}>← Previous</button>
|
|
||||||
<span class="pagination-info">Page ${currentPage} of ${totalPages} (${data.total_count} total items)</span>
|
|
||||||
<button onclick="nextPage()" ${isLastPage ? 'disabled' : ''}>Next →</button>
|
|
||||||
<button onclick="goToPage(${totalPages})" ${isLastPage ? 'disabled' : ''}>Last</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchResults.innerHTML = html;
|
searchResults.innerHTML = html;
|
||||||
|
|
||||||
// Add click event listeners to sortable headers
|
// Add click event listeners to sortable headers
|
||||||
|
|
@ -565,6 +540,98 @@ function displayResults(data) {
|
||||||
handleSort(sortField);
|
handleSort(sortField);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize resizable columns
|
||||||
|
initResizableColumns();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize resizable columns for the results table
|
||||||
|
*/
|
||||||
|
function initResizableColumns() {
|
||||||
|
const table = document.querySelector('.results-table');
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
const headers = table.querySelectorAll('th');
|
||||||
|
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
// Skip last column (no need to resize)
|
||||||
|
if (index === headers.length - 1) return;
|
||||||
|
|
||||||
|
// Create resize handle
|
||||||
|
const resizeHandle = document.createElement('div');
|
||||||
|
resizeHandle.className = 'resize-handle';
|
||||||
|
header.appendChild(resizeHandle);
|
||||||
|
|
||||||
|
let startX, startWidth;
|
||||||
|
|
||||||
|
resizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation(); // Prevent sorting when resizing
|
||||||
|
|
||||||
|
startX = e.pageX;
|
||||||
|
startWidth = header.offsetWidth;
|
||||||
|
|
||||||
|
header.classList.add('resizing');
|
||||||
|
resizeHandle.classList.add('resizing');
|
||||||
|
|
||||||
|
const onMouseMove = (e) => {
|
||||||
|
const diff = e.pageX - startX;
|
||||||
|
const newWidth = Math.max(30, startWidth + diff); // Minimum 30px width
|
||||||
|
header.style.width = newWidth + 'px';
|
||||||
|
header.style.minWidth = newWidth + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
header.classList.remove('resizing');
|
||||||
|
resizeHandle.classList.remove('resizing');
|
||||||
|
document.removeEventListener('mousemove', onMouseMove);
|
||||||
|
document.removeEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
// Save column widths to localStorage
|
||||||
|
saveColumnWidths();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onMouseMove);
|
||||||
|
document.addEventListener('mouseup', onMouseUp);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restore saved column widths
|
||||||
|
restoreColumnWidths();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save column widths to localStorage
|
||||||
|
*/
|
||||||
|
function saveColumnWidths() {
|
||||||
|
const headers = document.querySelectorAll('.results-table th');
|
||||||
|
const widths = [];
|
||||||
|
headers.forEach(header => {
|
||||||
|
widths.push(header.style.width || '');
|
||||||
|
});
|
||||||
|
localStorage.setItem('inventoryColumnWidths', JSON.stringify(widths));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore column widths from localStorage
|
||||||
|
*/
|
||||||
|
function restoreColumnWidths() {
|
||||||
|
const saved = localStorage.getItem('inventoryColumnWidths');
|
||||||
|
if (!saved) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const widths = JSON.parse(saved);
|
||||||
|
const headers = document.querySelectorAll('.results-table th');
|
||||||
|
headers.forEach((header, index) => {
|
||||||
|
if (widths[index]) {
|
||||||
|
header.style.width = widths[index];
|
||||||
|
header.style.minWidth = widths[index];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to restore column widths:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Apply client-side slot filtering
|
* Apply client-side slot filtering
|
||||||
|
|
@ -636,7 +703,7 @@ function sortResults(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle column sorting
|
* Handle column sorting - client-side sorting for all columns
|
||||||
*/
|
*/
|
||||||
function handleSort(field) {
|
function handleSort(field) {
|
||||||
// If clicking the same field, toggle direction
|
// If clicking the same field, toggle direction
|
||||||
|
|
@ -648,9 +715,16 @@ function handleSort(field) {
|
||||||
currentSort.direction = 'asc';
|
currentSort.direction = 'asc';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset to page 1 and perform new search with updated sort
|
// If we have cached results, sort them client-side and re-display
|
||||||
currentPage = 1;
|
if (currentResultsData && currentResultsData.items) {
|
||||||
performSearch();
|
// Make a copy to preserve original order
|
||||||
|
const sortedData = JSON.parse(JSON.stringify(currentResultsData));
|
||||||
|
sortResults(sortedData);
|
||||||
|
displayResults(sortedData);
|
||||||
|
} else {
|
||||||
|
// No cached results, need to fetch first
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -794,40 +868,4 @@ function displaySetAnalysisResults(data) {
|
||||||
setAnalysisResults.innerHTML = html;
|
setAnalysisResults.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Pagination functions removed - using client-side sorting with all results loaded
|
||||||
* Update pagination state from API response
|
|
||||||
*/
|
|
||||||
function updatePaginationState(data) {
|
|
||||||
totalPages = data.total_pages || 1;
|
|
||||||
// Current page is already tracked in currentPage
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to a specific page
|
|
||||||
*/
|
|
||||||
function goToPage(page) {
|
|
||||||
if (page < 1 || page > totalPages || page === currentPage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPage = page;
|
|
||||||
performSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to next page
|
|
||||||
*/
|
|
||||||
function nextPage() {
|
|
||||||
if (currentPage < totalPages) {
|
|
||||||
goToPage(currentPage + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Go to previous page
|
|
||||||
*/
|
|
||||||
function previousPage() {
|
|
||||||
if (currentPage > 1) {
|
|
||||||
goToPage(currentPage - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue