diff --git a/static/inventory.html b/static/inventory.html
index 2f217965..a36e7793 100644
--- a/static/inventory.html
+++ b/static/inventory.html
@@ -1138,6 +1138,10 @@
+
+ Columns:
+
+
Enter search criteria above and click "Search Items" to find inventory items.
diff --git a/static/inventory.js b/static/inventory.js
index dafdfe9e..eb27dca2 100644
--- a/static/inventory.js
+++ b/static/inventory.js
@@ -34,6 +34,7 @@ document.addEventListener('DOMContentLoaded', function() {
initializeEventListeners();
loadCharacterOptions();
+ renderColumnToggles();
});
/**
@@ -442,6 +443,85 @@ function getSelectedSlots() {
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: 'attack_bonus', label: 'Attack Bonus', sort: 'attack_bonus', defaultVisible: false, cls: 'text-right',
+ render: item => `${item.attack_bonus > 0 ? (item.attack_bonus * 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 renderColumnToggles() {
+ const container = document.getElementById('columnToggles');
+ if (!container) return;
+ container.innerHTML = '';
+ RESULT_COLUMNS.forEach(col => {
+ const label = document.createElement('label');
+ label.style.cssText = 'display:inline-flex;align-items:center;gap:2px;font-size:9px;margin-right:6px;cursor:pointer;';
+ const cb = document.createElement('input');
+ cb.type = 'checkbox';
+ cb.checked = columnVisibility[col.key] !== false;
+ cb.style.cssText = 'width:10px;height:10px;margin:0;';
+ cb.addEventListener('change', () => {
+ columnVisibility[col.key] = cb.checked;
+ saveColumnVisibility();
+ if (currentResultsData) displayResults(currentResultsData);
+ });
+ label.appendChild(cb);
+ label.appendChild(document.createTextNode(col.label));
+ container.appendChild(label);
+ });
+}
+
/**
* Display search results in the UI
*/
@@ -451,6 +531,8 @@ function displayResults(data) {
return;
}
+ const visibleCols = RESULT_COLUMNS.filter(c => columnVisibility[c.key] !== false);
+
const getSortIcon = (field) => {
if (currentSort.field === field) {
return currentSort.direction === 'asc' ? ' ▲' : ' ▼';
@@ -465,114 +547,23 @@ function displayResults(data) {
- | Character${getSortIcon('character_name')} |
- Status${getSortIcon('is_equipped')} |
- Item Name${getSortIcon('name')} |
- Type${getSortIcon('item_type_name')} |
- Slot${getSortIcon('slot_name')} |
- Coverage${getSortIcon('coverage')} |
- Armor${getSortIcon('armor')} |
- Set${getSortIcon('item_set')} |
- Spells/Cantrips${getSortIcon('spell_names')} |
- Crit Dmg${getSortIcon('crit_damage_rating')} |
- Dmg Rating${getSortIcon('damage_rating')} |
- Heal Boost${getSortIcon('heal_boost_rating')} |
- Vitality${getSortIcon('vitality_rating')} |
- Dmg Resist${getSortIcon('damage_resist_rating')} |
- Crit Dmg Resist${getSortIcon('crit_damage_resist_rating')} |
- Last Updated${getSortIcon('last_updated')} |
+ ${visibleCols.map(c => `${c.label}${getSortIcon(c.sort)} | `).join('\n ')}
`;
data.items.forEach((item) => {
- const armor = item.armor_level > 0 ? item.armor_level : '-';
- const critDmg = item.crit_damage_rating > 0 ? item.crit_damage_rating : '-';
- const dmgRating = item.damage_rating > 0 ? item.damage_rating : '-';
- const healBoostRating = item.heal_boost_rating > 0 ? item.heal_boost_rating : '-';
- const vitalityRating = item.vitality_rating > 0 ? item.vitality_rating : '-';
- const damageResistRating = item.damage_resist_rating > 0 ? item.damage_resist_rating : '-';
- const critDamageResistRating = item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-';
- const status = item.is_equipped ? '⚔️ Equipped' : '📦 Inventory';
- const statusClass = item.is_equipped ? 'status-equipped' : 'status-inventory';
-
- // Use the slot_name provided by the API instead of incorrect mapping
- // Replace commas with line breaks for better display
- const slot = item.slot_name ? item.slot_name.replace(/,\s*/g, '
') : 'Unknown';
-
- // Coverage placeholder - will need to be added to backend later
- // Replace commas with line breaks for better display
- const coverage = item.coverage ? item.coverage.replace(/,\s*/g, '
') : '-';
-
- // Format last updated timestamp
- const lastUpdated = item.last_updated ?
- new Date(item.last_updated).toLocaleString('sv-SE', {
- timeZone: 'Europe/Stockholm',
- month: 'short',
- day: 'numeric',
- hour: '2-digit',
- minute: '2-digit',
- hour12: false
- }) : '-';
-
- // Use the formatted name with material from the API
- let displayName = item.name;
-
- // The API should already include material in the name, but use material_name if available
- if (item.material_name && item.material_name !== '' && !item.name.toLowerCase().includes(item.material_name.toLowerCase())) {
- displayName = `${item.material_name} ${item.name}`;
- }
-
- // Format spells/cantrips list
- let spellsDisplay = '-';
- if (item.spell_names && item.spell_names.length > 0) {
- // Highlight legendary cantrips in a different color
- const formattedSpells = item.spell_names.map(spell => {
- if (spell.toLowerCase().includes('legendary')) {
- return `${spell}`;
- } else {
- return `${spell}`;
- }
- });
- spellsDisplay = formattedSpells.join('
');
- }
-
- // Get item type for display
- const itemType = item.item_type_name || '-';
-
- // Format equipment set name - prefer translated name
- let setDisplay = '-';
- if (item.item_set_name) {
- // Use the translated set name from backend
- setDisplay = item.item_set_name;
- // Remove redundant "Set" suffix if present for cleaner display
- setDisplay = setDisplay.replace(/\s+Set$/i, '');
- } else if (item.item_set) {
- // Fallback to raw set ID if name not available
- setDisplay = item.item_set.replace(/^Set\s+/i, '');
- setDisplay = setDisplay.replace(/\s+Set$/i, '');
- }
-
- html += `
-
- | ${item.character_name} |
- ${status} |
- ${displayName} |
- ${itemType} |
- ${slot} |
- ${coverage} |
- ${armor} |
- ${setDisplay} |
- ${spellsDisplay} |
- ${critDmg} |
- ${dmgRating} |
- ${healBoostRating} |
- ${vitalityRating} |
- ${damageResistRating} |
- ${critDamageResistRating} |
- ${lastUpdated} |
-
+ 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 += '
`;
});