diff --git a/static/app.js b/static/app.js index 522ea6c..bb954fd 100644 --- a/static/app.js +++ b/static/app.js @@ -1,12 +1,39 @@ let data = {}; +let showAllMatches = false; +let viewingNextSeason = false; +let initialLoad = true; + +function parseDate(str) { + const [d, m, y] = str.split('/').map(Number); + return new Date(2000 + y, m - 1, d); +} + +function getSeasonYear(dateStr) { + const [d, m, y] = dateStr.split('/').map(Number); + return m >= 4 ? 2000 + y : 1999 + y; +} + +function currentSeasonYear() { + const now = new Date(); + const m = now.getMonth() + 1; + return m >= 4 ? now.getFullYear() : now.getFullYear() - 1; +} function fetchData() { fetch('/api/data').then(r => r.json()).then(d => { data = d; + updateSeasonButton(); renderTable(); }); } +function updateSeasonButton() { + const thisSeason = currentSeasonYear(); + const hasNextSeason = (data.dates || []).some(d => getSeasonYear(d) === thisSeason + 1); + const btn = document.getElementById('next-season-btn'); + if (btn) btn.style.display = hasNextSeason ? '' : 'none'; +} + function saveData() { fetch('/api/data', { method: 'POST', @@ -15,17 +42,12 @@ function saveData() { }); } - - -let showAllMatches = false; // false = future matches only; true = all matches - -let initialLoad = true; function renderTable() { const container = document.getElementById('attendance-table'); - // Save scroll position of the container const scrollTop = window.scrollY; container.innerHTML = ''; const table = document.createElement('table'); + // Header row const thead = document.createElement('thead'); const headRow = document.createElement('tr'); @@ -36,50 +58,44 @@ function renderTable() { dateTh.style.maxWidth = '140px'; dateTh.style.textAlign = 'left'; headRow.appendChild(dateTh); - data.players.forEach((name, i) => { + data.players.forEach(name => { const th = document.createElement('th'); th.innerText = name; th.classList.add('name-col'); headRow.appendChild(th); }); - // Guest Name column (per date) const guestNameTh = document.createElement('th'); guestNameTh.innerText = 'Guest Name'; guestNameTh.classList.add('name-col'); headRow.appendChild(guestNameTh); thead.appendChild(headRow); table.appendChild(thead); + // Body rows const tbody = document.createElement('tbody'); if (!data.guestNames) data.guestNames = {}; - // Custom order: last played date (<= today) on top, next date (> today) second, others after + const today = new Date(); - // Parse dates as DD/MM/YY - function parseDate(str) { - const [d, m, y] = str.split('/').map(Number); - // Assume 20xx for years < 100 - return new Date(2000 + y, m - 1, d); + const thisSeason = currentSeasonYear(); + const targetSeason = viewingNextSeason ? thisSeason + 1 : thisSeason; + + let filteredDates = (data.dates || []).filter(d => getSeasonYear(d) === targetSeason); + if (!viewingNextSeason && !showAllMatches) { + filteredDates = filteredDates.filter(date => parseDate(date) > today); } - // Filter dates based on toggle - let filteredDates = []; - if (showAllMatches) { - filteredDates = data.dates; - } else { - filteredDates = data.dates.filter(date => parseDate(date) > today); - } - // Find closest date for scroll (from filtered) + + // Find closest date for auto-scroll let closestIdx = 0; let minDiff = Infinity; filteredDates.forEach((date, rowIdx) => { const diff = Math.abs(parseDate(date) - today); - if (diff < minDiff) { - minDiff = diff; - closestIdx = rowIdx; - } + if (diff < minDiff) { minDiff = diff; closestIdx = rowIdx; } }); + filteredDates.forEach((date, rowIdx) => { const tr = document.createElement('tr'); if (rowIdx === closestIdx) tr.id = 'current-match-row'; + // Date cell const dateTd = document.createElement('td'); dateTd.style.width = '120px'; @@ -95,59 +111,38 @@ function renderTable() { dateTd.innerText = date; } tr.appendChild(dateTd); - // Player attendance + + // Player attendance cells data.players.forEach((player, colIdx) => { const td = document.createElement('td'); - td.className = 'clickable name-col'; const key = `${date}|${colIdx}`; - if (data.attendance[key] === true) { - td.innerText = 'Yes'; - td.classList.add('yes'); - td.classList.remove('no', 'maybe'); - } else if (data.attendance[key] === 'no') { - td.innerText = 'No'; - td.classList.add('no'); - td.classList.remove('yes', 'maybe'); - } else if (data.attendance[key] === 'maybe') { - td.innerText = 'Maybe'; - td.classList.add('maybe'); - td.classList.remove('yes', 'no'); - } else { - td.innerText = ''; - td.classList.remove('yes', 'no', 'maybe'); - } - td.onclick = () => { - if (!data.attendance[key]) { - data.attendance[key] = true; - } else if (data.attendance[key] === true) { - data.attendance[key] = 'no'; - } else if (data.attendance[key] === 'no') { - data.attendance[key] = 'maybe'; - } else if (data.attendance[key] === 'maybe') { - delete data.attendance[key]; - } - // Immediately update the cell UI - if (data.attendance[key] === true) { - td.innerText = 'Yes'; - td.classList.add('yes'); - td.classList.remove('no', 'maybe'); - } else if (data.attendance[key] === 'no') { - td.innerText = 'No'; - td.classList.add('no'); - td.classList.remove('yes', 'maybe'); - } else if (data.attendance[key] === 'maybe') { - td.innerText = 'Maybe'; - td.classList.add('maybe'); - td.classList.remove('yes', 'no'); + + const setCell = val => { + if (val === true) { + td.innerText = 'Yes'; td.className = 'clickable name-col yes'; + } else if (val === 'no') { + td.innerText = 'No'; td.className = 'clickable name-col no'; + } else if (val === 'maybe') { + td.innerText = 'Maybe'; td.className = 'clickable name-col maybe'; } else { - td.innerText = ''; - td.classList.remove('yes', 'no', 'maybe'); + td.innerText = ''; td.className = 'clickable name-col'; } + }; + setCell(data.attendance[key]); + + td.onclick = () => { + const cur = data.attendance[key]; + if (!cur) data.attendance[key] = true; + else if (cur === true) data.attendance[key] = 'no'; + else if (cur === 'no') data.attendance[key] = 'maybe'; + else delete data.attendance[key]; + setCell(data.attendance[key]); saveData(); }; tr.appendChild(td); }); - // Guest Name column (input per date) + + // Guest Name column const guestNameTd = document.createElement('td'); guestNameTd.classList.add('name-col'); const guestNameInput = document.createElement('input'); @@ -157,28 +152,24 @@ function renderTable() { guestNameInput.maxLength = 50; guestNameInput.onchange = e => { let value = e.target.value; - // Allow clearing the guest name (empty string is valid) - if (value === "") { + if (value === '') { delete data.guestNames[date]; saveData(); return; } - // Only allow plain text, disallow HTML/script tags, max 50 chars if (//.test(value) || /["'`\\]/.test(value)) { - alert("Guest name cannot contain code or special characters like <, >, \", \\, or backticks."); + alert('Guest name cannot contain special characters like <, >, ", \\, or backticks.'); guestNameInput.value = data.guestNames[date] || ''; return; } - // Only allow a-z, A-Z, 0-9, spaces, hyphens, periods if (!/^([a-zA-Z0-9 .-]+)$/.test(value)) { alert('Guest name can only contain letters, numbers, spaces, hyphens, and periods.'); guestNameInput.value = data.guestNames[date] || ''; return; } if (value.length > 50) { - alert('Guest name cannot be longer than 50 characters.'); - guestNameInput.value = value.slice(0, 50); value = value.slice(0, 50); + guestNameInput.value = value; } data.guestNames[date] = value; saveData(); @@ -187,16 +178,16 @@ function renderTable() { tr.appendChild(guestNameTd); tbody.appendChild(tr); }); + table.appendChild(tbody); container.appendChild(table); - // Scroll to the most current match row after rendering + setTimeout(() => { if (initialLoad) { const row = document.getElementById('current-match-row'); if (row) row.scrollIntoView({ behavior: 'smooth', block: 'center' }); initialLoad = false; } else { - // Restore previous scroll position window.scrollTo({ top: scrollTop }); } }, 0); @@ -204,6 +195,7 @@ function renderTable() { document.addEventListener('DOMContentLoaded', () => { fetchData(); + const toggleBtn = document.getElementById('toggle-matches-btn'); if (toggleBtn) { toggleBtn.textContent = 'Show All Matches'; @@ -213,4 +205,17 @@ document.addEventListener('DOMContentLoaded', () => { renderTable(); }; } + + const nextSeasonBtn = document.getElementById('next-season-btn'); + if (nextSeasonBtn) { + nextSeasonBtn.onclick = () => { + viewingNextSeason = !viewingNextSeason; + nextSeasonBtn.textContent = viewingNextSeason ? 'Current Season' : 'Next Season'; + // Reset "show all" toggle when switching seasons + showAllMatches = false; + if (toggleBtn) toggleBtn.textContent = 'Show All Matches'; + initialLoad = true; + renderTable(); + }; + } }); diff --git a/templates/admin.html b/templates/admin.html index 5dea50c..9d183c7 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -154,6 +154,14 @@ Reports + +
Download all attendance data (all seasons, all players) as a CSV file.
+ + +