/* ============================================================ Screens: MyBookings, Profile + Booking / Edit / Cancel modals ============================================================ */ /* -------------------- CLAIM SPOT MODAL -------------------- */ function ClaimModal({ slot, dayLabel, member, onConfirm, onClose }) { const open = 4 - slot.players.length; const [transport, setTransport] = useState("walk"); return (
{slot.time}
{open} of 4 spots open
{slot.players.length > 0 && (
Players in this group
{slot.players.map((p, i) => (
{p.name}
HI {p.hi} · GHIN {p.ghin}
))}
)}
You'll join as
{member.name}
HI {member.handicapIndex ?? "—"} · GHIN {member.ghin || "—"} · CH {member.handicapIndex == null ? "—" : window.YCC.courseHandicapForTee(member.handicapIndex, member.tee || "Blue")} ({member.tee || "Blue"})

Your USGA handicap travels with you into this group's weekend game.

Walking or riding?
{[["walk", "walk", "Walk", "Carry or push"], ["ride", "cart", "Ride", "Take a cart"]].map(([val, icon, label, sub]) => { const on = transport === val; return ( ); })}

Defaults to walking — you can change this anytime before play.

); } /* -------------------- MOVE SPOT MODAL -------------------- */ function MoveSpotModal({ booking, fromTime, dateLabel, openSlots, onPick, onClose }) { const [sel, setSel] = useState(null); const sorted = window.YCC.sortByTime(openSlots); return (
{sorted.length === 0 &&

No other groups with an open spot that day.

}
{sorted.map(s => ( ))}
); } /* -------------------- MANAGER: MOVE ANY PLAYER -------------------- */ function ManagerMoveModal({ player, fromTime, dateLabel, openSlots, onPick, onClose }) { const [sel, setSel] = useState(null); const sorted = window.YCC.sortByTime(openSlots); return (
{player.name}{player.guest && Added by manager}
HI {player.hi} · GHIN {player.ghin}
Move to another group
{sorted.length === 0 &&

No other groups with an open spot that day.

}
{sorted.map(s => ( ))}
); } /* -------------------- MANAGER: ADD OFF-APP MEMBER -------------------- */ function AddPlayerModal({ slot, dayLabel, members, weekSlots, onAdd, onClose }) { const Y = window.YCC; const list = members || []; const fullName = (m) => `${m.first_name || ""} ${m.last_name || ""}`.trim() || m.email; const confirmed = (m) => !!m.handicap_confirmed_at; const signedNames = new Set(); Object.values(weekSlots || {}).forEach(slots => (slots || []).forEach(s => s.players.forEach(p => signedNames.add(p.name)))); const inThisSlot = new Set(slot.players.map(p => p.name.toLowerCase())); const signedUpConfirmed = list.filter(m => signedNames.has(fullName(m)) && confirmed(m)); const signedIds = new Set(signedUpConfirmed.map(m => m.id)); const others = list.filter(m => !signedIds.has(m.id)); const [pick, setPick] = useState(list.length ? "" : "__guest__"); const [form, setForm] = useState({ name: "", hi: "", ghin: "", tee: "White", transport: "walk" }); const open = 4 - slot.players.length; const dot = { Blue: "#2f5db0", White: "#c7c8b3", Green: "#3a7a4f", Gold: "#b08d4f", Red: "#9e3b2f" }; function selectPick(v) { setPick(v); if (v === "__guest__" || v === "") { setForm({ name: "", hi: "", ghin: "", tee: "White", transport: "walk" }); return; } const m = list.find(x => x.id === v); if (m) setForm({ name: fullName(m), hi: m.handicap_index == null ? "" : String(m.handicap_index), ghin: m.ghin || "", tee: m.tee || "White", transport: "walk" }); } const isGuest = pick === "__guest__"; const isMember = !!pick && pick !== "__guest__"; const selMember = isMember ? list.find(m => m.id === pick) : null; const dupe = (isMember || isGuest) && inThisSlot.has((form.name || "").trim().toLowerCase()); const canAdd = open > 0 && !!form.name.trim() && (isGuest || isMember) && !dupe; function submit() { onAdd({ name: form.name.trim(), hi: form.hi, ghin: form.ghin, tee: form.tee, transport: form.transport, guest: isGuest, memberId: isMember ? pick : undefined }); } return (
{dupe &&
Already in this group.
}
{isGuest && ( <>
A guest who isn't on the DIV Tour app. You're adding them on their behalf — their handicap drives the weekend game.
setForm({ ...form, name: e.target.value })} placeholder="e.g. Tom Bradley" />
)} {isMember && selMember && (
s[0]).join("").slice(0, 2).toUpperCase()} size={34} />
{form.name} {confirmed(selMember) ? Confirmed : Not confirmed}
GHIN {form.ghin || "—"}
)} {(isGuest || isMember) && ( <>
setForm({ ...form, hi: e.target.value.replace(/[^0-9.]/g, "").slice(0, 4) })} placeholder="e.g. 14.2" />
{isGuest &&
setForm({ ...form, ghin: e.target.value.replace(/[^0-9]/g, "").slice(0, 8) })} style={{ fontFamily: "ui-monospace, monospace" }} placeholder="8-digit" />
}
{Y.TEES.map(t => { const on = form.tee === t.name; return ( ); })}
{[["walk", "walk", "Walk"], ["ride", "cart", "Ride"]].map(([val, icon, label]) => { const on = form.transport === val; return ( ); })}
)}
); } /* -------------------- REQUEST A CHANGE (email the primary manager) -------------------- */ // Once member self-service closes (Wed 11:59 PM ET) the tee sheet is managed by // the tour manager. Members compose a change request that opens in their email app, // pre-addressed to the primary manager with their current spots filled in. function RequestChangeModal({ member, primary, primaryEmail, bookings, slotOf, tour, phase, toast, onClose }) { const Y = window.YCC; const rows = (bookings || []) .map(b => ({ b, slot: slotOf(b) })) .filter(x => x.slot) .sort((a, b) => a.b.dateISO.localeCompare(b.b.dateISO) || Y.parseTime(a.slot.time) - Y.parseTime(b.slot.time)); const KINDS = [ ["move", "Move to a different tee time"], ["release", "Release / cancel a spot"], ["add", "Add me to a group"], ["group", "Change who's in my group"], ["other", "Something else"], ]; const [kind, setKind] = useState("move"); const [note, setNote] = useState(""); const contacts = (Y.CLUB_CONTACTS || []); const [cc, setCc] = useState([]); const toggleCc = (email) => setCc((prev) => prev.includes(email) ? prev.filter((e) => e !== email) : [...prev, email]); const primaryName = (primary && primary.name) || "the DIV Tour Manager"; const lockedCopy = phase === "locked" ? "The Friday 9:00 AM ET lock has passed, so groups are final." : "Member sign-ups are closed for this week."; function buildBody() { const kindLabel = (KINDS.find(k => k[0] === kind) || [])[1] || "Change request"; const lines = []; lines.push(`Hi ${primaryName.split(" ")[0]},`); lines.push(""); lines.push(`I'd like to request a change to my DIV Tour entry for the weekend of ${tour.weekendLabel}.`); lines.push(""); lines.push(`Request type: ${kindLabel}`); if (rows.length) { lines.push(""); lines.push("My current spot(s):"); rows.forEach(({ b, slot }) => { lines.push(` • ${Y.fmtDate(new Date(b.dateISO + "T00:00:00"))} — ${slot.time}`); }); } else { lines.push(""); lines.push("I don't currently have a spot this weekend."); } lines.push(""); lines.push("Details:"); lines.push(note.trim() || "(add any specifics here)"); lines.push(""); lines.push("Thanks,"); lines.push(member.name || ""); lines.push(`HI ${member.handicapIndex ?? "—"} · GHIN ${member.ghin || "—"}${member.phone ? " · " + member.phone : ""}`); return lines.join("\n"); } function send() { const subject = `DIV Tour change request — ${member.name} (${tour.weekendLabel})`; // Always copy the requester at the email they signed up with, plus any chosen club inboxes. const selfEmail = (member.email || "").trim(); const ccList = [...cc]; if (selfEmail && selfEmail.toLowerCase() !== (primaryEmail || "").toLowerCase() && !ccList.some((e) => e.toLowerCase() === selfEmail.toLowerCase())) ccList.push(selfEmail); const ccPart = ccList.length ? `&cc=${encodeURIComponent(ccList.join(","))}` : ""; const url = `mailto:${encodeURIComponent(primaryEmail)}?subject=${encodeURIComponent(subject)}${ccPart}&body=${encodeURIComponent(buildBody())}`; try { window.location.href = url; } catch (e) {} if (toast) toast("Opening your email app…"); onClose(); } return (
{lockedCopy} Send your request to the primary manager and they'll take care of it.
Goes to
{primaryName}
{primaryEmail}
Primary
{contacts.length > 0 && (
Also notify (optional CC)
{contacts.map((c) => { const on = cc.includes(c.email); return ( ); })}
)} {rows.length > 0 && (
Your current spot{rows.length > 1 ? "s" : ""}
{rows.map(({ b, slot }) => (
{slot.time} {Y.fmtDate(new Date(b.dateISO + "T00:00:00"))}
))}
)}