Debug and inventory
This commit is contained in:
parent
1febf6e918
commit
80a0a16bab
15 changed files with 2764 additions and 341 deletions
722
static/inventory.js
Normal file
722
static/inventory.js
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue