/** * 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; // Items limit - load all items for client-side sorting (backend max is 50000) let currentPage = 1; let itemsPerPage = 50000; // 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(); renderHiddenColumnsBar(); }); /** * Initialize all event listeners */ function initializeEventListeners() { // Form submission searchForm.addEventListener('submit', async (e) => { e.preventDefault(); await performSearch(); }); // 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); // Show/hide weapon type dropdown when equipment type changes document.querySelectorAll('input[name="equipmentType"]').forEach(radio => { radio.addEventListener('change', (e) => { const weaponGroup = document.getElementById('weaponTypeGroup'); weaponGroup.style.display = e.target.value === 'weapon' ? '' : 'none'; if (e.target.value !== 'weapon') { document.getElementById('weaponTypeFilter').value = ''; } }); }); // 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 and weapon type document.getElementById('slotFilter').value = ''; document.getElementById('weaponTypeFilter').value = ''; document.getElementById('weaponTypeGroup').style.display = 'none'; // Clear item state checkboxes (not covered by form.reset for standalone checkboxes) ['searchBonded', 'searchAttuned', 'searchIsRare'].forEach(id => { const el = document.getElementById(id); if (el) el.checked = false; }); // Reset page currentPage = 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() { 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; // 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'); } else if (equipmentType === 'shirt') { params.append('shirt_only', 'true'); } else if (equipmentType === 'pants') { params.append('pants_only', 'true'); } else if (equipmentType === 'weapon') { params.append('weapon_only', 'true'); const weaponType = document.getElementById('weaponTypeFilter')?.value; if (weaponType) { params.append('weapon_type', weaponType); } } else if (equipmentType === 'clothing') { params.append('clothing_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'); addParam(params, 'min_crit_damage_resist_rating', 'searchMinCritDamageResistRating'); // Weapon / combat stats addParam(params, 'min_damage', 'searchMinDamage'); addParam(params, 'max_damage', 'searchMaxDamage'); addParam(params, 'min_attack_bonus', 'searchMinAttackBonus'); addParam(params, 'min_crit_resist_rating', 'searchMinCritResistRating'); addParam(params, 'min_tinks', 'searchMinTinks'); // 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'); // Spell text search addParam(params, 'spell_contains', 'searchSpellContains'); // Item state filters (only send when checked) if (document.getElementById('searchBonded')?.checked) { params.append('bonded', 'true'); } if (document.getElementById('searchAttuned')?.checked) { params.append('attuned', 'true'); } if (document.getElementById('searchIsRare')?.checked) { params.append('is_rare', 'true'); } // 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 all selected slot checkboxes from the all-slots container document.querySelectorAll('#all-slots input[type="checkbox"]:checked').forEach(cb => { selectedSlots.push(cb.value); }); return selectedSlots; } // Column definitions — single source of truth for headers, data, and visibility const RESULT_COLUMNS = [ { key: 'character_name', label: 'Character', sort: 'character_name', defaultVisible: true }, { key: 'status', label: 'Status', sort: 'is_equipped', defaultVisible: true, render: item => { const s = item.is_equipped; return `${s ? '⚔️ Equipped' : '📦 Inventory'}`; } }, { key: 'name', label: 'Item Name', sort: 'name', defaultVisible: true, cls: 'item-name', render: item => { let n = item.name; if (item.material_name && item.material_name !== '' && !n.toLowerCase().includes(item.material_name.toLowerCase())) n = `${item.material_name} ${n}`; return `${n}`; } }, { key: 'item_type_name', label: 'Type', sort: 'item_type_name', defaultVisible: true }, { key: 'slot_name', label: 'Slot', sort: 'slot_name', defaultVisible: true, cls: 'text-right narrow-col', render: item => `${item.slot_name ? item.slot_name.replace(/,\s*/g, '
') : '-'}` }, { key: 'coverage', label: 'Coverage', sort: 'coverage', defaultVisible: false, cls: 'text-right narrow-col', render: item => `${item.coverage ? item.coverage.replace(/,\s*/g, '
') : '-'}` }, { key: 'armor_level', label: 'Armor', sort: 'armor', defaultVisible: true, cls: 'text-right', render: item => `${item.armor_level > 0 ? item.armor_level : '-'}` }, { key: 'base_armor_level', label: 'Base Armor', sort: 'base_armor_level', defaultVisible: false, cls: 'text-right', render: item => `${item.base_armor_level > 0 ? item.base_armor_level : '-'}` }, { key: 'max_damage', label: 'Max Dmg', sort: 'max_damage', defaultVisible: true, cls: 'text-right', render: item => `${item.max_damage > 0 ? item.max_damage : '-'}` }, { key: 'base_max_damage', label: 'Base Dmg', sort: 'base_max_damage', defaultVisible: false, cls: 'text-right', render: item => `${item.base_max_damage > 0 ? item.base_max_damage : '-'}` }, { key: 'weapon_time', label: 'Speed', sort: 'weapon_time', defaultVisible: true, cls: 'text-right', render: item => `${item.weapon_time > 0 && item.weapon_time < 100 ? item.weapon_time : '-'}` }, { key: 'attack_bonus', label: 'Attack Bonus', sort: 'attack_bonus', defaultVisible: true, cls: 'text-right', render: item => `${item.attack_bonus > 0 ? '+' + ((item.attack_bonus - 1) * 100).toFixed(0) + '%' : '-'}` }, { key: 'melee_defense_bonus', label: 'Melee Def', sort: 'melee_defense_bonus', defaultVisible: true, cls: 'text-right', render: item => `${item.melee_defense_bonus > 0 ? '+' + ((item.melee_defense_bonus - 1) * 100).toFixed(0) + '%' : '-'}` }, { key: 'material_name', label: 'Material', sort: 'material_name', defaultVisible: false }, { key: 'workmanship', label: 'Wkm', sort: 'workmanship', defaultVisible: false, cls: 'text-right', render: item => `${item.workmanship > 0 ? item.workmanship : '-'}` }, { key: 'item_set', label: 'Set', sort: 'item_set', defaultVisible: true, cls: 'set-col', render: item => { let s = '-'; if (item.item_set_name) { s = item.item_set_name.replace(/\s+Set$/i, ''); } else if (item.item_set) { s = item.item_set.replace(/^Set\s+/i, '').replace(/\s+Set$/i, ''); } return `${s}`; } }, { key: 'spell_names', label: 'Spells/Cantrips', sort: 'spell_names', defaultVisible: true, cls: 'spells-cell medium-col', render: item => { if (!item.spell_names || item.spell_names.length === 0) return '-'; const f = item.spell_names.map(sp => sp.toLowerCase().includes('legendary') ? `${sp}` : `${sp}`); return `${f.join('
')}`; } }, { key: 'crit_damage_rating', label: 'Crit Dmg', sort: 'crit_damage_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.crit_damage_rating > 0 ? item.crit_damage_rating : '-'}` }, { key: 'damage_rating', label: 'Dmg Rating', sort: 'damage_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.damage_rating > 0 ? item.damage_rating : '-'}` }, { key: 'heal_boost_rating', label: 'Heal Boost', sort: 'heal_boost_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.heal_boost_rating > 0 ? item.heal_boost_rating : '-'}` }, { key: 'vitality_rating', label: 'Vitality', sort: 'vitality_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.vitality_rating > 0 ? item.vitality_rating : '-'}` }, { key: 'damage_resist_rating', label: 'Dmg Resist', sort: 'damage_resist_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.damage_resist_rating > 0 ? item.damage_resist_rating : '-'}` }, { key: 'crit_damage_resist_rating', label: 'Crit Dmg Resist', sort: 'crit_damage_resist_rating', defaultVisible: true, cls: 'text-right', render: item => `${item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-'}` }, { key: 'last_updated', label: 'Last Updated', sort: 'last_updated', defaultVisible: true, cls: 'text-right', render: item => `${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 }) : '-'}` }, ]; // Load column visibility from localStorage or use defaults function getColumnVisibility() { try { const saved = localStorage.getItem('inventoryColumnVisibility'); if (saved) return JSON.parse(saved); } catch (e) {} const vis = {}; RESULT_COLUMNS.forEach(c => vis[c.key] = c.defaultVisible); return vis; } let columnVisibility = getColumnVisibility(); function saveColumnVisibility() { localStorage.setItem('inventoryColumnVisibility', JSON.stringify(columnVisibility)); } function renderHiddenColumnsBar() { const bar = document.getElementById('hiddenColumnsBar'); const list = document.getElementById('hiddenColumnsList'); if (!bar || !list) return; const hidden = RESULT_COLUMNS.filter(c => columnVisibility[c.key] === false); if (hidden.length === 0) { bar.style.display = 'none'; return; } bar.style.display = 'flex'; list.innerHTML = ''; hidden.forEach(col => { const btn = document.createElement('button'); btn.textContent = `+ ${col.label}`; btn.style.cssText = 'font-size:9px;padding:1px 5px;border:1px solid #999;background:#fff;cursor:pointer;border-radius:3px;margin-right:3px;'; btn.addEventListener('click', () => { columnVisibility[col.key] = true; saveColumnVisibility(); if (currentResultsData) displayResults(currentResultsData); renderHiddenColumnsBar(); }); list.appendChild(btn); }); } function hideColumn(key) { columnVisibility[key] = false; saveColumnVisibility(); if (currentResultsData) displayResults(currentResultsData); renderHiddenColumnsBar(); } /** * 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 visibleCols = RESULT_COLUMNS.filter(c => columnVisibility[c.key] !== false); const getSortIcon = (field) => { if (currentSort.field === field) { return currentSort.direction === 'asc' ? ' ▲' : ' ▼'; } return ''; }; let html = `
Found ${data.total_count} items - Showing all results
${visibleCols.map(c => ``).join('\n ')} `; data.items.forEach((item) => { html += ''; visibleCols.forEach(col => { if (col.render) { html += col.render(item); } else { const val = item[col.key]; html += ``; } }); html += ''; }); html += `
${c.label}${getSortIcon(c.sort)}\u00d7
${val != null && val !== '' && val !== -1 ? val : '-'}
`; searchResults.innerHTML = html; // Add click event listeners to sortable headers (full th area sorts) document.querySelectorAll('.sortable').forEach(header => { header.style.cursor = 'pointer'; header.addEventListener('click', () => { const sortField = header.getAttribute('data-sort'); handleSort(sortField); }); }); // Add click event listeners to hide buttons document.querySelectorAll('.th-hide').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); hideColumn(btn.getAttribute('data-col')); }); }); // Update hidden columns bar renderHiddenColumnsBar(); // 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 */ 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 - client-side sorting for all columns */ 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'; } // If we have cached results, sort them client-side and re-display if (currentResultsData && currentResultsData.items) { // 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(); } } /** * 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; } // Pagination functions removed - using client-side sorting with all results loaded