transceiver-db/packages/api/src/routes/vendor-reliability.ts
Rene Fichtmueller e71b985c52 feat: 10 weitere Dashboard-Features (G–P)
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
2026-05-14 21:39:17 +02:00

84 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Vendor Reliability Scores
*
* Routes:
* GET /api/vendor-reliability — Reliability score 0100 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) });
}
});