G) SKU Bulk Pricer — POST /api/bulk-price (bis 100 Part Numbers), Preistabelle je Vendor, CSV-Export, Not-Found-Liste H) Side-by-side Comparison — Checkboxes in TX-Tabelle, Floating Comparison-Tray (max 4 SKUs), Modal mit Specs + Best-Price 7d nebeneinander I) Vendor Reliability Score — GET /api/vendors/reliability, Freshness(40) + Frequency(30) + Coverage(30) = 0–100, Progress-Bar-Badge auf Vendor-Cards J) Price Heat Map — GET /api/price-matrix?ids=, Row-normalisierte Farbmatrix SKU×Vendor (grün=günstig/rot=teuer), Sticky SKU-Spalte, Best-Price-Spalte K) Watchlist — localStorage-basiert (⭐/☆ in TX-Tabelle), Floating Drawer, live Preis-Update via Einzelabruf L) PDF / Print Report — window.print() + dediziertes @media print CSS (blendet UI-Chrome aus, behält Overview-Content) M) Global Search Overlay — Cmd+K / Ctrl+K, durchsucht Transceivers + KB + News + Documents gleichzeitig, clickbare Direktlinks N) Saved Filter Presets — localStorage tip_presets, Dropdown + 💾-Button in TX-Filterzeile, Save/Load/Delete O) Price Forecast — GET /api/price-forecast/:id (lineare Regression 90d → 30d Forecast), gestricheltes Overlay auf Price-History-Chart, Trend-Label (rising/stable/declining) P) Technology Radar — SVG Bull's-Eye (Adopt/Trial/Assess/Hold), Hype-Cycle-Phasen → Ringe gemappt, Bubbles mit Market-Signal-Score, Quadrant-Labels, interaktive Tooltips
84 lines
3.0 KiB
TypeScript
84 lines
3.0 KiB
TypeScript
/**
|
||
* Vendor Reliability Scores
|
||
*
|
||
* Routes:
|
||
* GET /api/vendor-reliability — Reliability score 0–100 per vendor
|
||
*/
|
||
import { Router, Request, Response } from "express";
|
||
import { pool } from "../db/client";
|
||
|
||
export const vendorReliabilityRouter = Router();
|
||
|
||
// ─── GET /api/vendor-reliability ─────────────────────────────────────────────
|
||
vendorReliabilityRouter.get("/", async (_req: Request, res: Response) => {
|
||
try {
|
||
const result = await pool.query<{
|
||
vendor_id: number;
|
||
vendor_name: string;
|
||
last_observation: Date;
|
||
obs_30d: string;
|
||
distinct_skus_60d: string;
|
||
days_since_last: string;
|
||
}>(`
|
||
WITH base AS (
|
||
SELECT
|
||
po.source_vendor_id AS vendor_id,
|
||
MAX(po.time) AS last_observation,
|
||
COUNT(*) FILTER (WHERE po.time > NOW() - INTERVAL '30 days')
|
||
AS obs_30d,
|
||
COUNT(DISTINCT po.transceiver_id)
|
||
FILTER (WHERE po.time > NOW() - INTERVAL '60 days')
|
||
AS distinct_skus_60d
|
||
FROM price_observations po
|
||
WHERE po.time > NOW() - INTERVAL '90 days'
|
||
GROUP BY po.source_vendor_id
|
||
)
|
||
SELECT
|
||
b.vendor_id,
|
||
v.name AS vendor_name,
|
||
b.last_observation,
|
||
b.obs_30d,
|
||
b.distinct_skus_60d,
|
||
EXTRACT(EPOCH FROM (NOW() - b.last_observation)) / 86400.0 AS days_since_last
|
||
FROM base b
|
||
JOIN vendors v ON v.id = b.vendor_id
|
||
ORDER BY b.last_observation DESC
|
||
`);
|
||
|
||
const vendors = result.rows.map((row) => {
|
||
const days = parseFloat(row.days_since_last);
|
||
const obs30d = parseInt(row.obs_30d, 10);
|
||
const skus60d = parseInt(row.distinct_skus_60d, 10);
|
||
|
||
const freshnessScore =
|
||
days <= 7 ? 40 :
|
||
days <= 14 ? 30 :
|
||
days <= 30 ? 20 :
|
||
days <= 60 ? 10 : 0;
|
||
|
||
const frequencyScore = Math.min(Math.round((obs30d / 100) * 30), 30);
|
||
const coverageScore = Math.min(Math.round((skus60d / 500) * 30), 30);
|
||
const reliabilityScore = freshnessScore + frequencyScore + coverageScore;
|
||
|
||
return {
|
||
vendor_id: row.vendor_id,
|
||
vendor_name: row.vendor_name,
|
||
reliability_score: reliabilityScore,
|
||
freshness_score: freshnessScore,
|
||
frequency_score: frequencyScore,
|
||
coverage_score: coverageScore,
|
||
last_observation: row.last_observation.toISOString().slice(0, 10),
|
||
obs_30d: obs30d,
|
||
distinct_skus_60d: skus60d,
|
||
};
|
||
});
|
||
|
||
vendors.sort((a, b) => b.reliability_score - a.reliability_score);
|
||
|
||
res.json({ success: true, vendors });
|
||
} catch (err) {
|
||
console.error("GET /api/vendor-reliability error:", err);
|
||
res.status(500).json({ success: false, error: String(err) });
|
||
}
|
||
});
|