mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-04-27 15:07:43 +00:00
feat: Better sale priorities
This commit is contained in:
parent
6ccd932556
commit
a91406ba11
1 changed files with 60 additions and 42 deletions
|
|
@ -9,7 +9,7 @@ export function renderSearch($app) {
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h1">Spirit Tracker</h1>
|
<h1 class="h1">Spirit Tracker Viz</h1>
|
||||||
<div class="small">Search name / url / sku (word AND)</div>
|
<div class="small">Search name / url / sku (word AND)</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn" href="#/link" style="text-decoration:none;">Link SKUs</a>
|
<a class="btn" href="#/link" style="text-decoration:none;">Link SKUs</a>
|
||||||
|
|
@ -81,9 +81,9 @@ export function renderSearch($app) {
|
||||||
const storeBadge = href
|
const storeBadge = href
|
||||||
? `<a class="badge" href="${esc(
|
? `<a class="badge" href="${esc(
|
||||||
href
|
href
|
||||||
)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">${esc(
|
)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">${esc(store)}${esc(
|
||||||
store
|
plus
|
||||||
)}${esc(plus)}</a>`
|
)}</a>`
|
||||||
: `<span class="badge">${esc(store)}${esc(plus)}</span>`;
|
: `<span class="badge">${esc(store)}${esc(plus)}</span>`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|
@ -147,13 +147,22 @@ export function renderSearch($app) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom priority:
|
// Custom priority:
|
||||||
// big % discounts > new unique > small discounts > removals > price increases > new (available elsewhere)
|
// - Sales that make this store cheapest (or tied cheapest) are most interesting
|
||||||
|
// - New unique (no other stores have canonical SKU)
|
||||||
|
// - Other sales (not cheapest) are demoted
|
||||||
|
// - Removed
|
||||||
|
// - Price increases
|
||||||
|
// - New (available elsewhere)
|
||||||
function rankRecent(r, canonSkuFn) {
|
function rankRecent(r, canonSkuFn) {
|
||||||
const rawSku = String(r?.sku || "");
|
const rawSku = String(r?.sku || "");
|
||||||
const sku = String(canonSkuFn ? canonSkuFn(rawSku) : rawSku);
|
const sku = String(canonSkuFn ? canonSkuFn(rawSku) : rawSku);
|
||||||
|
|
||||||
const agg = aggBySku.get(sku) || null;
|
const agg = aggBySku.get(sku) || null;
|
||||||
const storeCount = agg?.stores?.size || 0;
|
|
||||||
|
const storeLabelRaw = String(r?.storeLabel || r?.store || "").trim();
|
||||||
|
const bestStoreRaw = String(agg?.cheapestStoreLabel || "").trim();
|
||||||
|
|
||||||
|
const normStore = (s) => String(s || "").trim().toLowerCase();
|
||||||
|
|
||||||
// Treat "price_change" as down/up if we can infer direction
|
// Treat "price_change" as down/up if we can infer direction
|
||||||
let kind = String(r?.kind || "");
|
let kind = String(r?.kind || "");
|
||||||
|
|
@ -170,41 +179,52 @@ export function renderSearch($app) {
|
||||||
const pctUp = kind === "price_up" ? pctChange(r?.oldPrice || "", r?.newPrice || "") : null;
|
const pctUp = kind === "price_up" ? pctChange(r?.oldPrice || "", r?.newPrice || "") : null;
|
||||||
|
|
||||||
const isNew = kind === "new";
|
const isNew = kind === "new";
|
||||||
const isNewUnique = isNew && storeCount <= 1; // "across the board" (no other stores for canonical SKU)
|
const storeCount = agg?.stores?.size || 0;
|
||||||
const isNewOther = isNew && storeCount > 1;
|
const isNewUnique = isNew && storeCount <= 1;
|
||||||
|
|
||||||
const BIG_OFF = 15;
|
// For sales: demote if this store is NOT the cheapest available now (per aggregate index)
|
||||||
|
const newPriceNum = kind === "price_down" || kind === "price_up" ? parsePriceToNumber(r?.newPrice || "") : null;
|
||||||
|
const bestPriceNum = Number.isFinite(agg?.cheapestPriceNum) ? agg.cheapestPriceNum : null;
|
||||||
|
|
||||||
|
const EPS = 0.01;
|
||||||
|
const priceMatchesBest =
|
||||||
|
Number.isFinite(newPriceNum) && Number.isFinite(bestPriceNum) ? Math.abs(newPriceNum - bestPriceNum) <= EPS : false;
|
||||||
|
|
||||||
|
const storeIsBest = normStore(storeLabelRaw) && normStore(bestStoreRaw) && normStore(storeLabelRaw) === normStore(bestStoreRaw);
|
||||||
|
|
||||||
|
const saleIsCheapestHere = kind === "price_down" && storeIsBest && priceMatchesBest;
|
||||||
|
const saleIsTiedCheapest = kind === "price_down" && !storeIsBest && priceMatchesBest;
|
||||||
|
|
||||||
// Higher score => earlier
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
if (kind === "price_down") {
|
if (kind === "price_down") {
|
||||||
if (pctOff !== null && pctOff >= BIG_OFF) score = 6000 + pctOff;
|
if (saleIsCheapestHere) {
|
||||||
else score = 4000 + (pctOff || 0);
|
score = 6500 + (pctOff || 0);
|
||||||
|
} else if (saleIsTiedCheapest) {
|
||||||
|
score = 5900 + Math.floor((pctOff || 0) * 0.5);
|
||||||
|
} else {
|
||||||
|
score = 2400 + Math.min(25, Math.max(0, pctOff || 0));
|
||||||
|
}
|
||||||
} else if (isNewUnique) {
|
} else if (isNewUnique) {
|
||||||
score = 5000;
|
score = 6000;
|
||||||
} else if (kind === "restored") {
|
} else if (kind === "restored") {
|
||||||
// not specified, but generally interesting; keep near "new unique"
|
score = 5200;
|
||||||
score = 4500;
|
|
||||||
} else if (kind === "removed") {
|
} else if (kind === "removed") {
|
||||||
score = 3000;
|
score = 3000;
|
||||||
} else if (kind === "price_up") {
|
} else if (kind === "price_up") {
|
||||||
score = 2000 + Math.min(99, Math.max(0, pctUp || 0));
|
score = 2000 + Math.min(99, Math.max(0, pctUp || 0));
|
||||||
} else if (isNewOther) {
|
} else if (kind === "new") {
|
||||||
score = 1000;
|
score = 1000;
|
||||||
} else {
|
} else {
|
||||||
score = 0;
|
score = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Within same score bucket:
|
|
||||||
// - discounts: larger pctOff first
|
|
||||||
// - else: more recent first
|
|
||||||
let tie = 0;
|
let tie = 0;
|
||||||
if (kind === "price_down") tie = (pctOff || 0) * 100000 + tsValue(r);
|
if (kind === "price_down") tie = (pctOff || 0) * 100000 + tsValue(r);
|
||||||
else if (kind === "price_up") tie = (pctUp || 0) * 100000 + tsValue(r);
|
else if (kind === "price_up") tie = (pctUp || 0) * 100000 + tsValue(r);
|
||||||
else tie = tsValue(r);
|
else tie = tsValue(r);
|
||||||
|
|
||||||
return { sku, kind, pctOff, storeCount, isNewUnique, isNewOther, score, tie };
|
return { sku, kind, pctOff, storeCount, isNewUnique, score, tie };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRecent(recent, canonicalSkuFn) {
|
function renderRecent(recent, canonicalSkuFn) {
|
||||||
|
|
@ -216,6 +236,7 @@ export function renderSearch($app) {
|
||||||
|
|
||||||
const canon = typeof canonicalSkuFn === "function" ? canonicalSkuFn : (x) => x;
|
const canon = typeof canonicalSkuFn === "function" ? canonicalSkuFn : (x) => x;
|
||||||
|
|
||||||
|
// Filter to last 24 hours
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
const cutoffMs = nowMs - 24 * 60 * 60 * 1000;
|
const cutoffMs = nowMs - 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
|
@ -240,7 +261,6 @@ export function renderSearch($app) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rank + sort (custom)
|
|
||||||
const ranked = inWindow
|
const ranked = inWindow
|
||||||
.map((r) => ({ r, meta: rankRecent(r, canon) }))
|
.map((r) => ({ r, meta: rankRecent(r, canon) }))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
|
@ -294,10 +314,8 @@ export function renderSearch($app) {
|
||||||
)}</a>`
|
)}</a>`
|
||||||
: `<span class="badge">${esc((r.storeLabel || "") + plus)}</span>`;
|
: `<span class="badge">${esc((r.storeLabel || "") + plus)}</span>`;
|
||||||
|
|
||||||
// date as a badge so it sits nicely in the single meta row
|
|
||||||
const dateBadge = when ? `<span class="badge mono">${esc(when)}</span>` : "";
|
const dateBadge = when ? `<span class="badge mono">${esc(when)}</span>` : "";
|
||||||
|
|
||||||
// subtle styles (inline so you don’t need to touch CSS)
|
|
||||||
const offBadge =
|
const offBadge =
|
||||||
meta.kind === "price_down" && meta.pctOff !== null
|
meta.kind === "price_down" && meta.pctOff !== null
|
||||||
? `<span class="badge" style="margin-left:6px; color:rgba(20,110,40,0.95); background:rgba(20,110,40,0.10); border:1px solid rgba(20,110,40,0.20);">[${esc(
|
? `<span class="badge" style="margin-left:6px; color:rgba(20,110,40,0.95); background:rgba(20,110,40,0.10); border:1px solid rgba(20,110,40,0.20);">[${esc(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue