From 0b91f111ada380fc7faaba6b8e83eb45022d96ab Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 8 Apr 2026 18:22:13 +0200 Subject: [PATCH] feat: weapon stat columns + column visibility toggles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New columns: Max Damage, Attack Bonus, Material, Workmanship. All columns now driven by RESULT_COLUMNS config array. Column visibility toggles bar above results — checkboxes to show/hide any column, persisted to localStorage. Coverage column hidden by default. Co-Authored-By: Claude Opus 4.6 (1M context) --- static/inventory.html | 4 + static/inventory.js | 195 ++++++++++++++++++++---------------------- 2 files changed, 97 insertions(+), 102 deletions(-) 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) { - - - - - - - - - - - - - - - - + ${visibleCols.map(c => ``).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 += ` - - - - - - - - - - - - - - - - - - + html += ''; + visibleCols.forEach(col => { + if (col.render) { + html += col.render(item); + } else { + const val = item[col.key]; + html += ``; + } + }); + html += ' `; });
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')}${c.label}${getSortIcon(c.sort)}
${item.character_name}${status}${displayName}${itemType}${slot}${coverage}${armor}${setDisplay}${spellsDisplay}${critDmg}${dmgRating}${healBoostRating}${vitalityRating}${damageResistRating}${critDamageResistRating}${lastUpdated}
${val != null && val !== '' && val !== -1 ? val : '-'}