feat: side-by-side competitor comparison + fix 1.6T speed_gbps
- Fix OSFP-DR8-1.6T-FL and OSFP-2FR4-1.6T-FL: speed_gbps was 200, now 1600 → FS.com 1.6T products now correctly match as comparables for Flexoptix O.1316T.C.05.M - API: extend comparable price query to return comp_form_factor, comp_speed_gbps, comp_reach_meters, comp_reach_label, comp_fiber_type, comp_wavelengths - Dashboard: replace plain comparable price row with side-by-side spec comparison card showing Flexoptix vs. competitor: Form Factor, Speed, Reach, Fiber, Wavelengths with color coding (green=match, orange=mismatch) and savings badge (−45% günstiger)
This commit is contained in:
parent
2ebba07bb0
commit
7718356327
@ -77,7 +77,10 @@ transceiverRouter.get("/:id", async (req: Request, res: Response) => {
|
||||
`SELECT DISTINCT ON (po.source_vendor_id)
|
||||
po.price, po.currency, po.url, po.time,
|
||||
sv.name AS vendor_name, sv.type AS vendor_type,
|
||||
t2.part_number, t2.standard_name, t2.id AS comparable_id
|
||||
t2.part_number, t2.standard_name, t2.id AS comparable_id,
|
||||
t2.form_factor AS comp_form_factor, t2.speed_gbps AS comp_speed_gbps,
|
||||
t2.reach_meters AS comp_reach_meters, t2.reach_label AS comp_reach_label,
|
||||
t2.fiber_type AS comp_fiber_type, t2.wavelengths AS comp_wavelengths
|
||||
FROM transceivers t1
|
||||
JOIN transceivers t2 ON (
|
||||
t2.form_factor = t1.form_factor
|
||||
@ -117,6 +120,13 @@ transceiverRouter.get("/:id", async (req: Request, res: Response) => {
|
||||
is_same_product: false, // different SKU, same spec class
|
||||
comparable_part: row.part_number || row.standard_name,
|
||||
comparable_id: row.comparable_id,
|
||||
// Spec details for side-by-side comparison in dashboard
|
||||
comp_form_factor: row.comp_form_factor,
|
||||
comp_speed_gbps: row.comp_speed_gbps ? parseFloat(row.comp_speed_gbps) : null,
|
||||
comp_reach_meters: row.comp_reach_meters,
|
||||
comp_reach_label: row.comp_reach_label,
|
||||
comp_fiber_type: row.comp_fiber_type,
|
||||
comp_wavelengths: row.comp_wavelengths,
|
||||
}));
|
||||
|
||||
const allPrices = [...prices, ...comparablePrices];
|
||||
|
||||
@ -3499,12 +3499,91 @@ async function openTxDetail(id) {
|
||||
|
||||
directPrices.forEach(function(p) { h += renderPriceRow(p); });
|
||||
|
||||
if (comparPrices.length > 0) {
|
||||
h += '<div style="font-size:0.7rem;color:#888;margin:0.5rem 0 0.25rem;padding-top:0.4rem;border-top:1px solid var(--border)">Vergleichbare Produkte anderer Hersteller (gleiche Spezifikation)</div>';
|
||||
comparPrices.forEach(function(p) { h += renderPriceRow(p); });
|
||||
}
|
||||
|
||||
h += '</div>';
|
||||
|
||||
// Comparable products → Side-by-Side spec comparison cards
|
||||
if (comparPrices.length > 0) {
|
||||
h += '<div class="panel-section" style="margin-top:0.8rem">Vergleichbare Wettbewerber-Produkte</div>';
|
||||
h += '<div style="font-size:0.72rem;color:#888;margin-bottom:0.5rem">Gleiche Spezifikationsklasse — andere Part Number</div>';
|
||||
comparPrices.forEach(function(p) {
|
||||
// Calculate price delta (EUR-normalized)
|
||||
var myEur = null;
|
||||
var refPrice = directPrices.length > 0 ? directPrices[0] : null;
|
||||
if (refPrice) {
|
||||
var ra = parseFloat(refPrice.price), rc = (refPrice.currency||'USD').toUpperCase();
|
||||
myEur = rc === 'EUR' ? ra : rc === 'USD' ? ra * 0.92 : ra;
|
||||
}
|
||||
var compEur = null;
|
||||
var ca = parseFloat(p.price), cc = (p.currency||'USD').toUpperCase();
|
||||
compEur = cc === 'EUR' ? ca : cc === 'USD' ? ca * 0.92 : ca;
|
||||
|
||||
var savBadge = '';
|
||||
if (myEur && compEur && myEur > 0 && compEur > 0) {
|
||||
var diff = myEur - compEur;
|
||||
var pct = Math.round(Math.abs(diff) / myEur * 100);
|
||||
if (diff > 0) {
|
||||
savBadge = '<span style="background:rgba(22,163,74,0.15);color:#16a34a;font-size:0.72rem;font-weight:700;padding:2px 7px;border-radius:4px;border:1px solid rgba(22,163,74,0.35)">'
|
||||
+ '−' + pct + '% günstiger</span>';
|
||||
} else if (diff < 0) {
|
||||
savBadge = '<span style="background:rgba(220,38,38,0.1);color:#dc2626;font-size:0.72rem;font-weight:700;padding:2px 7px;border-radius:4px;border:1px solid rgba(220,38,38,0.25)">'
|
||||
+ '+' + pct + '% teurer</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// Spec comparison helper — highlight match/mismatch
|
||||
function specRow(label, myVal, compVal) {
|
||||
var match = myVal && compVal && String(myVal).toLowerCase() === String(compVal).toLowerCase();
|
||||
var compColor = !myVal || !compVal ? '#aaa' : match ? '#4ade80' : '#fb923c';
|
||||
return '<tr><td style="color:#888;font-size:0.7rem;padding:2px 6px 2px 0;white-space:nowrap">' + label + '</td>'
|
||||
+ '<td style="color:#ccc;font-size:0.7rem;padding:2px 8px 2px 0">' + esc(myVal || '—') + '</td>'
|
||||
+ '<td style="color:' + compColor + ';font-size:0.7rem;padding:2px 0">' + esc(compVal || '—') + '</td></tr>';
|
||||
}
|
||||
|
||||
var mySpeed = t.speed_gbps >= 1000 ? (t.speed_gbps / 1000).toFixed(1).replace('.0','') + 'T' : t.speed_gbps + 'G';
|
||||
var compSpeed = p.comp_speed_gbps ? (p.comp_speed_gbps >= 1000 ? (p.comp_speed_gbps/1000).toFixed(1).replace('.0','')+'T' : p.comp_speed_gbps+'G') : null;
|
||||
|
||||
h += '<div style="border:1px solid var(--border);border-radius:8px;margin-bottom:0.6rem;overflow:hidden">';
|
||||
|
||||
// Header: vendor + part + price + savings badge
|
||||
h += '<div style="display:flex;align-items:center;justify-content:space-between;padding:0.55rem 0.75rem;background:rgba(255,255,255,0.03);border-bottom:1px solid var(--border)">';
|
||||
h += '<div>';
|
||||
h += '<span style="font-size:0.78rem;font-weight:700;color:var(--accent)">' + esc(p.vendor_name) + '</span>';
|
||||
h += '<span style="font-size:0.7rem;color:#888;margin-left:0.5rem">' + esc(p.comparable_part || '—') + '</span>';
|
||||
h += '</div>';
|
||||
h += '<div style="display:flex;align-items:center;gap:0.4rem">';
|
||||
var priceDisplayEur = compEur ? ('EUR\u00a0' + compEur.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})) : '';
|
||||
h += '<span style="font-size:0.82rem;font-weight:700;color:var(--text)">' + priceDisplayEur + '</span>';
|
||||
if (p.url) h += '<a href="' + esc(p.url) + '" target="_blank" rel="noopener" style="color:var(--accent);font-size:0.7rem;text-decoration:none">↗</a>';
|
||||
h += '</div>';
|
||||
h += '</div>';
|
||||
|
||||
// Savings badge row
|
||||
if (savBadge) {
|
||||
h += '<div style="padding:0.3rem 0.75rem;background:rgba(255,255,255,0.02);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:0.5rem">';
|
||||
h += savBadge;
|
||||
h += '<span style="font-size:0.68rem;color:#666">vs. Flexoptix Listenpreis</span>';
|
||||
h += '</div>';
|
||||
}
|
||||
|
||||
// Spec comparison table: Flexoptix (links) vs. Wettbewerber (rechts)
|
||||
h += '<div style="padding:0.5rem 0.75rem">';
|
||||
h += '<table style="width:100%;border-collapse:collapse">';
|
||||
h += '<thead><tr>';
|
||||
h += '<th style="font-size:0.67rem;color:#555;text-align:left;padding-bottom:4px;padding-right:8px"></th>';
|
||||
h += '<th style="font-size:0.67rem;color:#888;text-align:left;padding-bottom:4px;padding-right:8px">Flexoptix</th>';
|
||||
h += '<th style="font-size:0.67rem;color:#888;text-align:left;padding-bottom:4px">' + esc(p.vendor_name) + '</th>';
|
||||
h += '</tr></thead><tbody>';
|
||||
h += specRow('Form Factor', t.form_factor, p.comp_form_factor);
|
||||
h += specRow('Speed', mySpeed, compSpeed);
|
||||
h += specRow('Reach', t.reach_label || (t.reach_meters ? t.reach_meters + 'm' : null), p.comp_reach_label || (p.comp_reach_meters ? p.comp_reach_meters + 'm' : null));
|
||||
h += specRow('Fiber', t.fiber_type, p.comp_fiber_type);
|
||||
if (t.wavelengths || p.comp_wavelengths) h += specRow('Wavelengths', t.wavelengths, p.comp_wavelengths);
|
||||
h += '</tbody></table>';
|
||||
h += '<div style="font-size:0.65rem;color:#555;margin-top:0.35rem">🕐 Stand: ' + fmtDate(p.observed_at) + (p.is_verified ? ' · <span style="color:#2d6a4f">✓ Verified</span>' : '') + '</div>';
|
||||
h += '</div>';
|
||||
h += '</div>'; // card end
|
||||
});
|
||||
}
|
||||
}
|
||||
// No competitor prices → show "Kein Markt" info block with last scan date
|
||||
if (!cVer && t.last_competitor_scan) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user