feat: weapon stat columns + column visibility toggles
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) <noreply@anthropic.com>
This commit is contained in:
parent
7f7595b5b6
commit
0b91f111ad
2 changed files with 97 additions and 102 deletions
|
|
@ -1138,6 +1138,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:#f0f0f0;padding:3px 8px;border:1px solid #ccc;border-bottom:none;font-size:9px;display:flex;align-items:center;gap:4px;flex-wrap:wrap;">
|
||||
<strong style="margin-right:4px;">Columns:</strong>
|
||||
<span id="columnToggles"></span>
|
||||
</div>
|
||||
<div class="results-container" id="searchResults">
|
||||
<div class="no-results">Enter search criteria above and click "Search Items" to find inventory items.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 `<td class="${s ? 'status-equipped' : 'status-inventory'}">${s ? '⚔️ Equipped' : '📦 Inventory'}</td>`; } },
|
||||
{ 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 `<td class="item-name">${n}</td>`; } },
|
||||
{ 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 => `<td class="text-right narrow-col">${item.slot_name ? item.slot_name.replace(/,\s*/g, '<br>') : '-'}</td>` },
|
||||
{ key: 'coverage', label: 'Coverage', sort: 'coverage', defaultVisible: false, cls: 'text-right narrow-col',
|
||||
render: item => `<td class="text-right narrow-col">${item.coverage ? item.coverage.replace(/,\s*/g, '<br>') : '-'}</td>` },
|
||||
{ key: 'armor_level', label: 'Armor', sort: 'armor', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.armor_level > 0 ? item.armor_level : '-'}</td>` },
|
||||
{ key: 'max_damage', label: 'Max Dmg', sort: 'max_damage', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.max_damage > 0 ? item.max_damage : '-'}</td>` },
|
||||
{ key: 'attack_bonus', label: 'Attack Bonus', sort: 'attack_bonus', defaultVisible: false, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.attack_bonus > 0 ? (item.attack_bonus * 100).toFixed(0) + '%' : '-'}</td>` },
|
||||
{ key: 'material_name', label: 'Material', sort: 'material_name', defaultVisible: false },
|
||||
{ key: 'workmanship', label: 'Wkm', sort: 'workmanship', defaultVisible: false, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.workmanship > 0 ? item.workmanship : '-'}</td>` },
|
||||
{ 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 `<td class="set-col" title="${s}">${s}</td>`; } },
|
||||
{ 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 '<td class="spells-cell medium-col">-</td>'; const f = item.spell_names.map(sp => sp.toLowerCase().includes('legendary') ? `<span class="legendary-cantrip">${sp}</span>` : `<span class="regular-spell">${sp}</span>`); return `<td class="spells-cell medium-col">${f.join('<br>')}</td>`; } },
|
||||
{ key: 'crit_damage_rating', label: 'Crit Dmg', sort: 'crit_damage_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.crit_damage_rating > 0 ? item.crit_damage_rating : '-'}</td>` },
|
||||
{ key: 'damage_rating', label: 'Dmg Rating', sort: 'damage_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.damage_rating > 0 ? item.damage_rating : '-'}</td>` },
|
||||
{ key: 'heal_boost_rating', label: 'Heal Boost', sort: 'heal_boost_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.heal_boost_rating > 0 ? item.heal_boost_rating : '-'}</td>` },
|
||||
{ key: 'vitality_rating', label: 'Vitality', sort: 'vitality_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.vitality_rating > 0 ? item.vitality_rating : '-'}</td>` },
|
||||
{ key: 'damage_resist_rating', label: 'Dmg Resist', sort: 'damage_resist_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.damage_resist_rating > 0 ? item.damage_resist_rating : '-'}</td>` },
|
||||
{ key: 'crit_damage_resist_rating', label: 'Crit Dmg Resist', sort: 'crit_damage_resist_rating', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${item.crit_damage_resist_rating > 0 ? item.crit_damage_resist_rating : '-'}</td>` },
|
||||
{ key: 'last_updated', label: 'Last Updated', sort: 'last_updated', defaultVisible: true, cls: 'text-right',
|
||||
render: item => `<td class="text-right">${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 }) : '-'}</td>` },
|
||||
];
|
||||
|
||||
// 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) {
|
|||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-sort="character_name">Character${getSortIcon('character_name')}</th>
|
||||
<th class="sortable" data-sort="is_equipped">Status${getSortIcon('is_equipped')}</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 narrow-col sortable" data-sort="slot_name">Slot${getSortIcon('slot_name')}</th>
|
||||
<th class="text-right narrow-col sortable" data-sort="coverage">Coverage${getSortIcon('coverage')}</th>
|
||||
<th class="text-right sortable" data-sort="armor">Armor${getSortIcon('armor')}</th>
|
||||
<th class="set-col sortable" data-sort="item_set">Set${getSortIcon('item_set')}</th>
|
||||
<th class="medium-col 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="heal_boost_rating">Heal Boost${getSortIcon('heal_boost_rating')}</th>
|
||||
<th class="text-right sortable" data-sort="vitality_rating">Vitality${getSortIcon('vitality_rating')}</th>
|
||||
<th class="text-right sortable" data-sort="damage_resist_rating">Dmg Resist${getSortIcon('damage_resist_rating')}</th>
|
||||
<th class="text-right sortable" data-sort="crit_damage_resist_rating">Crit Dmg Resist${getSortIcon('crit_damage_resist_rating')}</th>
|
||||
<th class="text-right sortable" data-sort="last_updated">Last Updated${getSortIcon('last_updated')}</th>
|
||||
${visibleCols.map(c => `<th class="${c.cls || ''} sortable" data-sort="${c.sort}">${c.label}${getSortIcon(c.sort)}</th>`).join('\n ')}
|
||||
</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 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, '<br>') : '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, '<br>') : '-';
|
||||
|
||||
// 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 `<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 || '-';
|
||||
|
||||
// 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 += `
|
||||
<tr>
|
||||
<td>${item.character_name}</td>
|
||||
<td class="${statusClass}">${status}</td>
|
||||
<td class="item-name">${displayName}</td>
|
||||
<td>${itemType}</td>
|
||||
<td class="text-right narrow-col">${slot}</td>
|
||||
<td class="text-right narrow-col">${coverage}</td>
|
||||
<td class="text-right">${armor}</td>
|
||||
<td class="set-col" title="${setDisplay}">${setDisplay}</td>
|
||||
<td class="spells-cell medium-col">${spellsDisplay}</td>
|
||||
<td class="text-right">${critDmg}</td>
|
||||
<td class="text-right">${dmgRating}</td>
|
||||
<td class="text-right">${healBoostRating}</td>
|
||||
<td class="text-right">${vitalityRating}</td>
|
||||
<td class="text-right">${damageResistRating}</td>
|
||||
<td class="text-right">${critDamageResistRating}</td>
|
||||
<td class="text-right">${lastUpdated}</td>
|
||||
</tr>
|
||||
html += '<tr>';
|
||||
visibleCols.forEach(col => {
|
||||
if (col.render) {
|
||||
html += col.render(item);
|
||||
} else {
|
||||
const val = item[col.key];
|
||||
html += `<td class="${col.cls || ''}">${val != null && val !== '' && val !== -1 ? val : '-'}</td>`;
|
||||
}
|
||||
});
|
||||
html += '</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue