/**
* 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: 'max_damage', label: 'Max Dmg', sort: 'max_damage', defaultVisible: true, cls: 'text-right',
render: item => `${item.max_damage > 0 ? item.max_damage : '-'} | ` },
{ key: 'weapon_time', label: 'Speed', sort: 'weapon_time', defaultVisible: true, cls: 'text-right',
render: item => `${item.weapon_time > 0 ? 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 => `| ${c.label}${getSortIcon(c.sort)}\u00d7 | `).join('\n ')}
`;
data.items.forEach((item) => {
html += '';
visibleCols.forEach(col => {
if (col.render) {
html += col.render(item);
} else {
const val = item[col.key];
html += `| ${val != null && val !== '' && val !== -1 ? val : '-'} | `;
}
});
html += '
';
});
html += `
`;
searchResults.innerHTML = html;
// Add click event listeners to sortable headers (click label to sort)
document.querySelectorAll('.sortable .th-label').forEach(label => {
label.style.cursor = 'pointer';
label.addEventListener('click', () => {
const sortField = label.closest('th').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
| Character |
Can Build? |
${data.primary_set.name} |
${data.secondary_set.name} |
Primary Items |
Secondary Items |
`;
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 += `
| ${char.character_name} |
${canBuildText} |
${primaryStatus} |
${secondaryStatus} |
${primaryItems} |
${secondaryItems} |
`;
});
html += `
`;
setAnalysisResults.innerHTML = html;
}
// Pagination functions removed - using client-side sorting with all results loaded