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
1ea73112c6
commit
15b5eba644
@ -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]
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 groups = {};
|
var verifiedOthers = otherList.filter(function(t) { return t.verification_method === 'vendor_matrix'; });
|
||||||
otherList.forEach(function(t) {
|
var specOthers = otherList.filter(function(t) { return t.verification_method !== 'vendor_matrix'; });
|
||||||
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
|
||||||
if (!groups[key]) groups[key] = [];
|
ch += '<div class="panel-section">Competitor Transceivers <span class="b b-green" style="margin-left:0.5rem">' + otherList.length + '</span>'
|
||||||
groups[key].push(t);
|
+ (verifiedOthers.length > 0 ? '<span style="font-size:0.67rem;color:#888;margin-left:0.5rem">(' + verifiedOthers.length + ' vendor-tested)</span>' : '') + '</div>';
|
||||||
});
|
|
||||||
Object.keys(groups).sort().forEach(function(key) {
|
// Show vendor-tested ones with price info
|
||||||
var items = groups[key];
|
if (verifiedOthers.length > 0) {
|
||||||
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>';
|
var groups = {};
|
||||||
ch += '<div style="display:flex;flex-wrap:wrap;gap:0.3rem">';
|
verifiedOthers.forEach(function(t) {
|
||||||
items.slice(0, 12).forEach(function(t) {
|
var key = (t.form_factor || '?') + ' ' + (t.speed || '?');
|
||||||
var fullyBadge = (t.fully_verified === true) ? '★ ' : '';
|
if (!groups[key]) groups[key] = [];
|
||||||
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 : '') + '">'
|
groups[key].push(t);
|
||||||
+ fullyBadge + esc(t.standard_name || t.slug || t.part_number) + '</span>';
|
});
|
||||||
});
|
Object.keys(groups).sort().forEach(function(key) {
|
||||||
if (items.length > 12) ch += '<span class="dim" style="font-size:0.7rem">+' + (items.length - 12) + ' more</span>';
|
var items = groups[key];
|
||||||
ch += '</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-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);
|
el('panel-content').insertAdjacentHTML('beforeend', ch);
|
||||||
}).catch(function() {});
|
}).catch(function() {});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user