From 6f5b47340847cde843bd375715735911a1813a72 Mon Sep 17 00:00:00 2001 From: mandalore Date: Fri, 26 Jun 2026 21:29:23 -0500 Subject: [PATCH] add Career W% column from BCP all-events history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each roster player, fire a parallel fetch to /placings?placingsType=player&userId={id} and aggregate wins/losses/ties across all events that have those fields. Renders asynchronously - shows … while loading, — if no data found. Column is sortable. Co-Authored-By: Claude Sonnet 4.6 --- index.html | 1 + src/main.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index 8c3a934..893f52d 100644 --- a/index.html +++ b/index.html @@ -75,6 +75,7 @@ ITC Points W–L–T Win % + Career W% ITC Rank diff --git a/src/main.js b/src/main.js index a8da12b..430e46e 100644 --- a/src/main.js +++ b/src/main.js @@ -60,6 +60,44 @@ const fmtPts = (n) => Number(n || 0).toLocaleString(undefined, { minimumFraction const fmtWpct = (w, l, t) => { const g = (+w||0)+(+l||0)+(+t||0); return g ? ((+w||0)/g*100).toFixed(1)+'%' : '—'; }; const fullName = (u = {}) => [u.firstName, u.lastName].filter(Boolean).join(" ").trim(); +/* ---- Career stats ---- */ +const careerData = {}; // userId → { w, l, t } | null (in-flight) + +async function fetchCareer(userId) { + const r = await fetch( + `${API}/placings?placingsType=player&userId=${userId}&limit=1000`, + { headers: HEADERS, credentials: 'omit' } + ); + if (!r.ok) return { w: 0, l: 0, t: 0 }; + const p = await r.json(); + const items = Array.isArray(p) ? p : (p.data || []); + let w = 0, l = 0, t = 0; + for (const item of items) { + const v = item.value || item; + if (v.wins != null || v.losses != null) { + w += +(v.wins || 0); + l += +(v.losses || 0); + t += +(v.ties || 0); + } + } + return { w, l, t }; +} + +async function loadCareerStats(kept) { + const toFetch = kept.filter(r => { + const uid = r.userId || r.user?.id; + return uid && !(uid in careerData); + }); + if (!toFetch.length) return; + toFetch.forEach(r => { careerData[r.userId || r.user?.id] = null; }); + await Promise.allSettled(toFetch.map(async r => { + const uid = r.userId || r.user?.id; + try { careerData[uid] = await fetchCareer(uid); } + catch { careerData[uid] = { w: 0, l: 0, t: 0 }; } + })); + if (lastState) render(lastState); +} + /* ---- Sort state ---- */ let sortState = { col: 'pts', dir: 'desc' }; let lastState = null; @@ -69,7 +107,8 @@ const sortCols = { name: { key: r => fullName(r.user || {}).toLowerCase() || '', defaultDir: 'asc' }, wlt: { key: r => (+r.wins||0) * 10000 - (+r.losses||0) * 100 - (+r.ties||0), defaultDir: 'desc' }, wpct: { key: r => { const g=(+r.wins||0)+(+r.losses||0)+(+r.ties||0); return g?(+r.wins||0)/g:-1; }, defaultDir: 'desc' }, - rank: { key: r => +(r.placing || Infinity), defaultDir: 'asc' }, + rank: { key: r => +(r.placing || Infinity), defaultDir: 'asc' }, + cwpct: { key: r => { const d = careerData[r.userId || r.user?.id]; if (!d) return -1; const g = d.w+d.l+d.t; return g ? d.w/g : -1; }, defaultDir: 'desc' }, }; function applySort(rows) { @@ -211,6 +250,9 @@ function render(state) { $("s-rec").textContent = fmtWpct(W, L, T); $("s-best").textContent = isFinite(best) ? ("#" + best) : "—"; + // Kick off career stat fetches for any players not yet loaded (non-blocking) + loadCareerStats(kept); + // Rows applySort(kept).forEach((r, i) => { const u = r.user || {}, name = fullName(u) || u.nickname || "Unknown"; @@ -223,6 +265,12 @@ function render(state) { `` + `${r.wins || 0}${r.losses || 0}–${r.ties || 0}` + `${fmtWpct(r.wins, r.losses, r.ties)}` + + (() => { const uid = r.userId || r.user?.id; const d = uid && careerData[uid]; + return `${ + d == null ? '' + : (d.w+d.l+d.t) === 0 ? '' + : fmtWpct(d.w, d.l, d.t) + }`; })() + `#${r.placing ?? "—"}`; tb.appendChild(tr); }); @@ -290,6 +338,7 @@ function showSkeleton() { `
` + `
` + `
` + + `
` + `
`; tb.appendChild(tr); }