/** * Inventory Search Application * Dedicated JavaScript for the inventory search page */ // Configuration - use main app proxy for inventory service const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' ? 'http://localhost:8766' // Local development - direct to inventory service : `${window.location.origin}/inv`; // Production - through main app proxy // DOM Elements - will be set after DOM loads let searchForm, clearBtn, searchResults; // Sorting state let currentSort = { field: 'name', direction: 'asc' }; // Store current search results for client-side sorting let currentResultsData = null; // Pagination state let currentPage = 1; let itemsPerPage = 5000; // 5k items per page for good performance let totalPages = 1; // Initialize the application document.addEventListener('DOMContentLoaded', function() { // Get DOM elements after DOM is loaded searchForm = document.getElementById('inventorySearchForm'); clearBtn = document.getElementById('clearBtn'); searchResults = document.getElementById('searchResults'); initializeEventListeners(); loadCharacterOptions(); }); /** * Initialize all event listeners */ function initializeEventListeners() { // Form submission searchForm.addEventListener('submit', async (e) => { e.preventDefault(); await performSearch(true); // Reset to page 1 on new search }); // Clear button clearBtn.addEventListener('click', clearAllFields); // Slot filter change document.getElementById('slotFilter').addEventListener('change', handleSlotFilterChange); // Set analysis buttons document.getElementById('setAnalysisBtn').addEventListener('click', showSetAnalysis); document.getElementById('backToSearch').addEventListener('click', showSearchSection); document.getElementById('runSetAnalysis').addEventListener('click', performSetAnalysis); // Checkbox visual feedback for cantrips and equipment sets document.querySelectorAll('.checkbox-item input[type="checkbox"]').forEach(checkbox => { checkbox.addEventListener('change', handleCheckboxChange); }); } /** * Load available characters for the checkbox list */ async function loadCharacterOptions() { try { // Use inventory service proxy endpoint for character list const response = await fetch(`${window.location.origin}/inventory-characters`); const data = await response.json(); if (data.characters && data.characters.length > 0) { const characterList = document.getElementById('characterList'); // Sort characters by name data.characters.sort((a, b) => a.character_name.localeCompare(b.character_name)); // Add character checkboxes data.characters.forEach(char => { const div = document.createElement('div'); div.className = 'character-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `char_${char.character_name}`; checkbox.value = char.character_name; checkbox.className = 'character-checkbox'; checkbox.checked = true; // Check all by default const label = document.createElement('label'); label.htmlFor = checkbox.id; label.textContent = char.character_name; div.appendChild(checkbox); div.appendChild(label); characterList.appendChild(div); }); // Set up event listeners for character selection setupCharacterCheckboxes(); } } catch (error) { console.warn('Could not load character list:', error); } } /** * Setup character checkbox functionality */ function setupCharacterCheckboxes() { const allCheckbox = document.getElementById('char_all'); const characterCheckboxes = document.querySelectorAll('.character-checkbox'); // Handle "All Characters" checkbox allCheckbox.addEventListener('change', function() { characterCheckboxes.forEach(cb => { cb.checked = this.checked; }); }); // Handle individual character checkboxes characterCheckboxes.forEach(cb => { cb.addEventListener('change', function() { // If any individual checkbox is unchecked, uncheck "All" if (!this.checked) { allCheckbox.checked = false; } else { // If all individual checkboxes are checked, check "All" const allChecked = Array.from(characterCheckboxes).every(checkbox => checkbox.checked); allCheckbox.checked = allChecked; } }); }); } /** * Handle checkbox change events for visual feedback */ function handleCheckboxChange(e) { const item = e.target.closest('.checkbox-item'); if (e.target.checked) { item.classList.add('checked'); } else { item.classList.remove('checked'); } } /** * Clear all form fields and checkboxes */ function clearAllFields() { searchForm.reset(); // Reset character selection to "All" document.getElementById('char_all').checked = true; document.querySelectorAll('.character-checkbox').forEach(cb => { cb.checked = true; }); // Clear checkbox visual states document.querySelectorAll('.checkbox-item').forEach(item => { item.classList.remove('checked'); }); // Reset equipment type to armor document.getElementById('armorOnly').checked = true; // Reset slot filter document.getElementById('slotFilter').value = ''; // Reset pagination currentPage = 1; totalPages = 1; // Reset results and clear stored data currentResultsData = null; searchResults.innerHTML = '
Enter search criteria above and click "Search Items" to find inventory items.
'; } /** * Handle slot filter changes */ function handleSlotFilterChange() { // If we have current results, reapply filtering and sorting if (currentResultsData) { // Reset items to original unfiltered data const originalData = JSON.parse(JSON.stringify(currentResultsData)); // Apply slot filtering applySlotFilter(originalData); // Apply sorting sortResults(originalData); // Display results displayResults(originalData); } } /** * Perform the search based on form inputs */ async function performSearch(resetPage = false) { // Reset to page 1 if this is a new search (not pagination) if (resetPage) { currentPage = 1; } searchResults.innerHTML = '
🔍 Searching inventory...
'; try { const params = buildSearchParameters(); const searchUrl = `${API_BASE}/search/items?${params.toString()}`; console.log('Search URL:', searchUrl); const response = await fetch(searchUrl); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || 'Search failed'); } // Store results for client-side re-sorting currentResultsData = data; // Update pagination state updatePaginationState(data); // Apply client-side slot filtering applySlotFilter(data); // Display results (already sorted by server) displayResults(data); } catch (error) { console.error('Search error:', error); searchResults.innerHTML = `
❌ Search failed: ${error.message}
`; } } /** * Build search parameters from form inputs */ function buildSearchParameters() { const params = new URLSearchParams(); // Equipment type selection const equipmentType = document.querySelector('input[name="equipmentType"]:checked').value; if (equipmentType === 'armor') { params.append('armor_only', 'true'); } else if (equipmentType === 'jewelry') { params.append('jewelry_only', 'true'); } // If 'all' is selected, don't add any type filter // Basic search parameters - handle character selection const allCharactersChecked = document.getElementById('char_all').checked; if (!allCharactersChecked) { // Get selected characters const selectedCharacters = Array.from(document.querySelectorAll('.character-checkbox:checked')) .map(cb => cb.value); if (selectedCharacters.length === 1) { // Single character selected params.append('character', selectedCharacters[0]); } else if (selectedCharacters.length > 1) { // Multiple characters - use comma-separated list params.append('characters', selectedCharacters.join(',')); } else { // No characters selected - search nothing return { items: [], total_count: 0, page: 1, total_pages: 0 }; } } else { // All characters selected params.append('include_all_characters', 'true'); } addParam(params, 'text', 'searchText'); addParam(params, 'material', 'searchMaterial'); const equipStatus = document.getElementById('searchEquipStatus').value; if (equipStatus && equipStatus !== 'all') { params.append('equipment_status', equipStatus); } // Armor statistics parameters addParam(params, 'min_armor', 'searchMinArmor'); addParam(params, 'max_armor', 'searchMaxArmor'); addParam(params, 'min_crit_damage_rating', 'searchMinCritDamage'); addParam(params, 'max_crit_damage_rating', 'searchMaxCritDamage'); addParam(params, 'min_damage_rating', 'searchMinDamageRating'); addParam(params, 'max_damage_rating', 'searchMaxDamageRating'); addParam(params, 'min_heal_boost_rating', 'searchMinHealBoost'); addParam(params, 'max_heal_boost_rating', 'searchMaxHealBoost'); addParam(params, 'min_vitality_rating', 'searchMinVitalityRating'); addParam(params, 'min_damage_resist_rating', 'searchMinDamageResistRating'); // Requirements parameters addParam(params, 'min_level', 'searchMinLevel'); addParam(params, 'max_level', 'searchMaxLevel'); addParam(params, 'min_workmanship', 'searchMinWorkmanship'); addParam(params, 'max_workmanship', 'searchMaxWorkmanship'); // Value parameters addParam(params, 'min_value', 'searchMinValue'); addParam(params, 'max_value', 'searchMaxValue'); addParam(params, 'max_burden', 'searchMaxBurden'); // Equipment set filters const selectedEquipmentSets = getSelectedEquipmentSets(); if (selectedEquipmentSets.length === 1) { params.append('item_set', selectedEquipmentSets[0]); } else if (selectedEquipmentSets.length > 1) { params.append('item_sets', selectedEquipmentSets.join(',')); } // Cantrip filters const selectedCantrips = getSelectedCantrips(); const selectedProtections = getSelectedProtections(); const allSpells = [...selectedCantrips, ...selectedProtections]; if (allSpells.length > 0) { params.append('legendary_cantrips', allSpells.join(',')); } // Equipment slot filters const selectedSlots = getSelectedSlots(); if (selectedSlots.length > 0) { params.append('slot_names', selectedSlots.join(',')); } // Pagination parameters params.append('page', currentPage); params.append('limit', itemsPerPage); // Sorting parameters params.append('sort_by', currentSort.field); params.append('sort_dir', currentSort.direction); return params; } /** * Helper function to add parameter if value exists */ function addParam(params, paramName, elementId) { const value = document.getElementById(elementId)?.value?.trim(); if (value) { params.append(paramName, value); } } /** * Get selected equipment sets */ function getSelectedEquipmentSets() { const selectedSets = []; document.querySelectorAll('#equipmentSets input[type="checkbox"]:checked').forEach(cb => { selectedSets.push(cb.value); }); return selectedSets; } /** * Get selected legendary cantrips */ function getSelectedCantrips() { const selectedCantrips = []; document.querySelectorAll('#cantrips input[type="checkbox"]:checked').forEach(cb => { selectedCantrips.push(cb.value); }); return selectedCantrips; } /** * Get selected protection spells */ function getSelectedProtections() { const selectedProtections = []; document.querySelectorAll('#protections input[type="checkbox"]:checked').forEach(cb => { selectedProtections.push(cb.value); }); return selectedProtections; } /** * Get selected equipment slots from checkboxes */ function getSelectedSlots() { const selectedSlots = []; // Get armor slots document.querySelectorAll('#armor-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); }); return selectedSlots; } /** * Display search results in the UI */ function displayResults(data) { if (!data.items || data.items.length === 0) { searchResults.innerHTML = '
No items found matching your search criteria.
'; return; } const getSortIcon = (field) => { if (currentSort.field === field) { return currentSort.direction === 'asc' ? ' ▲' : ' ▼'; } return ''; }; let html = `
Found ${data.total_count} items - Showing all results
`; data.items.forEach((item) => { const armor = item.armor_level > 0 ? item.armor_level : '-'; const critDmg = item.crit_damage_rating > 0 ? item.crit_damage_rating : '-'; const dmgRating = item.damage_rating > 0 ? item.damage_rating : '-'; const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-'; const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-'; const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-'; const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory'; const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory'; // Use the slot_name provided by the API instead of incorrect mapping // Replace commas with line breaks for better display const slot = item.slot_name ? item.slot_name.replace(/,\s*/g, '
') : 'Unknown'; // Coverage placeholder - will need to be added to backend later // Replace commas with line breaks for better display const coverage = item.coverage ? item.coverage.replace(/,\s*/g, '
') : '-'; // Format last updated timestamp const lastUpdated = item.last_updated ? new Date(item.last_updated).toLocaleString('sv-SE', { timeZone: 'Europe/Stockholm', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) : '-'; // Use the formatted name with material from the API let displayName = item.name; // The API should already include material in the name, but use material_name if available if (item.material_name && item.material_name !== '' && !item.name.toLowerCase().includes(item.material_name.toLowerCase())) { displayName = `${item.material_name} ${item.name}`; } // Format spells/cantrips list let spellsDisplay = '-'; if (item.spell_names && item.spell_names.length > 0) { // Highlight legendary cantrips in a different color const formattedSpells = item.spell_names.map(spell => { if (spell.toLowerCase().includes('legendary')) { return `${spell}`; } else { return `${spell}`; } }); spellsDisplay = formattedSpells.join('
'); } // Get item type for display const itemType = item.item_type_name || '-'; // Format equipment set name let setDisplay = '-'; if (item.item_set) { // Remove redundant "Set" prefix if present setDisplay = item.item_set.replace(/^Set\s+/i, ''); // Also handle if it ends with " Set" setDisplay = setDisplay.replace(/\s+Set$/i, ''); } html += ` `; }); html += `
Character${getSortIcon('character_name')} Item Name${getSortIcon('name')} Type${getSortIcon('item_type_name')} Slot${getSortIcon('slot_name')} Coverage${getSortIcon('coverage')} Armor${getSortIcon('armor')} Set${getSortIcon('item_set')} Spells/Cantrips${getSortIcon('spell_names')} Crit Dmg${getSortIcon('crit_damage_rating')} Dmg Rating${getSortIcon('damage_rating')} Heal Boost${getSortIcon('heal_boost_rating')} Vitality${getSortIcon('vitality_rating')} Dmg Resist${getSortIcon('damage_resist_rating')} Last Updated${getSortIcon('last_updated')}
${item.character_name} ${displayName} ${itemType} ${slot} ${coverage} ${armor} ${setDisplay} ${spellsDisplay} ${critDmg} ${dmgRating} ${healBoostRating} ${vitalityRating} ${damageResistRating} ${lastUpdated}
`; // Add pagination controls if needed if (totalPages > 1) { const isFirstPage = currentPage === 1; const isLastPage = currentPage === totalPages; html += `
Page ${currentPage} of ${totalPages} (${data.total_count} total items)
`; } searchResults.innerHTML = html; // Add click event listeners to sortable headers document.querySelectorAll('.sortable').forEach(header => { header.addEventListener('click', () => { const sortField = header.getAttribute('data-sort'); handleSort(sortField); }); }); } /** * Apply client-side slot filtering */ function applySlotFilter(data) { const selectedSlot = document.getElementById('slotFilter').value; if (!selectedSlot || !data.items) { return; // No filter or no data } // Filter items that can be equipped in the selected slot data.items = data.items.filter(item => { const slotName = item.slot_name || ''; // Check if the item's slot_name contains the selected slot // This handles multi-slot items like "Left Ring, Right Ring" return slotName.includes(selectedSlot); }); // Update total count data.total_count = data.items.length; } /** * Sort results client-side based on current sort settings */ function sortResults(data) { if (!data.items || data.items.length === 0) return; const field = currentSort.field; const direction = currentSort.direction; data.items.sort((a, b) => { let aVal = a[field]; let bVal = b[field]; // Handle null/undefined values if (aVal == null && bVal == null) return 0; if (aVal == null) return 1; if (bVal == null) return -1; // Special handling for spell_names array if (field === 'spell_names') { // Convert arrays to strings for sorting aVal = Array.isArray(aVal) ? aVal.join(', ').toLowerCase() : ''; bVal = Array.isArray(bVal) ? bVal.join(', ').toLowerCase() : ''; const result = aVal.localeCompare(bVal); return direction === 'asc' ? result : -result; } // Determine if we're sorting numbers or strings const isNumeric = typeof aVal === 'number' || (!isNaN(aVal) && !isNaN(parseFloat(aVal))); if (isNumeric) { // Numeric sorting aVal = parseFloat(aVal) || 0; bVal = parseFloat(bVal) || 0; const result = aVal - bVal; return direction === 'asc' ? result : -result; } else { // String sorting aVal = String(aVal).toLowerCase(); bVal = String(bVal).toLowerCase(); const result = aVal.localeCompare(bVal); return direction === 'asc' ? result : -result; } }); } /** * Handle column sorting */ function handleSort(field) { // If clicking the same field, toggle direction if (currentSort.field === field) { currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; } else { // New field, default to ascending currentSort.field = field; currentSort.direction = 'asc'; } // Reset to page 1 and perform new search with updated sort currentPage = 1; performSearch(); } /** * Show set analysis section */ function showSetAnalysis() { document.getElementById('setAnalysisSection').style.display = 'block'; document.getElementById('searchResults').style.display = 'none'; searchForm.style.display = 'none'; } /** * Show search section */ function showSearchSection() { document.getElementById('setAnalysisSection').style.display = 'none'; document.getElementById('searchResults').style.display = 'block'; searchForm.style.display = 'block'; } /** * Perform set combination analysis */ async function performSetAnalysis() { const primarySet = document.getElementById('primarySetSelect').value; const secondarySet = document.getElementById('secondarySetSelect').value; const setAnalysisResults = document.getElementById('setAnalysisResults'); if (primarySet === secondarySet) { setAnalysisResults.innerHTML = '
❌ Primary and secondary sets must be different.
'; return; } setAnalysisResults.innerHTML = '
🔍 Analyzing set combinations...
'; try { const params = new URLSearchParams(); params.append('primary_set', primarySet); params.append('secondary_set', secondarySet); params.append('primary_count', '5'); params.append('secondary_count', '4'); // Use selected characters or all characters const allCharactersChecked = document.getElementById('char_all').checked; if (allCharactersChecked) { params.append('include_all_characters', 'true'); } else { const selectedCharacters = Array.from(document.querySelectorAll('.character-checkbox:checked')) .map(cb => cb.value); if (selectedCharacters.length > 0) { params.append('characters', selectedCharacters.join(',')); } else { setAnalysisResults.innerHTML = '
❌ Please select at least one character or check "All Characters".
'; return; } } const analysisUrl = `${API_BASE}/analyze/sets?${params.toString()}`; console.log('Set Analysis URL:', analysisUrl); const response = await fetch(analysisUrl); const data = await response.json(); if (!response.ok) { throw new Error(data.detail || 'Set analysis failed'); } displaySetAnalysisResults(data); } catch (error) { console.error('Set analysis error:', error); setAnalysisResults.innerHTML = `
❌ Set analysis failed: ${error.message}
`; } } /** * Display set analysis results */ function displaySetAnalysisResults(data) { const setAnalysisResults = document.getElementById('setAnalysisResults'); if (!data.character_analysis || data.character_analysis.length === 0) { setAnalysisResults.innerHTML = '
No characters found with the selected sets.
'; return; } let html = `
${data.primary_set.name} (${data.primary_set.pieces_needed} pieces) + ${data.secondary_set.name} (${data.secondary_set.pieces_needed} pieces)
Found ${data.characters_can_build} of ${data.total_characters} characters who can build this combination
`; data.character_analysis.forEach((char) => { const canBuild = char.can_build_combination; const canBuildText = canBuild ? '✅ Yes' : '❌ No'; const canBuildClass = canBuild ? 'status-equipped' : 'status-inventory'; const primaryStatus = `${char.primary_set_available}/${char.primary_set_needed}`; const secondaryStatus = `${char.secondary_set_available}/${char.secondary_set_needed}`; // Format item lists const primaryItems = char.primary_items.map(item => `${item.name}${item.equipped ? ' ⚔️' : ''}` ).join('
') || '-'; const secondaryItems = char.secondary_items.map(item => `${item.name}${item.equipped ? ' ⚔️' : ''}` ).join('
') || '-'; html += ` `; }); html += `
Character Can Build? ${data.primary_set.name} ${data.secondary_set.name} Primary Items Secondary Items
${char.character_name} ${canBuildText} ${primaryStatus} ${secondaryStatus} ${primaryItems} ${secondaryItems}
`; setAnalysisResults.innerHTML = html; } /** * 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); } }