MosswartOverlord/static/inventory.js
2025-06-19 17:46:19 +00:00

722 lines
25 KiB
JavaScript

/**
* 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;
// 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();
});
// 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 results and clear stored data
currentResultsData = null;
searchResults.innerHTML = '<div class="no-results">Enter search criteria above and click "Search Items" to find inventory items.</div>';
}
/**
* 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() {
searchResults.innerHTML = '<div class="loading">🔍 Searching inventory...</div>';
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);
// Sort the results client-side before displaying
sortResults(data);
displayResults(data);
} catch (error) {
console.error('Search error:', error);
searchResults.innerHTML = `<div class="error">❌ Search failed: ${error.message}</div>`;
}
}
/**
* 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');
// 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(','));
}
// Pagination only - sorting will be done client-side
params.append('limit', '1000'); // Show all items on one page
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;
}
/**
* Display search results in the UI
*/
function displayResults(data) {
if (!data.items || data.items.length === 0) {
searchResults.innerHTML = '<div class="no-results">No items found matching your search criteria.</div>';
return;
}
const getSortIcon = (field) => {
if (currentSort.field === field) {
return currentSort.direction === 'asc' ? ' ▲' : ' ▼';
}
return '';
};
let html = `
<div class="results-info">
Found <strong>${data.total_count}</strong> items - Showing all results
</div>
<table class="results-table">
<thead>
<tr>
<th class="sortable" data-sort="character_name">Character${getSortIcon('character_name')}</th>
<th class="sortable" data-sort="name">Item Name${getSortIcon('name')}</th>
<th class="sortable" data-sort="item_type_name">Type${getSortIcon('item_type_name')}</th>
<th class="text-right sortable" data-sort="slot_name">Slot${getSortIcon('slot_name')}</th>
<th class="text-right sortable" data-sort="coverage">Coverage${getSortIcon('coverage')}</th>
<th class="text-right sortable" data-sort="armor_level">Armor${getSortIcon('armor_level')}</th>
<th class="sortable" data-sort="spell_names">Spells/Cantrips${getSortIcon('spell_names')}</th>
<th class="text-right sortable" data-sort="crit_damage_rating">Crit Dmg${getSortIcon('crit_damage_rating')}</th>
<th class="text-right sortable" data-sort="damage_rating">Dmg Rating${getSortIcon('damage_rating')}</th>
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
</tr>
</thead>
<tbody>
`;
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 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
const slot = item.slot_name || 'Unknown';
// Coverage placeholder - will need to be added to backend later
const coverage = item.coverage || '-';
// Format last updated timestamp
const lastUpdated = item.last_updated ?
new Date(item.last_updated).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
}) : '-';
// 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 `<span class="legendary-cantrip">${spell}</span>`;
} else {
return `<span class="regular-spell">${spell}</span>`;
}
});
spellsDisplay = formattedSpells.join('<br>');
}
// Get item type for display
const itemType = item.item_type_name || '-';
html += `
<tr>
<td>${item.character_name}</td>
<td class="item-name">${displayName}</td>
<td>${itemType}</td>
<td class="text-right">${slot}</td>
<td class="text-right">${coverage}</td>
<td class="text-right">${armor}</td>
<td class="spells-cell">${spellsDisplay}</td>
<td class="text-right">${critDmg}</td>
<td class="text-right">${dmgRating}</td>
<td class="text-right">${lastUpdated}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
// Add pagination info if needed
if (data.total_pages > 1) {
html += `
<div style="padding: 16px 24px; text-align: center; color: #5f6368; border-top: 1px solid #e8eaed;">
Showing page ${data.page} of ${data.total_pages} pages
</div>
`;
}
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';
}
// Re-display current results with new sorting (no new search needed)
if (currentResultsData) {
// Reset items to original unfiltered data
const originalData = JSON.parse(JSON.stringify(currentResultsData));
// Apply slot filtering first
applySlotFilter(originalData);
// Then apply sorting
sortResults(originalData);
// Display results
displayResults(originalData);
}
}
/**
* 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 = '<div class="error">❌ Primary and secondary sets must be different.</div>';
return;
}
setAnalysisResults.innerHTML = '<div class="loading">🔍 Analyzing set combinations...</div>';
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 = '<div class="error">❌ Please select at least one character or check "All Characters".</div>';
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 = `<div class="error">❌ Set analysis failed: ${error.message}</div>`;
}
}
/**
* Display set analysis results
*/
function displaySetAnalysisResults(data) {
const setAnalysisResults = document.getElementById('setAnalysisResults');
if (!data.character_analysis || data.character_analysis.length === 0) {
setAnalysisResults.innerHTML = '<div class="no-results">No characters found with the selected sets.</div>';
return;
}
let html = `
<div class="results-info">
<strong>${data.primary_set.name}</strong> (${data.primary_set.pieces_needed} pieces) +
<strong>${data.secondary_set.name}</strong> (${data.secondary_set.pieces_needed} pieces)<br>
Found <strong>${data.characters_can_build}</strong> of <strong>${data.total_characters}</strong> characters who can build this combination
</div>
<table class="results-table">
<thead>
<tr>
<th>Character</th>
<th>Can Build?</th>
<th>${data.primary_set.name}</th>
<th>${data.secondary_set.name}</th>
<th>Primary Items</th>
<th>Secondary Items</th>
</tr>
</thead>
<tbody>
`;
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('<br>') || '-';
const secondaryItems = char.secondary_items.map(item =>
`${item.name}${item.equipped ? ' ⚔️' : ''}`
).join('<br>') || '-';
html += `
<tr>
<td><strong>${char.character_name}</strong></td>
<td class="${canBuildClass}">${canBuildText}</td>
<td class="text-right">${primaryStatus}</td>
<td class="text-right">${secondaryStatus}</td>
<td class="spells-cell">${primaryItems}</td>
<td class="spells-cell">${secondaryItems}</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
setAnalysisResults.innerHTML = html;
}