mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
108 lines
3.1 KiB
JavaScript
108 lines
3.1 KiB
JavaScript
import { normImg } from "./dom.js";
|
|
import { parsePriceToNumber, keySkuForRow, normSearchText } from "./sku.js";
|
|
|
|
// Build one row per *canonical* SKU (after applying sku map) + combined searchable text
|
|
export function aggregateBySku(listings, canonicalizeSkuFn) {
|
|
const canon = typeof canonicalizeSkuFn === "function" ? canonicalizeSkuFn : (x) => x;
|
|
|
|
const bySku = new Map();
|
|
|
|
for (const r of listings) {
|
|
const rawSku = keySkuForRow(r);
|
|
const sku = canon(rawSku);
|
|
|
|
const name = String(r?.name || "");
|
|
const url = String(r?.url || "");
|
|
const storeLabel = String(r?.storeLabel || r?.store || "");
|
|
const removed = Boolean(r?.removed);
|
|
|
|
const img = normImg(r?.img || r?.image || r?.thumb || "");
|
|
|
|
const pNum = parsePriceToNumber(r?.price);
|
|
const pStr = String(r?.price || "");
|
|
|
|
let agg = bySku.get(sku);
|
|
if (!agg) {
|
|
agg = {
|
|
sku, // canonical sku
|
|
name: name || "",
|
|
img: "",
|
|
cheapestPriceStr: pStr || "",
|
|
cheapestPriceNum: pNum,
|
|
cheapestStoreLabel: storeLabel || "",
|
|
stores: new Set(), // LIVE stores only
|
|
storesEver: new Set(), // live + removed presence (history)
|
|
sampleUrl: url || "",
|
|
_searchParts: [],
|
|
searchText: "",
|
|
|
|
_imgByName: new Map(),
|
|
_imgAny: "",
|
|
};
|
|
bySku.set(sku, agg);
|
|
}
|
|
|
|
if (storeLabel) {
|
|
agg.storesEver.add(storeLabel);
|
|
if (!removed) agg.stores.add(storeLabel);
|
|
}
|
|
if (!agg.sampleUrl && url) agg.sampleUrl = url;
|
|
|
|
// Keep first non-empty name, but keep thumbnail aligned to chosen name
|
|
if (!agg.name && name) {
|
|
agg.name = name;
|
|
if (img) agg.img = img;
|
|
} else if (agg.name && name === agg.name && img && !agg.img) {
|
|
agg.img = img;
|
|
}
|
|
|
|
if (img) {
|
|
if (!agg._imgAny) agg._imgAny = img;
|
|
if (name) agg._imgByName.set(name, img);
|
|
}
|
|
|
|
// cheapest across LIVE rows only (so removed history doesn't "win")
|
|
if (!removed && pNum !== null) {
|
|
if (agg.cheapestPriceNum === null || pNum < agg.cheapestPriceNum) {
|
|
agg.cheapestPriceNum = pNum;
|
|
agg.cheapestPriceStr = pStr || "";
|
|
agg.cheapestStoreLabel = storeLabel || agg.cheapestStoreLabel;
|
|
}
|
|
}
|
|
|
|
// search parts: include canonical + raw sku so searching either works
|
|
agg._searchParts.push(sku);
|
|
if (rawSku && rawSku !== sku) agg._searchParts.push(rawSku);
|
|
if (name) agg._searchParts.push(name);
|
|
if (url) agg._searchParts.push(url);
|
|
if (storeLabel) agg._searchParts.push(storeLabel);
|
|
if (removed) agg._searchParts.push("removed");
|
|
}
|
|
|
|
const out = [...bySku.values()];
|
|
|
|
for (const it of out) {
|
|
if (!it.img) {
|
|
const m = it._imgByName;
|
|
if (it.name && m && m.has(it.name)) it.img = m.get(it.name) || "";
|
|
else it.img = it._imgAny || "";
|
|
}
|
|
|
|
delete it._imgByName;
|
|
delete it._imgAny;
|
|
|
|
it.storeCount = it.stores.size;
|
|
it.storeCountEver = it.storesEver.size;
|
|
it.removedEverywhere = it.storeCount === 0;
|
|
|
|
it._searchParts.push(it.sku);
|
|
it._searchParts.push(it.name || "");
|
|
it._searchParts.push(it.sampleUrl || "");
|
|
it._searchParts.push(it.cheapestStoreLabel || "");
|
|
it.searchText = normSearchText(it._searchParts.join(" | "));
|
|
delete it._searchParts;
|
|
}
|
|
|
|
out.sort((a, b) => (String(a.name) + a.sku).localeCompare(String(b.name) + b.sku));
|
|
return out;
|
|
}
|