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:
parent
4bf5c95824
commit
c0cd0dc1ca
@ -283,7 +283,7 @@ export async function getTransceiverDocuments(transceiverI: string) {
|
||||
|
||||
export async function getCompatibleTransceivers(switchId: string) {
|
||||
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,
|
||||
-- Latest verified price
|
||||
COALESCE(t.price_verified_eur,
|
||||
@ -298,6 +298,9 @@ export async function getCompatibleTransceivers(switchId: string) {
|
||||
WHERE c.switch_id = $1 AND c.status = 'compatible'
|
||||
ORDER BY
|
||||
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`,
|
||||
[switchId]
|
||||
);
|
||||
|
||||
@ -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 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 = {};
|
||||
foList.forEach(function(t) {
|
||||
foToShow.forEach(function(t) {
|
||||
var key = (t.speed || '?') + ' ' + (t.form_factor || '?');
|
||||
if (!foGroups[key]) foGroups[key] = [];
|
||||
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>';
|
||||
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) ────────────────────────────────────
|
||||
ch += '<div class="panel-section">Compatible Transceivers <span class="b b-green" style="margin-left:0.5rem">' + txList.length + '</span></div>';
|
||||
var groups = {};
|
||||
otherList.forEach(function(t) {
|
||||
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(t);
|
||||
});
|
||||
Object.keys(groups).sort().forEach(function(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="display:flex;flex-wrap:wrap;gap:0.3rem">';
|
||||
items.slice(0, 12).forEach(function(t) {
|
||||
var fullyBadge = (t.fully_verified === true) ? '★ ' : '';
|
||||
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 : '') + '">'
|
||||
+ fullyBadge + esc(t.standard_name || t.slug || t.part_number) + '</span>';
|
||||
});
|
||||
if (items.length > 12) ch += '<span class="dim" style="font-size:0.7rem">+' + (items.length - 12) + ' more</span>';
|
||||
ch += '</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 = {};
|
||||
verifiedOthers.forEach(function(t) {
|
||||
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(t);
|
||||
});
|
||||
Object.keys(groups).sort().forEach(function(key) {
|
||||
var items = groups[key];
|
||||
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-direction:column;gap:0.25rem">';
|
||||
items.slice(0, 6).forEach(function(t) {
|
||||
var priceStr = '';
|
||||
if (t.latest_price) {
|
||||
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 > 6) ch += '<div style="font-size:0.68rem;color:var(--text-dim)">+' + (items.length - 6) + ' more</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);
|
||||
}).catch(function() {});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user