feat: add tooltips throughout Procurement Intelligence tab + rename nav

- Rename nav tab and sub-nav from 'Procurement Intel' to 'Procurement Intelligence'
- Add data-tip tooltips to all 8 ABC table column headers
- Add title attributes to signal badges, ABC class badges, supply risk, stock/price/lead trend spans, signal strength bar
- Add hover descriptions to Market Intelligence type icons, buy signal badges, technology tags, impact horizon, source
- Add hover descriptions to Lifecycle Events type icons, buy signal badges, impact level, effective date
- Tooltips explain business meaning of every data point (e.g. ABC classification formula, demand score composition, supply risk levels)
This commit is contained in:
Rene Fichtmueller 2026-04-01 23:32:01 +02:00
parent af69040070
commit 3e780ce6b7

View File

@ -747,7 +747,7 @@
<div class="tab" data-tab="news">News</div> <div class="tab" data-tab="news">News</div>
<div class="tab" data-tab="finder">Finder</div> <div class="tab" data-tab="finder">Finder</div>
<div class="tab" data-tab="blog">Blog Engine</div> <div class="tab" data-tab="blog">Blog Engine</div>
<div class="tab" data-tab="procurement">Procurement Intel</div> <div class="tab" data-tab="procurement">Procurement Intelligence</div>
</div> </div>
<div class="main"> <div class="main">
@ -992,7 +992,7 @@
<div style="display:flex;gap:0.5rem;margin-bottom:1.25rem;flex-wrap:wrap;align-items:center"> <div style="display:flex;gap:0.5rem;margin-bottom:1.25rem;flex-wrap:wrap;align-items:center">
<button onclick="showProcSection('signals')" id="proc-btn-signals" class="proc-btn proc-btn-active">Reorder Signals</button> <button onclick="showProcSection('signals')" id="proc-btn-signals" class="proc-btn proc-btn-active">Reorder Signals</button>
<button onclick="showProcSection('abc')" id="proc-btn-abc" class="proc-btn">ABC Classes</button> <button onclick="showProcSection('abc')" id="proc-btn-abc" class="proc-btn">ABC Classes</button>
<button onclick="showProcSection('market')" id="proc-btn-market" class="proc-btn">Market Intel</button> <button onclick="showProcSection('market')" id="proc-btn-market" class="proc-btn">Market Intelligence</button>
<button onclick="showProcSection('lifecycle')" id="proc-btn-lifecycle" class="proc-btn">Lifecycle Events</button> <button onclick="showProcSection('lifecycle')" id="proc-btn-lifecycle" class="proc-btn">Lifecycle Events</button>
<div style="flex:1"></div> <div style="flex:1"></div>
<button onclick="loadProcurement()" style="background:var(--surface2);border:1px solid var(--border);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:0.75rem;color:var(--text)">↻ Refresh</button> <button onclick="loadProcurement()" style="background:var(--surface2);border:1px solid var(--border);padding:4px 12px;border-radius:6px;cursor:pointer;font-size:0.75rem;color:var(--text)">↻ Refresh</button>
@ -1023,14 +1023,14 @@
<div class="card" style="overflow-x:auto"> <div class="card" style="overflow-x:auto">
<table style="width:100%;border-collapse:collapse;font-size:0.8rem" id="abc-table"> <table style="width:100%;border-collapse:collapse;font-size:0.8rem" id="abc-table">
<thead><tr style="border-bottom:2px solid var(--border);color:var(--text-dim);font-size:0.7rem;font-weight:700;text-transform:uppercase"> <thead><tr style="border-bottom:2px solid var(--border);color:var(--text-dim);font-size:0.7rem;font-weight:700;text-transform:uppercase">
<th style="text-align:left;padding:8px 6px">Class</th> <th class="tip" data-tip="ABC inventory classification: A = high turnover / high value (top 20% products, ~80% of revenue). B = medium. C = low turnover / low value." style="text-align:left;padding:8px 6px">Class</th>
<th style="text-align:left;padding:8px 6px">Product</th> <th class="tip" data-tip="Transceiver product name, part number and vendor." style="text-align:left;padding:8px 6px">Product</th>
<th style="text-align:left;padding:8px 6px">Form Factor</th> <th class="tip" data-tip="Physical form factor: SFP, SFP+, QSFP28, QSFP-DD, OSFP, CFP, etc. Determines physical slot compatibility in switches." style="text-align:left;padding:8px 6px">Form Factor</th>
<th style="text-align:right;padding:8px 6px">Demand Score</th> <th class="tip" data-tip="Composite demand score (0100). Combines: price observation frequency, compatibility entry count, vendor count, hype cycle phase, and recent pricing activity." style="text-align:right;padding:8px 6px">Demand Score</th>
<th style="text-align:right;padding:8px 6px">Compat.</th> <th class="tip" data-tip="Number of compatibility entries — how many switch models support this transceiver. Higher = broader market reach and easier to sell." style="text-align:right;padding:8px 6px">Compat.</th>
<th style="text-align:right;padding:8px 6px">Vendors</th> <th class="tip" data-tip="Number of vendors offering this transceiver. More vendors = stronger competition = typically lower prices and better availability." style="text-align:right;padding:8px 6px">Vendors</th>
<th style="text-align:left;padding:8px 6px">Supply Risk</th> <th class="tip" data-tip="Supply chain risk level. High = single-source or constrained supply. Medium = some alternatives exist. Low = widely available from multiple sources." style="text-align:left;padding:8px 6px">Supply Risk</th>
<th style="text-align:left;padding:8px 6px">Signal</th> <th class="tip" data-tip="Procurement recommendation: 🔴 Buy Now = stock up, prices rising or supply tightening. 🟡 Wait = prices expected to drop. 🟢 Hold = stable, no action needed. 🔵 Monitor = watch for changes." style="text-align:left;padding:8px 6px">Signal</th>
</tr></thead> </tr></thead>
<tbody id="abc-tbody"><tr><td colspan="8" style="padding:1rem;color:var(--text-dim)">Loading...</td></tr></tbody> <tbody id="abc-tbody"><tr><td colspan="8" style="padding:1rem;color:var(--text-dim)">Loading...</td></tr></tbody>
</table> </table>
@ -3244,7 +3244,8 @@ function renderSignals(filterSig) {
try { reasons = JSON.parse(r.reasons || '[]'); } catch(e) {} try { reasons = JSON.parse(r.reasons || '[]'); } catch(e) {}
var sigClass = 'signal-' + (r.signal || 'monitor').replace('_','-'); var sigClass = 'signal-' + (r.signal || 'monitor').replace('_','-');
var badgeClass = 'sig-badge-' + (r.signal || 'monitor').replace('_now','').replace('_',''); var badgeClass = 'sig-badge-' + (r.signal || 'monitor').replace('_now','').replace('_','');
var abcBadge = r.abc_class ? '<span class="abc-' + r.abc_class.toLowerCase() + '">' + r.abc_class + '</span>' : ''; var abcTitles = { A:'Class A — high turnover product, top 20% by value. Prioritize stock availability.', B:'Class B — medium turnover. Standard replenishment cycle.', C:'Class C — low turnover. Order on demand only.' };
var abcBadge = r.abc_class ? '<span class="abc-' + r.abc_class.toLowerCase() + '" title="' + (abcTitles[r.abc_class] || '') + '">' + r.abc_class + '</span>' : '';
var strengthPct = Math.round((r.signal_strength || 0) * 100); var strengthPct = Math.round((r.signal_strength || 0) * 100);
var productName = r.standard_name || r.part_number || r.slug || '—'; var productName = r.standard_name || r.part_number || r.slug || '—';
var imgHtml = ''; var imgHtml = '';
@ -3260,19 +3261,19 @@ function renderSignals(filterSig) {
+ '</div>' + '</div>'
+ '</div>' + '</div>'
+ '<div style="display:flex;gap:0.4rem;align-items:center;margin-bottom:0.6rem;flex-wrap:wrap">' + '<div style="display:flex;gap:0.4rem;align-items:center;margin-bottom:0.6rem;flex-wrap:wrap">'
+ '<span class="intel-badge ' + badgeClass + '">' + (signalIcon[r.signal] || '') + ' ' + (signalLabel[r.signal] || r.signal) + '</span>' + '<span class="intel-badge ' + badgeClass + '" title="Procurement signal: Buy Now = act immediately (supply tightening or price rising). Wait = better prices expected. Hold = no action needed. Monitor = track closely.">' + (signalIcon[r.signal] || '') + ' ' + (signalLabel[r.signal] || r.signal) + '</span>'
+ abcBadge + abcBadge
+ (r.supply_risk ? '<span style="font-size:0.65rem;padding:2px 6px;border-radius:3px;background:var(--surface2);color:var(--text-dim)">' + esc(r.supply_risk) + ' risk</span>' : '') + (r.supply_risk ? '<span title="Supply chain risk: low = widely available, medium = some constraints, high = single-source or shortage risk" style="font-size:0.65rem;padding:2px 6px;border-radius:3px;background:var(--surface2);color:var(--text-dim)">' + esc(r.supply_risk) + ' risk</span>' : '')
+ '</div>' + '</div>'
+ '<div style="font-size:0.7rem;color:var(--text-dim);margin-bottom:0.5rem">' + '<div style="font-size:0.7rem;color:var(--text-dim);margin-bottom:0.5rem">'
+ (reasons.length ? reasons.map(function(r2) { return '→ ' + esc(r2); }).join('<br>') : 'Insufficient data') + (reasons.length ? reasons.map(function(r2) { return '→ ' + esc(r2); }).join('<br>') : 'Insufficient data')
+ '</div>' + '</div>'
+ '<div style="display:flex;gap:1rem;font-size:0.7rem;color:var(--text-dim)">' + '<div style="display:flex;gap:1rem;font-size:0.7rem;color:var(--text-dim)">'
+ (r.stock_trend ? '<span>Stock: <b style="color:var(--text)">' + r.stock_trend + '</b></span>' : '') + (r.stock_trend ? '<span title="Stock trend based on price observation frequency and vendor listing changes">Stock: <b style="color:var(--text)">' + r.stock_trend + '</b></span>' : '')
+ (r.price_trend ? '<span>Price: <b style="color:var(--text)">' + r.price_trend + '</b></span>' : '') + (r.price_trend ? '<span title="Price trend over last 30 days: rising/falling/stable">Price: <b style="color:var(--text)">' + r.price_trend + '</b></span>' : '')
+ (r.lead_time_weeks ? '<span>Lead: <b style="color:var(--text)">' + r.lead_time_weeks + 'w</b></span>' : '') + (r.lead_time_weeks ? '<span title="Estimated supplier lead time in weeks until delivery">Lead: <b style="color:var(--text)">' + r.lead_time_weeks + 'w</b></span>' : '')
+ '</div>' + '</div>'
+ '<div style="margin-top:0.6rem;background:var(--surface2);border-radius:3px;height:4px">' + '<div title="Signal strength (0100%): confidence in the procurement recommendation, based on data volume, price history consistency, and compatibility coverage." style="margin-top:0.6rem;background:var(--surface2);border-radius:3px;height:4px">'
+ '<div style="height:4px;border-radius:3px;width:' + strengthPct + '%;background:var(--accent)"></div>' + '<div style="height:4px;border-radius:3px;width:' + strengthPct + '%;background:var(--accent)"></div>'
+ '</div>' + '</div>'
+ '<div style="font-size:0.65rem;color:var(--text-dim);text-align:right;margin-top:2px">Signal strength: ' + strengthPct + '%</div>' + '<div style="font-size:0.65rem;color:var(--text-dim);text-align:right;margin-top:2px">Signal strength: ' + strengthPct + '%</div>'
@ -3326,25 +3327,35 @@ async function loadProcMarketIntel() {
capex_cycle:'💰', trade_show:'🎪', standard_ratified:'📋', capex_cycle:'💰', trade_show:'🎪', standard_ratified:'📋',
standard_draft:'📝', distributor_lead_time:'🚚', supply_chain:'🏭', tender:'📑' standard_draft:'📝', distributor_lead_time:'🚚', supply_chain:'🏭', tender:'📑'
}; };
var typeDesc = {
capex_cycle:'Capital expenditure cycle event — customer budget release, fiscal year start, major infrastructure spend',
trade_show:'Trade show or conference (OFC, ECOC, MWC, IEEE) — often signals new product launches and technology shifts',
standard_ratified:'IEEE/MSA standard officially ratified — technology is production-ready, adoption typically accelerates',
standard_draft:'Standard in draft phase — technology is emerging, early adopters phase',
distributor_lead_time:'Distributor lead time change — indicates supply chain pressure or inventory build-up',
supply_chain:'Supply chain event — factory capacity, shortage, logistics disruption',
tender:'Public or enterprise tender/RFP published — indicates near-term procurement demand'
};
container.innerHTML = items.map(function(item) { container.innerHTML = items.map(function(item) {
var sig = item.buy_signal_implication || 'none'; var sig = item.buy_signal_implication || 'none';
var badgeClass = 'intel-' + sig.replace('_now','').replace('_',''); var badgeClass = 'intel-' + sig.replace('_now','').replace('_','');
var sigLabel = { buy_now:'🔴 Buy Now', wait:'🟡 Wait', hold:'🟢 Hold', monitor:'🔵 Monitor', none:'—' }; var sigLabel = { buy_now:'🔴 Buy Now', wait:'🟡 Wait', hold:'🟢 Hold', monitor:'🔵 Monitor', none:'—' };
var sigDesc = { buy_now:'Buy Now: this market event suggests immediate procurement — prices or availability will worsen', wait:'Wait: conditions suggest holding off — better pricing or availability expected soon', hold:'Hold: market stable, no urgency to act', monitor:'Monitor: track this development, not yet actionable', none:'No specific procurement implication' };
var techs = (item.technologies || []).map(function(t) { var techs = (item.technologies || []).map(function(t) {
return '<span style="font-size:0.65rem;padding:1px 6px;border-radius:3px;background:var(--surface2);color:var(--text-dim)">' + esc(t) + '</span>'; return '<span title="Technology segment this intelligence applies to" style="font-size:0.65rem;padding:1px 6px;border-radius:3px;background:var(--surface2);color:var(--text-dim)">' + esc(t) + '</span>';
}).join(' '); }).join(' ');
return '<div class="intel-card">' return '<div class="intel-card">'
+ '<div style="display:flex;gap:0.5rem;align-items:flex-start;margin-bottom:0.4rem">' + '<div style="display:flex;gap:0.5rem;align-items:flex-start;margin-bottom:0.4rem">'
+ '<span style="font-size:1.2rem">' + (typeIcon[item.intel_type] || '📊') + '</span>' + '<span title="' + esc(typeDesc[item.intel_type] || item.intel_type || '') + '" style="font-size:1.2rem;cursor:default">' + (typeIcon[item.intel_type] || '📊') + '</span>'
+ '<div style="flex:1">' + '<div style="flex:1">'
+ '<span class="intel-badge ' + badgeClass + '">' + (sigLabel[sig] || sig) + '</span>' + '<span class="intel-badge ' + badgeClass + '" title="' + esc(sigDesc[sig] || sig) + '">' + (sigLabel[sig] || sig) + '</span>'
+ '<div style="font-weight:700;font-size:0.82rem;line-height:1.3;margin-top:0.2rem">' + esc(item.title) + '</div>' + '<div style="font-weight:700;font-size:0.82rem;line-height:1.3;margin-top:0.2rem">' + esc(item.title) + '</div>'
+ '</div></div>' + '</div></div>'
+ '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.6rem;line-height:1.5">' + esc(item.summary || '') + '</div>' + '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.6rem;line-height:1.5">' + esc(item.summary || '') + '</div>'
+ (techs ? '<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-bottom:0.5rem">' + techs + '</div>' : '') + (techs ? '<div style="display:flex;gap:0.3rem;flex-wrap:wrap;margin-bottom:0.5rem">' + techs + '</div>' : '')
+ '<div style="display:flex;justify-content:space-between;font-size:0.68rem;color:var(--text-dim)">' + '<div style="display:flex;justify-content:space-between;font-size:0.68rem;color:var(--text-dim)">'
+ '<span>' + esc(item.source_name) + '</span>' + '<span title="Intelligence source">' + esc(item.source_name) + '</span>'
+ (item.impact_horizon_months ? '<span>Impact: ~' + item.impact_horizon_months + ' months</span>' : '') + (item.impact_horizon_months ? '<span title="Estimated months until this event has measurable market impact on pricing or availability">Impact: ~' + item.impact_horizon_months + ' months</span>' : '')
+ '</div>' + '</div>'
+ '</div>'; + '</div>';
}).join(''); }).join('');
@ -3367,23 +3378,36 @@ async function loadProcLifecycle() {
standard_draft:'📝', capex_peak:'💰', trade_show:'🎪', standard_draft:'📝', capex_peak:'💰', trade_show:'🎪',
supply_risk:'⚠️', tender:'📑', price_floor:'📉' supply_risk:'⚠️', tender:'📑', price_floor:'📉'
}; };
var typeDesc = {
eol_announced:'End-of-Life announced — vendor has confirmed a product or standard will be discontinued. Start planning migration.',
eol_effective:'End-of-Life effective — product is no longer manufactured or supported. Immediately find replacements.',
standard_ratified:'Standard officially ratified by IEEE or MSA — technology is mature, safe to deploy at scale.',
standard_draft:'Standard in draft — technology is emerging. Early adopters phase, compatibility not yet guaranteed.',
capex_peak:'Capital expenditure peak — major procurement wave expected. May affect availability and pricing.',
trade_show:'Trade show event (OFC, ECOC, MWC) — often triggers new product launches and price adjustments.',
supply_risk:'Supply chain risk identified — potential shortage, capacity constraint, or geopolitical factor.',
tender:'Public or enterprise tender published — indicates confirmed near-term demand from large buyer.',
price_floor:'Price floor reached — technology has hit bottom pricing. Unlikely to drop further; good time to stock up.'
};
var impactColor = { critical:'#c1121f', high:'#c1121f', medium:'var(--yellow)', low:'var(--green)' }; var impactColor = { critical:'#c1121f', high:'#c1121f', medium:'var(--yellow)', low:'var(--green)' };
var impactDesc = { critical:'Critical impact — immediate action required', high:'High impact — plan response within weeks', medium:'Medium impact — monitor and prepare response', low:'Low impact — informational' };
var sigLabel = { buy_now:'🔴 Buy Now', wait:'🟡 Wait', hold:'🟢 Hold', monitor:'🔵 Monitor' }; var sigLabel = { buy_now:'🔴 Buy Now', wait:'🟡 Wait', hold:'🟢 Hold', monitor:'🔵 Monitor' };
var sigDesc = { buy_now:'Buy Now: this event signals immediate procurement urgency', wait:'Wait: better conditions expected after this event resolves', hold:'Hold: no change to current procurement strategy', monitor:'Monitor: track how this event develops before acting' };
container.innerHTML = items.map(function(item) { container.innerHTML = items.map(function(item) {
var ic = impactColor[item.impact_level] || 'var(--text-dim)'; var ic = impactColor[item.impact_level] || 'var(--text-dim)';
var productInfo = item.part_number ? esc(item.part_number) + (item.form_factor ? ' · ' + esc(item.form_factor) : '') : ''; var productInfo = item.part_number ? esc(item.part_number) + (item.form_factor ? ' · ' + esc(item.form_factor) : '') : '';
var dateStr = item.effective_date ? new Date(item.effective_date).toLocaleDateString('de-DE') : ''; var dateStr = item.effective_date ? new Date(item.effective_date).toLocaleDateString('de-DE') : '';
return '<div class="intel-card" style="border-left:3px solid ' + ic + '">' return '<div class="intel-card" style="border-left:3px solid ' + ic + '" title="' + esc(impactDesc[item.impact_level] || '') + '">'
+ '<div style="display:flex;gap:0.5rem;align-items:flex-start;margin-bottom:0.4rem">' + '<div style="display:flex;gap:0.5rem;align-items:flex-start;margin-bottom:0.4rem">'
+ '<span style="font-size:1.2rem">' + (typeIcon[item.event_type] || '📌') + '</span>' + '<span title="' + esc(typeDesc[item.event_type] || item.event_type || '') + '" style="font-size:1.2rem;cursor:default">' + (typeIcon[item.event_type] || '📌') + '</span>'
+ '<div style="flex:1">' + '<div style="flex:1">'
+ (item.buy_signal ? '<span class="intel-badge intel-' + item.buy_signal.replace('_now','').replace('_','') + '">' + (sigLabel[item.buy_signal] || item.buy_signal) + '</span>' : '') + (item.buy_signal ? '<span class="intel-badge intel-' + item.buy_signal.replace('_now','').replace('_','') + '" title="' + esc(sigDesc[item.buy_signal] || item.buy_signal) + '">' + (sigLabel[item.buy_signal] || item.buy_signal) + '</span>' : '')
+ '<div style="font-weight:700;font-size:0.82rem;line-height:1.3;margin-top:0.2rem">' + esc(item.title) + '</div>' + '<div style="font-weight:700;font-size:0.82rem;line-height:1.3;margin-top:0.2rem">' + esc(item.title) + '</div>'
+ '</div></div>' + '</div></div>'
+ (item.description ? '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem;line-height:1.5">' + esc(item.description.substring(0, 200)) + (item.description.length > 200 ? '…' : '') + '</div>' : '') + (item.description ? '<div style="font-size:0.75rem;color:var(--text-dim);margin-bottom:0.5rem;line-height:1.5">' + esc(item.description.substring(0, 200)) + (item.description.length > 200 ? '…' : '') + '</div>' : '')
+ '<div style="display:flex;justify-content:space-between;font-size:0.68rem;color:var(--text-dim)">' + '<div style="display:flex;justify-content:space-between;font-size:0.68rem;color:var(--text-dim)">'
+ '<span>' + esc(item.source_name || '') + (productInfo ? ' · ' + productInfo : '') + '</span>' + '<span>' + esc(item.source_name || '') + (productInfo ? ' · ' + productInfo : '') + '</span>'
+ (dateStr ? '<span style="color:' + ic + ';font-weight:600">' + dateStr + '</span>' : '') + (dateStr ? '<span title="Effective date of this lifecycle event" style="color:' + ic + ';font-weight:600">' + dateStr + '</span>' : '')
+ '</div>' + '</div>'
+ '</div>'; + '</div>';
}).join(''); }).join('');