/* ============================================================ Weekend Games — a scorecard for EVERY tee-time group, using the manager's selected game for that day. The Group Tees at the top drive every card. Scores are shared with the group (synced + live), blank until game day, dots = strokes vs the group's low handicap (the low player has none). ============================================================ */ const STAKE_LABEL = { skins: "Skin value", stableford: "Ante (each)", nassau: "Bet / match", "split-sixes": "Point value" }; function GamesScreen({ ctx }) { const Y = window.YCC; const G = window.YCC_GAMES; const ENG = window.YCC_SCORING; const { tour, member, gameConfig, setGameConfig, role, scores, setScore, sideResults, setSideResult } = ctx; const isManager = role === "manager"; const { skinValue, carryover, scoring, groupTee } = gameConfig; const setCfg = (patch) => setGameConfig({ ...gameConfig, ...patch }); const [rulesOpen, setRulesOpen] = useState(null); const todayISO = Y.isoDay(Y.TODAY); const dayMeta = [ { iso: tour.satISO, label: "Saturday" }, { iso: tour.sunISO, label: "Sunday" }, ]; const groups = []; dayMeta.forEach((d) => { Y.sortByTime(tour.slots[d.iso] || []).forEach((s) => { if (s.players && s.players.length > 0) groups.push({ dayISO: d.iso, dayLabel: d.label, slotId: s.id, time: s.time, slot: s }); }); }); const idsInPlay = Array.from(new Set(groups.map((g) => ENG.whichGame(gameConfig, g.dayISO, tour.satISO)))); const anySkins = idsInPlay.includes("skins"); const stakeLabel = idsInPlay.length === 1 ? (STAKE_LABEL[idsInPlay[0]] || "Stake") : "Stake"; const satGame = G.gameById(ENG.whichGame(gameConfig, tour.satISO, tour.satISO)); const sunGame = G.gameById(ENG.whichGame(gameConfig, tour.sunISO, tour.satISO)); // daily field low handicap (lowest course handicap among everyone signed up that day) const hiN = (h) => { const n = typeof h === "number" ? h : parseFloat(h); return isFinite(n) ? n : null; }; const dayLow = {}; dayMeta.forEach((d) => { const chs = []; (tour.slots[d.iso] || []).forEach((s) => s.players.forEach((p) => { const n = hiN(p.hi); if (n != null) chs.push(Y.courseHandicapForTee(n, groupTee)); })); dayLow[d.iso] = chs.length ? Math.min(...chs) : null; }); const lowMode = gameConfig.lowMode || "day"; // ===== EMPTY STATE — no groups have players yet ===== if (groups.length === 0) { const sameGame = satGame.id === sunGame.id; return (
Weekend Games

Weekend Games

{sameGame ? <>This weekend the DIV Tour is playing {satGame.name}. : <>Saturday is {satGame.name}, Sunday is {sunGame.name}.} A scorecard opens for each tee-time group, on game day. Tap any game below to read its rules.

{(sameGame ? [["This weekend", satGame]] : [["Saturday", satGame], ["Sunday", sunGame]]).map(([label, gm]) => (
{label}'s game
{gm.name}

{gm.blurb}

))}
All game formats
{rulesOpen && setRulesOpen(null)} />}
); } return (
Weekend Games

Group scorecards

{satGame.id === sunGame.id ? <>Playing {satGame.name} both days. : <>Saturday: {satGame.name} · Sunday: {sunGame.name}.} Each tee-time group has its own card below — scores are shared live with everyone in the group on game day.

{/* global controls (manager) — Group Tees drive every card */} {/* Game controls (skin value, scoring, group tees, carryover) now live in Manager → Tour Setup → Weekend Games. They still drive every card here. */}
{groups.map((g) => ( setRulesOpen(gm)} /> ))}
{rulesOpen && setRulesOpen(null)} />}
); } /* -------- one tee-time group's standings + scorecard -------- */ function GroupScorecard({ group, gameConfig, scoring, carryover, stake, groupTee, isManager, member, scores, setScore, sideResults, setSideResult, todayISO, lowMode, dayLowCH, confirmedNames, onRules }) { const Y = window.YCC; const G = window.YCC_GAMES; const ENG = window.YCC_SCORING; const resolvedId = gameConfig.splitDays ? (group.dayLabel === "Saturday" ? (gameConfig.gameSat || "skins") : (gameConfig.gameSun || "skins")) : (gameConfig.gameType || "skins"); const game = G.gameById(resolvedId); const players = group.slot.players.map((p) => ({ name: p.name, initials: p.initials, hi: p.hi, ghin: p.ghin, transport: p.transport || "walk", self: p.name === member.name, guest: !!p.guest, })); const hiNum = (h) => { const n = typeof h === "number" ? h : parseFloat(h); return isFinite(n) ? n : null; }; const cHcp = players.map((p) => { const n = hiNum(p.hi); return n == null ? null : Y.courseHandicapForTee(n, groupTee); }); const cHcpCalc = cHcp.map((c) => (c == null ? 0 : c)); const numericCH = cHcp.filter((c) => c != null); const groupLow = numericCH.length ? Math.min(...numericCH) : 0; const lowCH = (lowMode === "day" && dayLowCH != null) ? dayLowCH : groupLow; const lowIdx = cHcp.findIndex((c) => c != null && c === lowCH); const groupKey = group.dayISO + "|" + group.slotId; const groupScores = (scores && scores[groupKey]) || {}; const grossOf = (name, hIdx) => { const a = groupScores[name]; const v = a && a[hIdx]; return (typeof v === "number" && isFinite(v)) ? v : null; }; const locked = todayISO < group.dayISO; const inThisGroup = players.some((p) => p.self); const canEdit = !locked && (isManager || inThisGroup); // only players in the group (or a manager) enter scores const siOf = (pIdx, hIdx) => Y.siForTee(groupTee, hIdx); const dotsFor = (pIdx, hIdx) => (cHcp[pIdx] == null ? 0 : Y.strokesVsLow(cHcp[pIdx], lowCH, siOf(pIdx, hIdx))); // Add-on games for THIS day → column highlighting on the scorecard. // Closest-to-the-Pin holes get a gold tint; the Longest-Drive hole a green tint, // running the full column from the Yards row down through the last player. const sgDayKey = group.dayLabel === "Saturday" ? "sat" : "sun"; const sgActive = G.activeSideGames(gameConfig, sgDayKey); const ctpSet = new Set(sgActive.includes("ctp") ? G.ctpHolesForDay(gameConfig, sgDayKey) : []); const ldNum = sgActive.includes("longDrive") ? G.longDriveHoleForDay(gameConfig, sgDayKey) : null; const HL = { ctp: { bg: "color-mix(in srgb, var(--accent) 17%, transparent)", bgDark: "color-mix(in srgb, var(--accent) 42%, transparent)", bar: "var(--accent)", label: "Closest to pin" }, ld: { bg: "color-mix(in srgb, var(--ok) 18%, transparent)", bgDark: "color-mix(in srgb, var(--ok) 50%, transparent)", bar: "var(--ok)", label: "Longest drive" }, }; const holeKind = (hIdx) => { const n = hIdx + 1; if (ctpSet.has(n)) return "ctp"; if (ldNum === n) return "ld"; return null; }; const colStyle = (hIdx, dark) => { const k = holeKind(hIdx); if (!k) return null; const c = HL[k]; return dark ? { background: c.bgDark, boxShadow: "inset 0 -3px 0 " + c.bar } : { background: c.bg }; }; const scoresMatrix = players.map((p) => Y.HOLES.map((h, i) => grossOf(p.name, i))); const engine = ENG.computeGame(resolvedId, { players, scores: scoresMatrix, holes: Y.HOLES, cHcps: cHcpCalc, siOf, stake, scoring, carryover }); const ranked = [...engine.standings].sort((a, b) => { if (engine.computable) return b.money - a.money; const av = a.lines[0] && typeof a.lines[0].value === "number" ? a.lines[0].value : 0; const bv = b.lines[0] && typeof b.lines[0].value === "number" ? b.lines[0].value : 0; return av - bv; }); const anyScores = scoresMatrix.some((row) => row.some((v) => v != null)); const showStandings = !locked && anyScores; const playDateLabel = Y.fmtDate(new Date(group.dayISO + "T00:00:00")); const front = Y.HOLES.slice(0, 9), back = Y.HOLES.slice(9, 18); const gTee = Y.teeByName(groupTee); const yOut = front.reduce((s, h, i) => s + Y.yardsForTee(groupTee, i), 0); const yIn = back.reduce((s, h, i) => s + Y.yardsForTee(groupTee, i + 9), 0); const pOut = front.reduce((s, h) => s + h.par, 0); const pIn = back.reduce((s, h) => s + h.par, 0); const SC = { hole: 40, stub: 150, edge: 50 }; const cellTd = (extra) => ({ padding: "7px 0", textAlign: "center", width: SC.hole, ...extra }); const edgeTd = (extra) => ({ padding: "7px 0", textAlign: "center", width: SC.edge, background: "var(--surface-2)", fontWeight: 700, ...extra }); const stubTd = (extra) => ({ padding: "9px 14px", textAlign: "left", width: SC.stub, position: "sticky", left: 0, background: "var(--surface)", zIndex: 1, whiteSpace: "nowrap", ...extra }); const sumRange = (name, from, to) => { let s = 0, any = false; for (let i = from; i < to; i++) { const v = grossOf(name, i); if (v != null) { s += v; any = true; } } return any ? s : ""; }; function Dots({ n }) { if (!n) return null; return ( {n <= 4 ? Array.from({ length: n }).map((_, i) => ) : {n}} ); } function ScoreCell({ name, pIdx, h, hIdx }) { const raw = grossOf(name, hIdx); const won = engine.winnerByHole && engine.winnerByHole[hIdx] === pIdx; const toPar = raw == null ? 0 : raw - h.par; return ( e.target.select()} onChange={(e) => { const d = e.target.value.replace(/[^0-9]/g, "").slice(0, 2); if (d === "") { setScore(group.dayISO, group.slotId, name, hIdx, null); return; } setScore(group.dayISO, group.slotId, name, hIdx, Math.max(1, Math.min(15, parseInt(d, 10)))); }} style={{ width: 26, height: 26, textAlign: "center", border: "none", background: "transparent", outline: "none", fontFamily: "var(--font-body)", fontSize: 13.5, fontWeight: won ? 800 : raw != null && toPar < 0 ? 700 : 500, color: won ? "#1b1404" : raw == null ? "var(--ink-faint)" : "var(--ink)", padding: 0, cursor: canEdit ? "text" : "default", }} /> ); } function PlayerRow({ p, pIdx }) { const fOut = sumRange(p.name, 0, 9), fIn = sumRange(p.name, 9, 18); const tot = (typeof fOut === "number" ? fOut : 0) + (typeof fIn === "number" ? fIn : 0); const isLow = pIdx === lowIdx; return (
{p.self ? "You" : p.name} {isLow && LOW}
CH {cHcp[pIdx] == null ? "—" : cHcp[pIdx]}
{Y.HOLES.map((h, hIdx) => { const cells = []; if (h.n === 9) cells.push({fOut}); if (h.n === 18) { cells.push({fIn}); cells.push({tot || ""}); } return cells; })} ); } function MetaRow({ label, get, out, inn, tot, dark }) { return ( {label} {Y.HOLES.map((h, hIdx) => { const cells = [{get(h, hIdx)}]; if (h.n === 9) cells.push({out}); if (h.n === 18) { cells.push({inn}); cells.push({tot}); } return cells; })} ); } return (
{/* group heading */}
{group.time} {group.dayLabel} {game.name} {group.slot.players.some((p) => p.name === member.name) && Your group} {!locked && }
{!locked ? ( Live · {canEdit ? "enter scores" : "view only"} ) : ( {engine.summary} )}
{/* lock banner */} {locked && (
Opens on game day — {playDateLabel}. Any player in this group can enter scores once play begins; everyone sees them live.
)} {/* standings */} {showStandings && (
{ranked.map((st, rank) => { const p = players[st.idx]; return (
{p.self ? "You" : p.name}
CH {cHcp[st.idx] == null ? "—" : cHcp[st.idx]}
{rank === 0 && }
{st.lines.map((ln, i) => (
{ln.value}
{ln.label}
))} {engine.computable &&
${st.money}
won
}
); })}
)} {/* scorecard */}
YCC
Yardley Country Club
{group.dayLabel.toUpperCase()} · {group.time} · {game.name.toUpperCase()} · {groupTee.toUpperCase()} TEES
{[[gTee.total.toLocaleString(), "Yards"], [gTee.rating.toFixed(1), "Rating"], [String(gTee.slope), "Slope"], ["72", "Par"]].map(([v, l]) => (
{v}
{l}
))}
h.n} out="Out" inn="In" tot="Tot" /> Y.yardsForTee(groupTee, i)} out={yOut} inn={yIn} tot={gTee.total} /> h.par} out={pOut} inn={pIn} tot={pOut + pIn} /> Y.siForTee(groupTee, i)} out="" inn="" tot="" /> {players.map((p, pIdx) => )} {engine.perHole && !locked && ( {Y.HOLES.map((h, hIdx) => { const c = engine.perHole.cells[hIdx]; const cells = []; if (h.n === 9) cells.push(); if (h.n === 18) { cells.push(); cells.push(); } return cells; })} )}
{engine.perHole.label}{typeof c === "string" && c.startsWith("$") ? {c} : {c}}
Strokes vs {lowMode === "day" ? "the day's low" : "the group's low"} {engine.winnerByHole && Skin won} Birdie or better {ctpSet.size > 0 && Closest to the pin {ctpSet.size < 4 ? "(" + [...ctpSet].sort((a, b) => a - b).map((n) => "#" + n).join(" ") + ")" : ""}} {ldNum && Longest drive (#{ldNum})} {canEdit && Tap any score to edit} {!locked && !canEdit && Read-only — only players in this group enter scores} {!engine.computable && !locked && Winner settled by your group}
{/* Daily add-on games — Closest to the Pin / Longest Drive winners */} {(() => { const dayKey = group.dayLabel === "Saturday" ? "sat" : "sun"; const activeIds = G.activeSideGames(gameConfig, dayKey); if (!activeIds.length) return null; const sideRes = (sideResults && sideResults[groupKey]) || {}; const ctpOn = activeIds.includes("ctp"); const ldOn = activeIds.includes("longDrive"); const ctpHoleNums = G.ctpHolesForDay(gameConfig, dayKey); const par3s = Y.HOLES.filter((h) => h.par === 3 && ctpHoleNums.includes(h.n)); const ldHole = Y.HOLES.find((h) => h.n === G.longDriveHoleForDay(gameConfig, dayKey)) || Y.HOLES[0]; const Chip = ({ name, initials, self, won, onClick }) => ( ); const pins = {}; if (ctpOn) par3s.forEach((h) => { const w = (sideRes.ctp || {})[h.n - 1]; if (w) pins[w] = (pins[w] || 0) + 1; }); const pinLeaders = Object.entries(pins).sort((a, b) => b[1] - a[1]); const Row = ({ hLabel, sub, winner, onPick }) => (
{hLabel}
{sub}
{players.map((p) => ( onPick(winner === p.name ? null : p.name)} /> ))} {winner == null && {canEdit ? "Tap the winner" : "Not marked yet"}}
); return (
Add-on games · {group.dayLabel} {!canEdit && !locked && Players in this group mark winners} {canEdit && Tap a player to mark a winner}
{ctpOn && par3s.length > 0 && (
Closest to the Pin
Nearest tee shot on each par 3 wins the hole.
{pinLeaders.length > 0 && (
{pinLeaders.map(([n, c]) => ( {n === member.name ? "You" : n.split(" ")[0]} · {c} pin{c > 1 ? "s" : ""} ))}
)}
{par3s.map((h) => { const hIdx = h.n - 1; return setSideResult(group.dayISO, group.slotId, "ctp", hIdx, name)} />; })}
)} {ldOn && (
Longest Drive
Longest drive in the fairway on the designated hole.
setSideResult(group.dayISO, group.slotId, "longDrive", null, name)} />
)}
); })()}
); } function TeeSelect({ value, onChange }) { const dot = { Blue: "#2f5db0", White: "#c7c8b3", Green: "#3a7a4f", Gold: "#b08d4f", Red: "#9e3b2f" }; return ( ); } function Seg({ label, value, onChange, options }) { return (
{label}
{options.map(([val, lab]) => ( ))}
); } Object.assign(window, { GamesScreen, GroupScorecard, TeeSelect, Seg });