187 lines
6.5 KiB
TypeScript
187 lines
6.5 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { pool } from "../db/client";
|
|
|
|
export const equivalencesRouter = Router();
|
|
|
|
// GET /api/equivalences?q=<part_number>&vendor=<vendor>&limit=50&offset=0
|
|
// Search equivalences by competitor or Flexoptix part number
|
|
equivalencesRouter.get("/", async (req: Request, res: Response) => {
|
|
const { q, vendor, limit: lim, offset: off } = req.query as Record<string, string>;
|
|
const limit = Math.min(parseInt(lim || "50"), 200);
|
|
const offset = parseInt(off || "0");
|
|
|
|
const conditions: string[] = ["e.status IN ('approved', 'auto_approved')"];
|
|
const values: unknown[] = [];
|
|
let idx = 1;
|
|
|
|
if (q) {
|
|
conditions.push(
|
|
`(fx.part_number ILIKE $${idx} OR fx.standard_name ILIKE $${idx} OR cx.part_number ILIKE $${idx} OR cx.standard_name ILIKE $${idx})`
|
|
);
|
|
values.push(`%${q}%`);
|
|
idx++;
|
|
}
|
|
if (vendor) {
|
|
conditions.push(`(cv.name ILIKE $${idx} OR fv.name ILIKE $${idx})`);
|
|
values.push(`%${vendor}%`);
|
|
idx++;
|
|
}
|
|
|
|
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
|
|
const query = `
|
|
SELECT
|
|
e.id,
|
|
e.confidence,
|
|
e.match_basis,
|
|
e.status,
|
|
e.created_at,
|
|
-- Flexoptix side
|
|
fx.id AS flexoptix_id,
|
|
fx.part_number AS flexoptix_pn,
|
|
fx.standard_name AS flexoptix_std,
|
|
fx.form_factor AS flexoptix_form_factor,
|
|
fx.speed AS flexoptix_speed,
|
|
fx.speed_gbps AS flexoptix_speed_gbps,
|
|
fx.reach_label AS flexoptix_reach,
|
|
fx.product_page_url AS flexoptix_url,
|
|
fx.price_verified_eur AS flexoptix_price_eur,
|
|
fx.market_status AS flexoptix_market_status,
|
|
-- Competitor side
|
|
cx.id AS competitor_id,
|
|
cx.part_number AS competitor_pn,
|
|
cx.standard_name AS competitor_std,
|
|
cx.form_factor AS competitor_form_factor,
|
|
cx.speed AS competitor_speed,
|
|
cx.reach_label AS competitor_reach,
|
|
cx.product_page_url AS competitor_url,
|
|
cx.price_verified_eur AS competitor_price_eur,
|
|
cx.market_status AS competitor_market_status,
|
|
cv.name AS competitor_vendor,
|
|
cv.website AS competitor_vendor_website
|
|
FROM transceiver_equivalences e
|
|
JOIN transceivers fx ON fx.id = e.flexoptix_id
|
|
JOIN vendors fv ON fv.id = fx.vendor_id
|
|
JOIN transceivers cx ON cx.id = e.competitor_id
|
|
JOIN vendors cv ON cv.id = cx.vendor_id
|
|
${where}
|
|
ORDER BY e.confidence DESC, e.status DESC
|
|
LIMIT ${limit} OFFSET ${offset}
|
|
`;
|
|
|
|
const countQuery = `
|
|
SELECT COUNT(*) FROM transceiver_equivalences e
|
|
JOIN transceivers fx ON fx.id = e.flexoptix_id
|
|
JOIN vendors fv ON fv.id = fx.vendor_id
|
|
JOIN transceivers cx ON cx.id = e.competitor_id
|
|
JOIN vendors cv ON cv.id = cx.vendor_id
|
|
${where}
|
|
`;
|
|
|
|
try {
|
|
const [data, count] = await Promise.all([
|
|
pool.query(query, values),
|
|
pool.query(countQuery, values),
|
|
]);
|
|
res.json({
|
|
success: true,
|
|
data: data.rows,
|
|
total: parseInt(count.rows[0].count),
|
|
limit,
|
|
offset,
|
|
});
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: (err as Error).message });
|
|
}
|
|
});
|
|
|
|
// GET /api/equivalences/transceiver/:id — all equivalences for a specific transceiver (both sides)
|
|
equivalencesRouter.get("/transceiver/:id", async (req: Request, res: Response) => {
|
|
const { id } = req.params;
|
|
|
|
try {
|
|
const result = await pool.query(
|
|
`SELECT
|
|
e.id,
|
|
e.confidence,
|
|
e.match_basis,
|
|
e.status,
|
|
-- Flexoptix side
|
|
fx.id AS flexoptix_id,
|
|
fx.part_number AS flexoptix_pn,
|
|
fx.standard_name AS flexoptix_std,
|
|
fx.form_factor AS flexoptix_form_factor,
|
|
fx.speed AS flexoptix_speed,
|
|
fx.reach_label AS flexoptix_reach,
|
|
fx.product_page_url AS flexoptix_url,
|
|
fx.price_verified_eur AS flexoptix_price_eur,
|
|
-- Competitor side
|
|
cx.id AS competitor_id,
|
|
cx.part_number AS competitor_pn,
|
|
cx.standard_name AS competitor_std,
|
|
cx.form_factor AS competitor_form_factor,
|
|
cx.speed AS competitor_speed,
|
|
cx.reach_label AS competitor_reach,
|
|
cx.product_page_url AS competitor_url,
|
|
cx.price_verified_eur AS competitor_price_eur,
|
|
cv.name AS competitor_vendor,
|
|
cv.website AS competitor_vendor_website
|
|
FROM transceiver_equivalences e
|
|
JOIN transceivers fx ON fx.id = e.flexoptix_id
|
|
JOIN transceivers cx ON cx.id = e.competitor_id
|
|
JOIN vendors cv ON cv.id = cx.vendor_id
|
|
WHERE (e.flexoptix_id = $1::uuid OR e.competitor_id = $1::uuid)
|
|
AND e.status IN ('approved', 'auto_approved')
|
|
ORDER BY e.confidence DESC`,
|
|
[id]
|
|
);
|
|
res.json({ success: true, data: result.rows, total: result.rows.length });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: (err as Error).message });
|
|
}
|
|
});
|
|
|
|
// GET /api/equivalences/stats — overview numbers
|
|
equivalencesRouter.get("/stats", async (_req: Request, res: Response) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
COUNT(*) FILTER (WHERE status IN ('approved','auto_approved')) AS active,
|
|
COUNT(DISTINCT competitor_id) FILTER (WHERE status IN ('approved','auto_approved')) AS unique_competitor_products,
|
|
COUNT(DISTINCT flexoptix_id) FILTER (WHERE status IN ('approved','auto_approved')) AS unique_flexoptix_products,
|
|
COUNT(DISTINCT cv.name) AS unique_competitor_vendors,
|
|
AVG(confidence) FILTER (WHERE status IN ('approved','auto_approved'))::numeric(4,3) AS avg_confidence
|
|
FROM transceiver_equivalences e
|
|
JOIN transceivers cx ON cx.id = e.competitor_id
|
|
JOIN vendors cv ON cv.id = cx.vendor_id
|
|
`);
|
|
res.json({ success: true, stats: result.rows[0] });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: (err as Error).message });
|
|
}
|
|
});
|
|
|
|
// GET /api/equivalences/top-vendors — which competitor vendors have most equivalences
|
|
equivalencesRouter.get("/top-vendors", async (_req: Request, res: Response) => {
|
|
try {
|
|
const result = await pool.query(`
|
|
SELECT
|
|
cv.name AS vendor,
|
|
cv.website,
|
|
COUNT(*) AS equiv_count,
|
|
COUNT(DISTINCT e.competitor_id) AS products_covered,
|
|
AVG(e.confidence)::numeric(4,3) AS avg_confidence
|
|
FROM transceiver_equivalences e
|
|
JOIN transceivers cx ON cx.id = e.competitor_id
|
|
JOIN vendors cv ON cv.id = cx.vendor_id
|
|
WHERE e.status IN ('approved','auto_approved')
|
|
GROUP BY cv.name, cv.website
|
|
ORDER BY equiv_count DESC
|
|
LIMIT 20
|
|
`);
|
|
res.json({ success: true, data: result.rows });
|
|
} catch (err) {
|
|
res.status(500).json({ success: false, error: (err as Error).message });
|
|
}
|
|
});
|