Maple Hills Country Club

Golf Tournament Hub & Skins Tracker

Live Scoring Matrix & Real-Time Skins Ledger

Teams / Hole 123456789 OUT 101112131415161718 IN TOT SKINS

Tournament Leaderboard (Gross Team Stroke)

Skins Summary & Financial Payouts

Total Game Purse: $0.00 | Value Per Skin: $0.00

"; // 2. Turn the content into a downloadable blob file const blob = new Blob([pageContent], { type: "text/html" }); const link = document.createElement("a"); // 3. Name the new file dynamically using the current time const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); link.download = `Scorecard-Update-${timestamp}.html`; // 4. Force browser to export and save it link.href = URL.createObjectURL(blob); link.click(); }); function loadTournamentData() { const savedData = localStorage.getItem('golf_20team_scores_save_gross_final'); if (savedData) { try { const parsed = JSON.parse(savedData); if (parsed.scores && parsed.scores.length === TOTAL_TEAMS) { appState = parsed; } } catch(e) { console.error("Error setting app state framework initialization", e); } } } function saveTournamentData() { localStorage.setItem('golf_20team_scores_save_gross_final', JSON.stringify(appState)); } // 📊 EXPORT FUNCTION: Compiles app metrics into an Excel-readable CSV document function exportCsvFormat() { let csvLines = []; // 1. Spreadsheet Header Generation let headerRow = ["Hole / Team Name", "1", "2", "3", "4", "5", "6", "7", "8", "9", "OUT", "10", "11", "12", "13", "14", "15", "16", "17", "18", "IN", "TOT", "SKINS WON", "NET PAYOUT ($)"]; csvLines.push(headerRow.join(",")); // 2. Par Row Calculation & Injection let parOut = appState.pars.slice(0, 9).reduce((a, b) => a + b, 0); let parIn = appState.pars.slice(9, 18).reduce((a, b) => a + b, 0); let parTot = parOut + parIn; let parRowLine = ["Course Par", ...appState.pars.slice(0, 9), parOut, ...appState.pars.slice(9, 18), parIn, parTot, "-", "-"]; csvLines.push(parRowLine.join(",")); // 3. Yardage Row Calculation & Injection let yOut = appState.yards.slice(0, 9).reduce((a, b) => a + b, 0); let yIn = appState.yards.slice(9, 18).reduce((a, b) => a + b, 0); let yTot = yOut + yIn; let yardRowLine = ["Course Yardage", ...appState.yards.slice(0, 9), yOut, ...appState.yards.slice(9, 18), yIn, yTot, "-", "-"]; csvLines.push(yardRowLine.join(",")); // 4. Retrieve Active Ledger Data Elements const dataCalculation = getCalculatedTeamsData(); const activeLayoutData = dataCalculation.teamsData; activeLayoutData.forEach(t => { let rowLine = [ `"${t.name}"`, // Double quotes ensure text strings map correctly in Excel ...appState.scores[t.index].slice(0, 9), t.out, ...appState.scores[t.index].slice(9, 18), t.in, t.total, t.skinsCount, t.payout.toFixed(2) ]; csvLines.push(rowLine.join(",")); }); // 5. Transform to Blob object and launch background anchor download action const csvContent = "\uFEFF" + csvLines.join("\n"); // Prepend Byte Order Mark (BOM) for accurate Excel UTF-8 display mapping const csvBlob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const csvUrl = URL.createObjectURL(csvBlob); const anchor = document.createElement('a'); anchor.href = csvUrl; anchor.setAttribute("download", `Golf_Tournament_Export_${new Date().toISOString().slice(0, 10)}.csv`); document.body.appendChild(anchor); anchor.click(); anchor.remove(); } function exportTournamentFile() { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(appState, null, 2)); const downloadAnchor = document.createElement('a'); downloadAnchor.setAttribute("href", dataStr); downloadAnchor.setAttribute("download", `Golf_Tournament_Backup_${new Date().toISOString().slice(0, 10)}.json`); document.body.appendChild(downloadAnchor); downloadAnchor.click(); downloadAnchor.remove(); } function importTournamentFile(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { const parsedData = JSON.parse(e.target.result); if (parsedData.scores && parsedData.scores.length === TOTAL_TEAMS) { appState = parsedData; renderTableGridStructure(); calculateTournamentEngine(); alert("Data imported successfully!"); } else { alert("Invalid backup file structure layout."); } } catch(err) { alert("Error reading system file properties."); } }; reader.readAsText(file); } function renderTableGridStructure() { // Build Par Header Block Row const parRow = document.getElementById('parRow'); parRow.innerHTML = 'Par'; for(let i=0; i<18; i++) { if(i === 9) parRow.insertAdjacentHTML('beforeend', '36'); parRow.insertAdjacentHTML('beforeend', ``); } parRow.insertAdjacentHTML('beforeend', '3672-'); // Build Yardage Row directly below Par row elements const yardRow = document.getElementById('yardRow'); yardRow.innerHTML = 'Yards'; for(let i=0; i<18; i++) { if(i === 9) yardRow.insertAdjacentHTML('beforeend', '0'); yardRow.insertAdjacentHTML('beforeend', ``); } yardRow.insertAdjacentHTML('beforeend', '00-'); // Build 20 Team Table Grid Rows const tbody = document.getElementById('scorecardBody'); tbody.innerHTML = ''; appState.teamNames.forEach((name, tIdx) => { let rowHTML = ` `; for(let h=0; h<18; h++) { if(h === 9) rowHTML += `0`; rowHTML += ``; } rowHTML += `000`; tbody.insertAdjacentHTML('beforeend', rowHTML); }); document.getElementById('skin_val').value = appState.skinValue; } function getCalculatedTeamsData() { const teamsData = appState.teamNames.map((name, idx) => ({ index: idx, name: name, out: 0, in: 0, total: 0, skinsCount: 0, skinsHoles: [], payout: 0 })); const holeScoresMatrix = Array.from({ length: 18 }, () => []); const rows = document.querySelectorAll('#scorecardBody .player-row'); rows.forEach(row => { const tIdx = parseInt(row.getAttribute('data-tidx')); const inputs = row.querySelectorAll('.t-score-input'); let outTot = 0; let inTot = 0; inputs.forEach((input, hIdx) => { const val = parseInt(input.value) || 0; appState.scores[tIdx][hIdx] = val; if(val > 0) { holeScoresMatrix[hIdx].push({ tIdx: tIdx, score: val }); if(hIdx < 9) outTot += val; else inTot += val; } applyScoreStylingContext(input, val, appState.pars[hIdx]); input.parentElement.classList.remove('skin-won-cell'); }); teamsData[tIdx].out = outTot; teamsData[tIdx].in = inTot; teamsData[tIdx].total = outTot + inTot; }); // Live Real-Time Skins Logic (Instant updates without waiting for complete roster blocks) let totalSkinsClaimed = 0; for(let h=0; h<18; h++) { const validScores = holeScoresMatrix[h]; if(validScores.length === 0) continue; const minScore = Math.min(...validScores.map(o => o.score)); const tiesList = validScores.filter(o => o.score === minScore); if(tiesList.length === 1) { const winIdx = tiesList[0].tIdx; teamsData[winIdx].skinsCount++; teamsData[winIdx].skinsHoles.push(h + 1); totalSkinsClaimed++; const cellInput = document.querySelector(`.t-score-input[data-tidx="${winIdx}"][data-hole="${h}"]`); if(cellInput) cellInput.parentElement.classList.add('skin-won-cell'); } } const totalPurseValue = TOTAL_TEAMS * appState.skinValue; const skinPerValue = totalSkinsClaimed > 0 ? (totalPurseValue / totalSkinsClaimed) : 0; teamsData.forEach(t => { t.payout = (t.skinsCount * skinPerValue) - appState.skinValue; }); return { teamsData, totalPurseValue, skinPerValue }; } function calculateTournamentEngine() { let parOut = 0; let parIn = 0; appState.pars.forEach((val, idx) => { if(idx < 9) parOut += val; else parIn += val; }); document.getElementById('parOut').textContent = parOut; document.getElementById('parIn').textContent = parIn; document.getElementById('parTotal').textContent = parOut + parIn; let yardOut = 0; let yardIn = 0; appState.yards.forEach((val, idx) => { if(idx < 9) yardOut += val; else yardIn += val; }); document.getElementById('yardOut').textContent = yardOut; document.getElementById('yardIn').textContent = yardIn; document.getElementById('yardTotal').textContent = yardOut + yardIn; const { teamsData, totalPurseValue, skinPerValue } = getCalculatedTeamsData(); teamsData.forEach(t => { const row = document.querySelector(`.player-row[data-tidx="${t.index}"]`); if(row) { row.querySelector('.out-score').textContent = t.out; row.querySelector('.in-score').textContent = t.in; row.querySelector('.total-score').textContent = t.total; row.querySelector('.skins-total').textContent = t.skinsCount; } }); document.getElementById('purseDisplay').textContent = `$${totalPurseValue.toFixed(2)}`; document.getElementById('skinPerValueDisplay').textContent = `$${skinPerValue.toFixed(2)}`; renderDashboardDisplays(teamsData); saveTournamentData(); } function applyScoreStylingContext(input, score, par) { input.classList.remove('birdie', 'eagle', 'bogey', 'double'); if (score <= 0 || par <= 0) return; const diff = score - par; if(diff === -1) input.classList.add('birdie'); else if(diff <= -2) input.classList.add('eagle'); else if(diff === 1) input.classList.add('bogey'); else if(diff >= 2) input.classList.add('double'); } function renderDashboardDisplays(teamsData) { const sortedGross = [...teamsData].sort((a,b) => { if (a.total === 0) return 1; if (b.total === 0) return -1; return a.total - b.total; }); const lb1 = document.getElementById('leaderboardCol1'); const lb2 = document.getElementById('leaderboardCol2'); lb1.innerHTML = ''; lb2.innerHTML = ''; sortedGross.forEach((t, i) => { const medal = i===0 && t.total > 0 ? '🥇 ' : i===1 && t.total > 0 ? '🥈 ' : i===2 && t.total > 0 ? '🥉 ' : ''; const targetCol = i < 10 ? lb1 : lb2; const displayTotal = t.total > 0 ? t.total : '--'; targetCol.insertAdjacentHTML('beforeend', `

