add Career W% column from BCP all-events history
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 <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,7 @@
|
|||||||
<th class="text-right cursor-pointer select-none" data-sort="pts">ITC Points</th>
|
<th class="text-right cursor-pointer select-none" data-sort="pts">ITC Points</th>
|
||||||
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wlt">W–L–T</th>
|
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wlt">W–L–T</th>
|
||||||
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wpct">Win %</th>
|
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="wpct">Win %</th>
|
||||||
|
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="cwpct">Career W%</th>
|
||||||
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="rank">ITC Rank</th>
|
<th class="text-right hidden sm:table-cell cursor-pointer select-none" data-sort="rank">ITC Rank</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
+50
-1
@@ -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 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();
|
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 ---- */
|
/* ---- Sort state ---- */
|
||||||
let sortState = { col: 'pts', dir: 'desc' };
|
let sortState = { col: 'pts', dir: 'desc' };
|
||||||
let lastState = null;
|
let lastState = null;
|
||||||
@@ -69,7 +107,8 @@ const sortCols = {
|
|||||||
name: { key: r => fullName(r.user || {}).toLowerCase() || '', defaultDir: 'asc' },
|
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' },
|
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' },
|
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) {
|
function applySort(rows) {
|
||||||
@@ -211,6 +250,9 @@ function render(state) {
|
|||||||
$("s-rec").textContent = fmtWpct(W, L, T);
|
$("s-rec").textContent = fmtWpct(W, L, T);
|
||||||
$("s-best").textContent = isFinite(best) ? ("#" + best) : "—";
|
$("s-best").textContent = isFinite(best) ? ("#" + best) : "—";
|
||||||
|
|
||||||
|
// Kick off career stat fetches for any players not yet loaded (non-blocking)
|
||||||
|
loadCareerStats(kept);
|
||||||
|
|
||||||
// Rows
|
// Rows
|
||||||
applySort(kept).forEach((r, i) => {
|
applySort(kept).forEach((r, i) => {
|
||||||
const u = r.user || {}, name = fullName(u) || u.nickname || "Unknown";
|
const u = r.user || {}, name = fullName(u) || u.nickname || "Unknown";
|
||||||
@@ -223,6 +265,12 @@ function render(state) {
|
|||||||
`<td class="text-right font-mono hidden sm:table-cell">` +
|
`<td class="text-right font-mono hidden sm:table-cell">` +
|
||||||
`<span class="text-success">${r.wins || 0}</span>–<span class="text-error">${r.losses || 0}</span>–${r.ties || 0}</td>` +
|
`<span class="text-success">${r.wins || 0}</span>–<span class="text-error">${r.losses || 0}</span>–${r.ties || 0}</td>` +
|
||||||
`<td class="text-right font-mono hidden sm:table-cell">${fmtWpct(r.wins, r.losses, r.ties)}</td>` +
|
`<td class="text-right font-mono hidden sm:table-cell">${fmtWpct(r.wins, r.losses, r.ties)}</td>` +
|
||||||
|
(() => { const uid = r.userId || r.user?.id; const d = uid && careerData[uid];
|
||||||
|
return `<td class="text-right font-mono hidden sm:table-cell">${
|
||||||
|
d == null ? '<span class="opacity-20">…</span>'
|
||||||
|
: (d.w+d.l+d.t) === 0 ? '<span class="opacity-30">—</span>'
|
||||||
|
: fmtWpct(d.w, d.l, d.t)
|
||||||
|
}</td>`; })() +
|
||||||
`<td class="text-right font-mono text-base-content/40 hidden sm:table-cell">#${r.placing ?? "—"}</td>`;
|
`<td class="text-right font-mono text-base-content/40 hidden sm:table-cell">#${r.placing ?? "—"}</td>`;
|
||||||
tb.appendChild(tr);
|
tb.appendChild(tr);
|
||||||
});
|
});
|
||||||
@@ -290,6 +338,7 @@ function showSkeleton() {
|
|||||||
`<td><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
|
`<td><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
|
||||||
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-14 ml-auto"></div></td>` +
|
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-14 ml-auto"></div></td>` +
|
||||||
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
|
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
|
||||||
|
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-12 ml-auto"></div></td>` +
|
||||||
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-10 ml-auto"></div></td>`;
|
`<td class="hidden sm:table-cell"><div class="skeleton h-4 w-10 ml-auto"></div></td>`;
|
||||||
tb.appendChild(tr);
|
tb.appendChild(tr);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user