feat: compatibility panel — verification_method, competitor prices, spec-match collapsible

- API: getCompatibleTransceivers() returns verification_method, orders vendor_compat first
- Dashboard: Flexoptix section splits vendor-tested vs spec-match (collapsed)
- Dashboard: Competitor section shows vendor-tested with prices, spec-match as chips
This commit is contained in:
Rene Fichtmueller 2026-04-20 22:52:49 +02:00
parent 1ea73112c6
commit 15b5eba644
2 changed files with 81 additions and 22 deletions

View File

@ -283,7 +283,7 @@ export async function getTransceiverDocuments(transceiverI: string) {
export async function getCompatibleTransceivers(switchId: string) { export async function getCompatibleTransceivers(switchId: string) {
const result = await pool.query( const result = await pool.query(
`SELECT t.*, c.status, c.verified_by, c.notes as compat_notes, `SELECT t.*, c.status, c.verified_by, c.verification_method, c.notes as compat_notes,
v.name AS vendor_name, v.type AS vendor_type, v.website AS vendor_website, v.name AS vendor_name, v.type AS vendor_type, v.website AS vendor_website,
-- Latest verified price -- Latest verified price
COALESCE(t.price_verified_eur, COALESCE(t.price_verified_eur,
@ -298,6 +298,9 @@ export async function getCompatibleTransceivers(switchId: string) {
WHERE c.switch_id = $1 AND c.status = 'compatible' WHERE c.switch_id = $1 AND c.status = 'compatible'
ORDER BY ORDER BY
CASE WHEN LOWER(v.name) = 'flexoptix' THEN 0 ELSE 1 END, CASE WHEN LOWER(v.name) = 'flexoptix' THEN 0 ELSE 1 END,
CASE WHEN c.verification_method = 'vendor_compat' THEN 0
WHEN c.verification_method = 'vendor_matrix' THEN 1
ELSE 2 END,
t.speed_gbps DESC`, t.speed_gbps DESC`,
[switchId] [switchId]
); );

View File

@ -4070,9 +4070,19 @@ async function openSwitchDetail(id) {
ch += '<div class="panel-section" style="color:#ff6600;margin-top:1rem">Flexoptix Recommended <span style="background:#ff660018;color:#ff6600;font-size:0.7rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-left:0.4rem">' + foList.length + '</span></div>'; ch += '<div class="panel-section" style="color:#ff6600;margin-top:1rem">Flexoptix Recommended <span style="background:#ff660018;color:#ff6600;font-size:0.7rem;font-weight:700;padding:2px 8px;border-radius:10px;margin-left:0.4rem">' + foList.length + '</span></div>';
ch += '<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:0.5rem">Directly available from Flexoptix — FlexBox coding supported</div>'; ch += '<div style="font-size:0.72rem;color:var(--text-dim);margin-bottom:0.5rem">Directly available from Flexoptix — FlexBox coding supported</div>';
// Group Flexoptix by speed class // Split Flexoptix by verification method
var foVendorVerified = foList.filter(function(t) { return t.verification_method === 'vendor_compat' || t.verification_method === 'vendor_matrix'; });
var foSpecMatch = foList.filter(function(t) { return t.verification_method !== 'vendor_compat' && t.verification_method !== 'vendor_matrix'; });
// Show explicitly verified first
var foToShow = foVendorVerified.length > 0 ? foVendorVerified : foList;
if (foVendorVerified.length > 0 && foSpecMatch.length > 0) {
ch += '<div style="font-size:0.67rem;color:#16a34a;font-weight:600;margin-bottom:0.4rem">✓ Vendor-tested compatibility (' + foVendorVerified.length + ')</div>';
}
// Group by speed class
var foGroups = {}; var foGroups = {};
foList.forEach(function(t) { foToShow.forEach(function(t) {
var key = (t.speed || '?') + ' ' + (t.form_factor || '?'); var key = (t.speed || '?') + ' ' + (t.form_factor || '?');
if (!foGroups[key]) foGroups[key] = []; if (!foGroups[key]) foGroups[key] = [];
foGroups[key].push(t); foGroups[key].push(t);
@ -4107,28 +4117,74 @@ async function openSwitchDetail(id) {
if (items.length > 8) ch += '<div style="font-size:0.7rem;color:var(--text-dim);padding:0.2rem 0">+' + (items.length - 8) + ' more Flexoptix options</div>'; if (items.length > 8) ch += '<div style="font-size:0.7rem;color:var(--text-dim);padding:0.2rem 0">+' + (items.length - 8) + ' more Flexoptix options</div>';
ch += '</div>'; ch += '</div>';
}); });
// Form-factor matches (spec_match) — collapsed summary
if (foSpecMatch.length > 0) {
ch += '<details style="margin-top:0.5rem">'
+ '<summary style="font-size:0.67rem;color:var(--text-dim);cursor:pointer">+ ' + foSpecMatch.length + ' more by form factor match</summary>'
+ '<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.3rem;padding:0.3rem 0.5rem;background:var(--surface2);border-radius:4px">'
+ foSpecMatch.slice(0, 20).map(function(t) {
return '<span style="cursor:pointer;color:var(--text);margin-right:0.5rem" onclick="openTxDetail(\'' + esc(t.id) + '\')" title="' + esc((t.form_factor||'') + ' ' + (t.speed||'')) + '">'
+ esc(t.standard_name || t.part_number) + '</span>';
}).join('')
+ (foSpecMatch.length > 20 ? '<span style="color:var(--text-dim)">+' + (foSpecMatch.length - 20) + ' more</span>' : '')
+ '</div></details>';
}
} }
// ── ALL COMPATIBLE (other vendors) ──────────────────────────────────── // ── ALL COMPATIBLE (other vendors) ────────────────────────────────────
ch += '<div class="panel-section">Compatible Transceivers <span class="b b-green" style="margin-left:0.5rem">' + txList.length + '</span></div>'; if (otherList.length > 0) {
var verifiedOthers = otherList.filter(function(t) { return t.verification_method === 'vendor_matrix'; });
var specOthers = otherList.filter(function(t) { return t.verification_method !== 'vendor_matrix'; });
ch += '<div class="panel-section">Competitor Transceivers <span class="b b-green" style="margin-left:0.5rem">' + otherList.length + '</span>'
+ (verifiedOthers.length > 0 ? '<span style="font-size:0.67rem;color:#888;margin-left:0.5rem">(' + verifiedOthers.length + ' vendor-tested)</span>' : '') + '</div>';
// Show vendor-tested ones with price info
if (verifiedOthers.length > 0) {
var groups = {}; var groups = {};
otherList.forEach(function(t) { verifiedOthers.forEach(function(t) {
var key = (t.form_factor || '?') + ' ' + (t.speed || '?'); var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
if (!groups[key]) groups[key] = []; if (!groups[key]) groups[key] = [];
groups[key].push(t); groups[key].push(t);
}); });
Object.keys(groups).sort().forEach(function(key) { Object.keys(groups).sort().forEach(function(key) {
var items = groups[key]; var items = groups[key];
ch += '<div style="margin:0.6rem 0 0.3rem;font-weight:600;font-size:0.8rem;color:var(--accent)">' + esc(key) + ' <span class="dim" style="font-weight:400">(' + items.length + ')</span></div>'; ch += '<div style="margin:0.5rem 0 0.3rem;font-weight:600;font-size:0.76rem;color:var(--text-bright)">' + esc(key) + ' <span class="dim" style="font-weight:400">(' + items.length + ')</span></div>';
ch += '<div style="display:flex;flex-wrap:wrap;gap:0.3rem">'; ch += '<div style="display:flex;flex-direction:column;gap:0.25rem">';
items.slice(0, 12).forEach(function(t) { items.slice(0, 6).forEach(function(t) {
var fullyBadge = (t.fully_verified === true) ? '★ ' : ''; var priceStr = '';
ch += '<span class="b b-neutral" style="cursor:pointer;font-size:0.7rem" onclick="openTxDetail(\'' + esc(t.id) + '\')" title="' + esc(t.vendor_name || '') + (t.reach_label ? ' · ' + t.reach_label : '') + '">' if (t.latest_price) {
+ fullyBadge + esc(t.standard_name || t.slug || t.part_number) + '</span>'; var _pAmt = parseFloat(t.latest_price);
var _pCur = (t.latest_currency || 'USD').toUpperCase();
var _pUSD = toUSD(_pAmt, _pCur);
priceStr = _pUSD !== null ? fmtUSD(_pUSD) : _pCur + ' ' + _pAmt.toFixed(2);
}
ch += '<div style="display:flex;align-items:center;gap:0.4rem;padding:0.25rem 0.4rem;background:var(--surface2);border-radius:5px;cursor:pointer;font-size:0.72rem" onclick="openTxDetail(\'' + esc(t.id) + '\')">'
+ '<span style="font-weight:600;color:var(--text-bright)">' + esc(t.part_number || t.standard_name) + '</span>'
+ '<span style="color:var(--text-dim)">' + esc(t.vendor_name || '') + '</span>'
+ (priceStr ? '<span style="color:#f59e0b;margin-left:auto">' + priceStr + '</span>' : '')
+ '</div>';
}); });
if (items.length > 12) ch += '<span class="dim" style="font-size:0.7rem">+' + (items.length - 12) + ' more</span>'; if (items.length > 6) ch += '<div style="font-size:0.68rem;color:var(--text-dim)">+' + (items.length - 6) + ' more</div>';
ch += '</div>'; ch += '</div>';
}); });
}
// Show spec-match ones as compact chips
if (specOthers.length > 0) {
ch += '<div style="margin-top:0.5rem">';
ch += '<div style="font-size:0.67rem;color:var(--text-dim);margin-bottom:0.3rem">Form factor compatible</div>';
ch += '<div style="display:flex;flex-wrap:wrap;gap:0.25rem">';
specOthers.slice(0, 20).forEach(function(t) {
var fullyBadge = (t.fully_verified === true) ? '★ ' : '';
ch += '<span class="b b-neutral" style="cursor:pointer;font-size:0.68rem" onclick="openTxDetail(\'' + esc(t.id) + '\')" title="' + esc(t.vendor_name || '') + (t.reach_label ? ' · ' + t.reach_label : '') + '">'
+ fullyBadge + esc(t.standard_name || t.slug || t.part_number) + '</span>';
});
if (specOthers.length > 20) ch += '<span class="dim" style="font-size:0.68rem">+' + (specOthers.length - 20) + ' more</span>';
ch += '</div></div>';
}
}
el('panel-content').insertAdjacentHTML('beforeend', ch); el('panel-content').insertAdjacentHTML('beforeend', ch);
}).catch(function() {}); }).catch(function() {});