${medal}${i+1}. ${t.name} ${displayTotal}

`); }); const sk1 = document.getElementById('skinsContainerCol1'); const sk2 = document.getElementById('skinsContainerCol2'); sk1.innerHTML = ''; sk2.innerHTML = ''; teamsData.forEach((t, i) => { const targetCol = i < 10 ? sk1 : sk2; const badges = t.skinsHoles.map(h => `H${h}`).join(''); const payText = t.payout >= 0 ? `+$${t.payout.toFixed(2)}` : `-$${Math.abs(t.payout).toFixed(2)}`; const payStyle = t.payout > 0 ? 'color:#10b981; font-weight:bold;' : t.payout < 0 ? 'color:#ef4444;' : 'color:#94a3b8;'; targetCol.insertAdjacentHTML('beforeend', `

${t.name} ${payText} (${t.skinsCount})
${badges || 'No Skins'}

`); }); } function attachEventHandlers() { const view = document.getElementById('scorecardBody'); view.addEventListener('input', (e) => { if(e.target.classList.contains('t-score-input')) { calculateTournamentEngine(); } if(e.target.classList.contains('team-name-input')) { const idx = parseInt(e.target.getAttribute('data-tidx')); appState.teamNames[idx] = e.target.value || `Team ${idx+1}`; saveTournamentData(); } }); document.getElementById('parRow').addEventListener('input', (e) => { if(e.target.classList.contains('par-input')) { const idx = parseInt(e.target.getAttribute('data-hole')); appState.pars[idx] = parseInt(e.target.value) || 4; calculateTournamentEngine(); } }); document.getElementById('yardRow').addEventListener('input', (e) => { if(e.target.classList.contains('yard-input')) { const idx = parseInt(e.target.getAttribute('data-hole')); appState.yards[idx] = parseInt(e.target.value) || 0; calculateTournamentEngine(); } }); document.getElementById('saveTeamsBtn').addEventListener('click', () => { const inputs = document.querySelectorAll('.team-name-input'); inputs.forEach(input => { const idx = parseInt(input.getAttribute('data-tidx')); appState.teamNames[idx] = input.value || `Team ${idx+1}`; }); calculateTournamentEngine(); alert("Roster updates saved!"); }); document.getElementById('exportCsvBtn').addEventListener('click', exportCsvFormat); document.getElementById('exportBtn').addEventListener('click', exportTournamentFile); document.getElementById('importBtn').addEventListener('click', () => document.getElementById('fileInput').click()); document.getElementById('fileInput').addEventListener('change', importTournamentFile); document.getElementById('resetAppBtn').addEventListener('click', () => { if(confirm("Reset all 20 team scorecards back to match Par directly?")) { localStorage.removeItem('golf_20team_scores_save_gross_final'); appState.scores = Array.from({length: TOTAL_TEAMS}, () => [...appState.pars]); renderTableGridStructure(); calculateTournamentEngine(); } }); document.getElementById('skin_val').addEventListener('input', (e) => { appState.skinValue = parseFloat(e.target.value) || 0; calculateTournamentEngine(); }); }