/* ============================================================
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.
{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 (
{typeof c === "string" && c.startsWith("$") ? {c} : {c}}
];
if (h.n === 9) cells.push(
);
if (h.n === 18) { cells.push(
); cells.push(
); }
return cells;
})}
)}
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}