/* ============================================================ Season — scorecard stats + handicaps for every member. Visible to ALL members. Sortable columns (click a header). Aggregates every POSTED weekend's stats (stored on the season doc) plus the live current weekend. Managers also get the "post this weekend" control. ============================================================ */ function SeasonScreen({ ctx }) { const Y = window.YCC; const { seasonDoc, accountMembers, tour, scores, sideResults, gameConfig, postWeekendResults, clearWeekendResults, isManagerUser, member } = ctx; const ENG = window.YCC_SCORING; const fullName = (m) => `${m.first_name || ""} ${m.last_name || ""}`.trim() || m.email; const initials = (n) => (n || "?").trim().split(/\s+/).map((s) => s[0]).join("").slice(0, 2).toUpperCase(); const [sort, setSort] = useState({ key: "wins", dir: "desc" }); // ---- aggregate per-member stats across the season ---- const postedStats = (seasonDoc && seasonDoc.stats) || {}; const live = ENG.weekendStats ? ENG.weekendStats({ tour, scores, sideResults, gameConfig }) : {}; const NUM_KEYS = ["eagles", "birdies", "pars", "bogeys", "doubles", "bogeyPlus", "holes", "strokes", "toPar", "rounds", "ctp", "longDrive", "wins"]; const agg = {}; const blank = () => { const o = { weekendsWon: 0 }; NUM_KEYS.forEach((k) => (o[k] = 0)); return o; }; const addWeekend = (byName) => { Object.entries(byName || {}).forEach(([name, s]) => { const a = agg[name] || (agg[name] = blank()); NUM_KEYS.forEach((k) => { a[k] += (s[k] || 0); }); if ((s.wins || 0) > 0) a.weekendsWon += 1; }); }; Object.entries(postedStats).forEach(([wk, byName]) => addWeekend(byName)); // include the live current weekend only if it isn't already posted (avoids double count) const liveIncluded = !postedStats[tour.satISO]; if (liveIncluded) addWeekend(live); // handicap lookup (current index) by member name const hiByName = {}; (accountMembers || []).forEach((m) => { const n = fullName(m); if (m.handicap_index != null) hiByName[n] = m.handicap_index; }); if (member && member.name && member.handicapIndex != null && hiByName[member.name] == null) hiByName[member.name] = member.handicapIndex; // universe of names: every account member + anyone with stats const names = new Set(); (accountMembers || []).forEach((m) => names.add(fullName(m))); Object.keys(agg).forEach((n) => names.add(n)); if (member && member.name) names.add(member.name); const rows = Array.from(names).map((name) => { const a = agg[name] || blank(); const avg = a.holes ? Math.round((a.strokes / a.holes) * 18) : null; return { name, hi: hiByName[name] != null ? hiByName[name] : null, rounds: a.rounds, eagles: a.eagles, birdies: a.birdies, pars: a.pars, bogeys: a.bogeys, doubles: a.doubles, bogeyPlus: a.bogeyPlus, avg, ctp: a.ctp, longDrive: a.longDrive, wins: a.wins, weekendsWon: a.weekendsWon, played: a.holes > 0, }; }); // ---- sorting ---- const COLS = [ { key: "name", label: "Member", text: true }, { key: "hi", label: "HI", hint: "Handicap Index" }, { key: "rounds", label: "Rds", hint: "Rounds played" }, { key: "eagles", label: "Eagles" }, { key: "birdies", label: "Birdies" }, { key: "pars", label: "Pars" }, { key: "bogeys", label: "Bogeys" }, { key: "doubles", label: "Dbl", hint: "Double bogeys" }, { key: "bogeyPlus", label: "Bgy+", hint: "Triple bogey or worse" }, { key: "avg", label: "Avg", hint: "Scoring average per 18 holes", lowGood: true }, { key: "ctp", label: "CTP", hint: "Closest to the pin" }, { key: "longDrive", label: "Long Dr", hint: "Longest drive" }, { key: "wins", label: "Wins", hint: "Game wins (most wins by day)" }, { key: "weekendsWon", label: "Wknds", hint: "Weekends won" }, ]; function clickSort(key) { const col = COLS.find((c) => c.key === key); setSort((s) => s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: col && col.text ? "asc" : "desc" }); } const sorted = [...rows].sort((a, b) => { const { key, dir } = sort; if (key === "name") return dir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); let av = a[key], bv = b[key]; av = (typeof av === "number" && isFinite(av)) ? av : null; bv = (typeof bv === "number" && isFinite(bv)) ? bv : null; if (av == null && bv == null) return a.name.localeCompare(b.name); if (av == null) return 1; if (bv == null) return -1; if (av === bv) return a.name.localeCompare(b.name); return dir === "asc" ? av - bv : bv - av; }); // ---- summary tiles ---- const seasonWeeks = Object.keys(postedStats).length; const totalBirdiesEagles = rows.reduce((n, r) => n + r.eagles + r.birdies, 0); const totalRounds = rows.reduce((n, r) => n + r.rounds, 0); const winLeader = [...rows].filter((r) => r.wins > 0).sort((a, b) => b.wins - a.wins)[0]; const numCellStyle = (highlight) => ({ padding: "10px 12px", textAlign: "center", fontWeight: 600, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", color: highlight ? "var(--brand)" : "var(--ink)" }); return (
DIV Tour

Season

Every member's scorecard stats and handicap across the season. {liveIncluded ? "Includes this weekend's live scores. " : ""}Tap any column heading to sort.

{/* summary */}
1 ? "s" : ""}` : "no wins yet"} accent />
{/* posting to the season is done from the Manager · Tour Setup tab (rounds must be certified first) */} {/* stats table */}

Member stats

Sorted by {COLS.find((c) => c.key === sort.key).label} {sort.dir === "asc" ? "↑" : "↓"}
{COLS.map((c) => { const active = sort.key === c.key; const stub = c.key === "name"; return ( ); })} {sorted.map((r, i) => { const leader = i === 0 && sort.key !== "name"; return ( ); })}
clickSort(c.key)} title={c.hint || c.label} style={{ padding: "11px 12px", fontSize: 11, fontWeight: 700, letterSpacing: ".04em", textTransform: "uppercase", color: active ? "var(--brand)" : "var(--ink-soft)", whiteSpace: "nowrap", cursor: "pointer", userSelect: "none", textAlign: stub ? "left" : "center", position: stub ? "sticky" : "static", left: stub ? 0 : "auto", background: "var(--surface-2)", zIndex: stub ? 2 : 1, borderBottom: active ? "2px solid var(--accent)" : "2px solid transparent", }}> {c.label} {active ? (sort.dir === "asc" ? "▲" : "▼") : "↕"}
{r.name === (member && member.name) ? r.name + " (you)" : r.name}
{r.hi == null ? : r.hi} {r.rounds || } 0)}>{r.eagles || } 0)}>{r.birdies || } {r.pars || } {r.bogeys || } {r.doubles || } {r.bogeyPlus || } {r.avg == null ? : r.avg} 0)}>{r.ctp ? {r.ctp} : } 0)}>{r.longDrive ? {r.longDrive} : } 0 ? "var(--accent-deep)" : "var(--brand)" }}>{r.wins || } {r.weekendsWon || }
Avg = gross scoring average per 18 holes Bgy+ = triple bogey or worse CTP = closest-to-the-pin wins Wknds = weekends with a win
); } /* -------- Manager-only: certify a weekend's rounds & post them to member stats. Lives on the Manager · Tour Setup tab. Posting is gated on certification. -------- */ function PostWeekendCard({ ctx }) { const Y = window.YCC; const ENG = window.YCC_SCORING; const { tour, scores, gameConfig, seasonDoc, postWeekendResults, clearWeekendResults } = ctx; const initials = (n) => (n || "?").trim().split(/\s+/).map((s) => s[0]).join("").slice(0, 2).toUpperCase(); const preview = ENG.weekendWins ? ENG.weekendWins({ tour, scores, gameConfig }) : { winsByName: {}, results: [] }; const posted = !!(seasonDoc && seasonDoc.weeks && seasonDoc.weeks[tour.satISO]); const previewTotal = Object.values(preview.winsByName).reduce((a, b) => a + b, 0); const [certified, setCertified] = useState(posted); const canPost = previewTotal > 0 && certified; return (

Member stats · post to season

Certify the rounds, then post this weekend's results into every member's season stats.
{preview.results.length === 0 ? (

Once a group finishes a skins / Stableford / Nassau / Split-Sixes game and scores are in, the winners show here. Posting writes this weekend's stats (eagles, birdies, CTP, wins…) into the Season tab for every member.

) : (
{preview.results.map((r, i) => (
{r.dayLabel} · {r.time} {r.game} {r.winners.map((w, j) => ( {w} ))}
))}
)} {/* certification gate */}
{posted && } {posted && Posted to member stats}
); } Object.assign(window, { SeasonScreen, PostWeekendCard });