<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f8f9fa; color: #333; }
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 25px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
h2 { color: #1a73e8; margin-top: 0; }
.box { margin-bottom: 15px; max-width: 500px; }
/* CONTROLS LAYOUT */
.controls-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 2px solid #f1f3f4; padding-bottom: 15px; }
.right-controls { display: flex; align-items: center; gap: 15px; }
.sort-box { width: auto !important; display: inline-block; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
input[type="text"], input[type="number"], select { width: 100%; padding: 10px; margin: 5px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; font-size: 16px; }
button { background-color: #1a73e8; color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; width: 100%; transition: background-color 0.2s ease; }
button:hover { filter: brightness(90%); }
.hidden { display: none !important; }
/* FINALIZE BUTTON TOP HEADER STYLING */
.finalize-btn { background-color: #e65100; padding: 10px 20px; font-weight: bold; font-size: 15px; letter-spacing: 0.3px; width: auto; margin: 0; }
.finalize-btn:disabled { background-color: #9e9e9e; cursor: not-allowed; }
/* DASHBOARD SPLIT-SCREEN LAYOUT */
.dashboard-grid { display: flex; gap: 30px; align-items: flex-start; }
.left-panel { flex: 7; min-width: 0; }
.right-panel { flex: 5; min-width: 0; display: flex; flex-direction: column; gap: 20px; }
/* Data Tables */
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; transition: background-color 0.2s; }
th { background-color: #f1f3f4; color: #202124; }
tr:nth-child(even) { background-color: #f8f9fa; }
/* Input Adjustments */
.text-center { text-align: center; }
.score-input { width: 80px !important; text-align: center; margin: 0 auto !important; padding: 6px !important; display: block; }
.score-input:disabled { background-color: #e8eaed !important; color: #5f6368; cursor: not-allowed; }
.loading { color: #666; font-style: italic; }
.tie-warning { background-color: #f8d7da !important; border: 2px solid #dc3545 !important; }
/* CHECK-IN STYLES */
.check-box-input { width: 22px; height: 22px; cursor: pointer; accent-color: #1a73e8; }
.checked-in-row td { background-color: #d4edda !important; }
/* STANDINGS CARDS STYLES */
.leaderboard-card { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.leaderboard-card h4 { margin: 0 0 10px 0; color: #1a73e8; border-bottom: 1px solid #e8f0fe; padding-bottom: 5px; text-transform: uppercase; font-size: 14px; letter-spacing: 0.5px; text-align: center; }
.leaderboard-table { font-size: 13px; margin-top: 5px; }
.leaderboard-table th, .leaderboard-table td { padding: 6px 10px; }
.leaderboard-table tr:nth-child(1) { font-weight: bold; background-color: #fff8e1; }
.rank-col { width: 45px; font-weight: bold; color: #5f6368; text-align: center !important; }
/* CUSTOM POP-UP MODAL OVERLAY STYLES */
.modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; }
.modal-overlay.active { opacity: 1; pointer-events: auto; }
.modal-box { background: white; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%; box-shadow: 0 10px 25px rgba(0,0,0,0.2); transform: translateY(-20px); transition: transform 0.3s ease; text-align: center; }
.modal-overlay.active .modal-box { transform: translateY(0); }
.modal-box h3 { margin-top: 0; font-size: 22px; }
.modal-box p { color: #5f6368; font-size: 16px; line-height: 1.5; margin: 15px 0 25px 0; }
.modal-buttons { display: flex; gap: 15px; justify-content: center; }
.modal-btn { padding: 12px 24px; border: none; border-radius: 4px; font-size: 16px; font-weight: bold; cursor: pointer; flex: 1; }
.btn-confirm { background-color: #e65100; color: white; }
.btn-cancel { background-color: #e8eaed; color: #333; }
.btn-success { background-color: #1a73e8; color: white; }
</style>
</head>
<body>
<div class="container">
<div id="loginSection">
<h2>Contest Director Portal</h2>
<p>Please enter your unique Access Token to open your assigned digital score sheet.</p>
<div class="box">
<input type="text" id="tokenInput" placeholder="Enter Access Token Here...">
</div>
<button id="loginBtn" onclick="login()">Access Score Sheet</button>
<p id="loginError" style="color: red;"></p>
</div>
<div id="portalSection" class="hidden">
<div class="controls-row">
<div>
<h2 id="welcomeText">Welcome</h2>
<h3 id="assignmentText" style="color: #5f6368; margin: 0;">Loading Assignment...</h3>
</div>
<div class="right-controls">
<div id="sortContainer">
<label for="sortSelect" style="font-weight: bold; font-size: 14px;">Sort Roster: </label>
<select id="sortSelect" class="sort-box" onchange="applySort()">
<option value="name">Alphabetical by Name</option>
<option value="school">By School, then Name</option>
</select>
</div>
<button id="clearCheckinsBtn" class="hidden" onclick="showClearCheckinsModal()" style="width: auto; padding: 10px 20px; font-weight: bold; background-color: #dc3545; color: white;">Clear Check-ins</button>
<button id="toggleViewBtn" class="hidden" onclick="toggleIdView()" style="width: auto; padding: 10px 20px; font-weight: bold; margin-right: 15px;"></button>
<button id="finalizeBtn" class="finalize-btn hidden" onclick="showFinalizeModal()">🔒 Finalize Results</button>
</div>
</div>
<div id="loadingRoster" class="loading hidden">Loading student roster...</div>
<div class="dashboard-grid">
<div class="left-panel">
<table id="rosterTable" class="hidden">
<thead>
<tr id="tableHeaderRow"></tr>
</thead>
<tbody id="rosterBody"></tbody>
</table>
</div>
<div class="right-panel" id="leaderboardContainer"></div>
</div>
</div>
</div>
<div id="customModal" class="modal-overlay">
<div class="modal-box">
<h3 id="modalTitle" style="color: #e65100;">Confirm Finalization</h3>
<p id="modalMessage">Are you sure you want to finalize and lock the results?</p>
<div class="modal-buttons" id="modalButtonsContainer"></div>
</div>
</div>
<script>
const API_URL = "https://script.google.com/macros/s/AKfycbzKjS3O_8oUHn1LIRD9i0fQ_5LUSJkv5P8BR_jvcsTeENmnlPtwXlcz9M_lQbfEb_sg/exec";
let globalSettings = { config: {} };
let localRosterData = [];
let idViewState = 'scoring';
async function callBackend(action, data = {}) {
try {
const url = API_URL + "?action=" + action + "&data=" + encodeURIComponent(JSON.stringify(data));
const response = await fetch(url, { method: 'GET', redirect: 'follow', headers: { "Content-Type": "text/plain;charset=utf-8" } });
if (!response.ok) throw new Error('Network response was not ok');
return await response.json();
} catch (error) {
console.error("Backend Error:", error);
return { success: false, message: "Network connection error. Try again." };
}
}
window.onload = async function() {
const settings = await callBackend("getGlobalSettings");
if(settings) globalSettings = { ...globalSettings, ...settings };
};
async function login() {
const token = document.getElementById('tokenInput').value.trim();
const errorEl = document.getElementById('loginError');
const loginBtn = document.getElementById('loginBtn');
errorEl.innerText = "";
if(!token) { errorEl.innerText = "Token cannot be blank."; return; }
loginBtn.innerText = "Loading...";
loginBtn.disabled = true;
const response = await callBackend("authenticateDirector", { token: token });
if(response.success) {
document.getElementById('loginSection').classList.add('hidden');
document.getElementById('portalSection').classList.remove('hidden');
document.getElementById('welcomeText').innerText = "Welcome, " + response.directorName;
document.getElementById('assignmentText').innerText = "Event: " + response.eventAssignment;
globalSettings.currentEvent = response.eventAssignment;
globalSettings.config = response.config || { hasTiebreaker: true, hasSizeDivisions: true, hasGradeDivisions: false, hasTeamRankings: true, identifierType: "Name", scoringMethod: "Score" };
if (response.status === "SCORING COMPLETE") {
const finalizeBtn = document.getElementById('finalizeBtn');
finalizeBtn.innerText = "🔒 Finalized";
finalizeBtn.disabled = true;
globalSettings.isLocked = true;
} else {
globalSettings.isLocked = false;
}
loadRoster(response.eventAssignment);
} else {
errorEl.innerText = response.message;
loginBtn.innerText = "Access Score Sheet";
loginBtn.disabled = false;
}
}
async function loadRoster(eventName) {
const loadingEl = document.getElementById('loadingRoster');
const tableEl = document.getElementById('rosterTable');
loadingEl.classList.remove('hidden');
const roster = await callBackend("getEventRoster", { eventName: eventName });
loadingEl.classList.add('hidden');
tableEl.classList.remove('hidden');
localRosterData = roster || [];
// FORCE state based on the config fetched from your sheet
const config = globalSettings.config;
const isPlacing = (String(config.scoringMethod || "").toLowerCase() === "place");
const isContestIdMode = (String(config.identifierType || "").toLowerCase() .includes("id"));
if (isPlacing && isContestIdMode) {
idViewState = 'roster';
// Force to Roster (Check-in) view first
document.getElementById('toggleViewBtn').classList.remove('hidden');
document.getElementById('sortSelect').value = "name";
localRosterData.sort((a, b) => (a.lastName || "").localeCompare(b.lastName || ""));
} else {
idViewState = 'scoring';
// Force to Scoring view
document.getElementById('toggleViewBtn').classList.add('hidden');
document.getElementById('clearCheckinsBtn').classList.add('hidden');
}
renderTableGrid();
}
function renderTableGrid() {
const headerRow = document.getElementById('tableHeaderRow');
const body = document.getElementById('rosterBody');
body.innerHTML = "";
headerRow.innerHTML = "";
const config = globalSettings.config;
const isPlacingEvent = (String(config.scoringMethod || "").toLowerCase() === "place");
const isContestIdMode = (String(config.identifierType || "").toLowerCase() .includes("id"));
const hasTiebreaker = config.hasTiebreaker;
let scoreLabel = isPlacingEvent ? "Place" : "Score";
// 1. UI STATE MANAGEMENT
if (isPlacingEvent && isContestIdMode) {
if (idViewState === 'roster') {
document.getElementById('sortContainer').classList.remove('hidden');
document.getElementById('leaderboardContainer').style.display = 'none';
document.getElementById('finalizeBtn').classList.add('hidden');
document.getElementById('clearCheckinsBtn').classList.remove('hidden');
let btn = document.getElementById('toggleViewBtn');
btn.innerText = " Go to Score Entry";
btn.style.backgroundColor = "#1a73e8";
} else if (idViewState === 'scoring') {
document.getElementById('sortContainer').classList.add('hidden');
document.getElementById('leaderboardContainer').style.display = 'block';
document.getElementById('clearCheckinsBtn').classList.add('hidden');
document.getElementById('finalizeBtn').classList.remove('hidden');
let btn = document.getElementById('toggleViewBtn');
btn.innerText = " Back to ID Roster";
btn.style.backgroundColor = "#5f6368";
localRosterData.sort((a, b) => {
let idA = (a.contestId || a.uniqueId || "").toString();
let idB = (b.contestId || b.uniqueId || "").toString();
return idA.localeCompare(idB);
});
}
} else {
document.getElementById('sortContainer').classList.remove('hidden');
document.getElementById('leaderboardContainer').style.display = 'block';
document.getElementById('finalizeBtn').classList.remove('hidden');
document.getElementById('clearCheckinsBtn').classList.add('hidden');
}
// 2. BUILD HEADERS
let headerHTML = "";
if (isPlacingEvent && isContestIdMode) {
if (idViewState === 'roster') {
headerHTML = `<th class="text-center" style="width: 80px;">Checked In</th><th>Student Name</th><th>School</th><th class="text-center">Contest ID</th>`;
} else {
headerHTML = `<th class="text-center">Contest ID</th><th class='text-center'>${scoreLabel}</th>`;
}
} else {
let identifierHeader = isContestIdMode ? "Contest ID" : "Student Name";
headerHTML = `<th>${identifierHeader}</th><th>School</th><th class='text-center'>${scoreLabel}</th>`;
if (hasTiebreaker && !isPlacingEvent) headerHTML += `<th class='text-center'>Tiebreaker</th>`;
}
headerRow.innerHTML = headerHTML;
let disabledAttribute = globalSettings.isLocked ? "disabled" : "";
// 3. BUILD ROWS
localRosterData.forEach(function(student) {
const row = document.createElement('tr');
let lookupId = "";
let leftPanelHTML = "";
if (isPlacingEvent && isContestIdMode) {
if (idViewState === 'roster') {
lookupId = student.uniqueId;
let displayId = student.contestId || student.uniqueId;
let isChecked = student.checkedIn ? "checked" : "";
if (student.checkedIn) row.classList.add("checked-in-row");
leftPanelHTML = `
<td class="text-center">
<input type="checkbox" class="check-box-input" data-check-id="${lookupId}" onchange="toggleCheckIn('${lookupId}', this.checked)" ${isChecked}>
</td>
<td>${student.lastName}, ${student.firstName}</td>
<td>${student.school}</td>
<td class="text-center" style="font-size: 16px; color: #1a73e8;"><strong>${displayId}</strong></td>
`;
} else {
lookupId = student.uniqueId;
let displayId = student.contestId || student.uniqueId;
leftPanelHTML = `
<td class="text-center"><strong>${displayId}</strong></td>
<td class="text-center">
<input type="number" class="score-input main-score" data-id="${lookupId}" data-type="score" value="${student.score !== undefined ? student.score : ''}" oninput="updateLocalData('${lookupId}', 'score', this.value)" ${disabledAttribute}>
</td>
`;
}
} else {
// Standard Events & Name/Place Events
let identifierCell = "";
if (isContestIdMode) {
identifierCell = `<td>${student.uniqueId}</td>`;
lookupId = student.uniqueId;
} else {
identifierCell = `<td>${student.lastName}, ${student.firstName}</td>`;
lookupId = `${student.lastName}, ${student.firstName}`;
}
let tiebreakerCell = (hasTiebreaker && !isPlacingEvent) ?
`
<td class="text-center">
<input type="number" class="score-input tie-score" data-id="${lookupId}" data-type="tiebreaker" value="${student.tiebreaker}" oninput="updateLocalData('${lookupId}', 'tiebreaker', this.value)" ${disabledAttribute}>
</td>` : "";
leftPanelHTML = `
${identifierCell}
<td>${student.school}</td>
<td class="text-center">
<input type="number" class="score-input main-score" data-id="${lookupId}" data-type="score" value="${student.score !== undefined ? student.score : ''}" oninput="updateLocalData('${lookupId}', 'score', this.value)" ${disabledAttribute}>
</td>
${tiebreakerCell}
`;
}
row.innerHTML = leftPanelHTML;
body.appendChild(row);
});
if (globalSettings.isLocked) document.getElementById('sortSelect').disabled = true;
calculateLiveStandings();
}
function toggleIdView() {
if (idViewState === 'roster') {
idViewState = 'scoring';
renderTableGrid();
} else {
idViewState = 'roster';
applySort();
}
}
async function toggleCheckIn(lookupId, isChecked) {
let match = localRosterData.find(s => (s.lastName + ", " + s.firstName) === lookupId || s.uniqueId === lookupId);
if (match) match.checkedIn = isChecked;
const checkbox = document.querySelector(`input[data-check-id="${lookupId}"]`);
if (checkbox) {
const tr = checkbox.closest('tr');
if (isChecked) tr.classList.add('checked-in-row');
else tr.classList.remove('checked-in-row');
}
await callBackend("saveLiveScore", {
uniqueId: lookupId,
inputType: "checkin",
value: isChecked,
eventName: globalSettings.currentEvent
});
}
function showClearCheckinsModal() {
document.getElementById('modalTitle').innerText = "Clear All Check-ins?";
document.getElementById('modalTitle').style.color = "#dc3545";
document.getElementById('modalMessage').innerText = "Are you sure you want to completely clear all check-ins for this event? This action cannot be undone.";
document.getElementById('modalButtonsContainer').innerHTML = `
<button class="modal-btn btn-cancel" onclick="closeModal()">Cancel</button>
<button class="modal-btn btn-confirm" style="background-color: #dc3545;" onclick="executeClearCheckins()">Yes, Clear All</button>
`;
document.getElementById('customModal').classList.add('active');
}
async function executeClearCheckins() {
closeModal();
const btn = document.getElementById('clearCheckinsBtn');
btn.innerText = "Clearing...";
btn.disabled = true;
const response = await callBackend("clearAllCheckins", { eventName: globalSettings.currentEvent });
if (response.success) {
localRosterData.forEach(student => student.checkedIn = false);
renderTableGrid();
} else {
alert("Error clearing check-ins: " + response.message);
}
btn.innerText = "Clear Check-ins";
btn.disabled = false;
}
function applySort() {
const config = globalSettings.config;
const isPlacingEvent = (String(config.scoringMethod || "").toLowerCase() === "place");
if (isPlacingEvent && idViewState === 'scoring') return;
const sortType = document.getElementById('sortSelect').value;
if (sortType === 'name') {
localRosterData.sort((a, b) => (a.lastName || "").localeCompare(b.lastName || ""));
} else if (sortType === 'school') {
localRosterData.sort((a, b) => {
let schoolCompare = (a.school || "").localeCompare(b.school || "");
if (schoolCompare !== 0) return schoolCompare;
return (a.lastName || "").localeCompare(b.lastName || "");
});
}
renderTableGrid();
}
function updateLocalData(lookupId, type, val) {
let match = localRosterData.find(s => (s.lastName + ", " + s.firstName) === lookupId || s.uniqueId === lookupId);
if (match) match[type] = val !== "" ? Number(val) : "";
calculateLiveStandings();
}
function calculateLiveStandings() {
const container = document.getElementById('leaderboardContainer');
container.innerHTML = "";
const config = globalSettings.config;
const isPlacingEvent = (String(config.scoringMethod || "").toLowerCase() === "place");
const isContestIdMode = (String(config.identifierType || "").toLowerCase() .includes("id"));
const hasTiebreaker = config.hasTiebreaker;
const hasGradeDivisions = config.hasGradeDivisions;
const hasSizeDivisions = config.hasSizeDivisions !== undefined ? config.hasSizeDivisions : true;
const hasTeamRankings = config.hasTeamRankings;
let divGroups = {};
let divCounts = {};
localRosterData.forEach(s => {
if (s.score !== "" && !isNaN(s.score)) {
let d = s.division || "Overall";
if (!divGroups[d]) divGroups[d] = [];
divGroups[d].push(s);
}
});
for (let d in divGroups) {
divGroups[d].sort((a, b) => isPlacingEvent ? a.score - b.score : b.score - a.score);
divCounts[d] = {};
divGroups[d].forEach(s => {
divCounts[d][s.score] = (divCounts[d][s.score] || 0) + 1;
});
}
// Tiebreaker Warning Logic
document.querySelectorAll('.tie-score').forEach(input => {
if (!hasTiebreaker || isPlacingEvent) {
input.classList.remove('tie-warning');
input.placeholder = "";
return;
}
const id = input.getAttribute('data-id');
let student = localRosterData.find(s => (s.lastName + ", " + s.firstName) === id || s.uniqueId === id);
if (student && student.score !== "") {
let d = student.division || "Overall";
let pool = divGroups[d] || [];
let counts = divCounts[d] || {};
let rIdx = pool.findIndex(s => s.score === student.score);
if (counts[student.score] > 1 && rIdx < 6 && (student.tiebreaker === "" || student.tiebreaker === null)) {
input.classList.add('tie-warning');
input.placeholder = "Tie! *";
} else {
input.classList.remove('tie-warning');
input.placeholder = "";
}
} else {
input.classList.remove('tie-warning');
input.placeholder = "";
}
});
let masterSequence = [];
if (hasSizeDivisions) {
if (hasGradeDivisions) {
masterSequence = [
{ type: "individual", key: "Large School", subKeys: ["upper", "(11-12)", "11-12"], label: "Large School Upper Individual Top 10" },
{ type: "individual", key: "Large School", subKeys: ["lower", "(9-10)", "9-10"], label: "Large School Lower Individual Top 10" },
{ type: "team", key: "Large School", label: "Large School Overall Team Standings" },
{ type: "individual", key: "Small School", subKeys: ["upper", "(11-12)", "11-12"], label: "Small School Upper Individual Top 10" },
{ type: "individual", key: "Small School", subKeys: ["lower", "(9-10)", "9-10"], label: "Small School Lower Individual Top 10" },
{ type: "team", key: "Small School", label: "Small School Overall Team Standings" }
];
} else {
masterSequence = [
{ type: "individual", key: "Large School", label: "Large School Individual Top 10" },
{ type: "team", key: "Large School", label: "Large School Overall Team Standings" },
{ type: "individual", key: "Small School", label: "Small School Individual Top 10" },
{ type: "team", key: "Small School", label: "Small School Overall Team Standings" }
];
}
} else {
if (hasGradeDivisions) {
masterSequence = [
{ type: "individual", key: "", subKeys: ["upper", "(11-12)", "11-12"], label: "Upper Individual Top 10" },
{ type: "individual", key: "", subKeys: ["lower", "(9-10)", "9-10"], label: "Lower Individual Top 10" },
{ type: "team", key: "", label: "Overall Team Standings" }
];
} else {
masterSequence = [
{ type: "individual", key: "", label: "Overall Individual Top 10" },
{ type: "team", key: "", label: "Overall Team Standings" }
];
}
}
masterSequence.forEach(section => {
if (section.type === "team" && !hasTeamRankings) return;
if (section.type === "individual") {
let pool = localRosterData.filter(s => {
if (s.score === "" || isNaN(s.score)) return false;
let div = (s.division || "").toLowerCase();
if (section.key !== "" && !div.includes(section.key.toLowerCase())) return false;
if (section.subKeys) {
return section.subKeys.some(sub => div.includes(sub));
}
return true;
});
pool.sort((a, b) => {
if (isPlacingEvent) {
return a.score - b.score;
} else {
if (b.score !== a.score) return b.score - a.score;
return (b.tiebreaker || 0) - (a.tiebreaker || 0);
}
});
const card = document.createElement('div');
card.className = "leaderboard-card";
let labelText = section.label;
if (isPlacingEvent) labelText = labelText.replace("Top 10", "Top 6");
card.innerHTML = `<h4>${labelText}</h4>`;
if (pool.length === 0) {
card.innerHTML += `<p style="color: #999; font-size: 13px; margin: 5px 0 0 0; text-align: center;">Waiting for individual ${isPlacingEvent ? 'places' : 'scores'}...</p>`;
} else {
let tableHTML = `<table class="leaderboard-table">
<thead>
<tr><th class="rank-col">Rank</th><th>Student Name</th><th>School</th><th class="text-center">${isPlacingEvent ? 'Place' : 'Score'}</th></tr>
</thead>
<tbody>`;
let currentRank = 1;
let displayedCount = 0;
let displayLimit = isPlacingEvent ? 6 : 10;
for (let k = 0; k < pool.length; k++) {
if (displayedCount >= displayLimit) break;
let s = pool[k];
if (k > 0 && s.score !== pool[k - 1].score) currentRank = k + 1;
// Standings ALWAYS show the Name and School, even for Contest ID placing events
let displayName = `${s.lastName || "Unknown"}, ${s.firstName || ""}`;
let tiebreakerString = (!isPlacingEvent && s.tiebreaker !== "" && s.tiebreaker !== 0 && s.tiebreaker !== null && !isNaN(s.tiebreaker))
? ` <span style="font-size:11px; color:#888;">(${s.tiebreaker})</span>` : "";
tableHTML += `<tr>
<td class="rank-col">#${currentRank}</td>
<td>${displayName}</td>
<td>${s.school || "—"}</td>
<td class="text-center"><strong>${s.score}</strong>${tiebreakerString}</td>
</tr>`;
displayedCount++;
}
tableHTML += `</tbody></table>`;
card.innerHTML += tableHTML;
}
container.appendChild(card);
} else if (section.type === "team") {
let sizeTierStudents = localRosterData.filter(s => {
if (s.score === "" || isNaN(s.score)) return false;
let div = (s.division || "").toLowerCase();
if (section.key !== "" && !div.includes(section.key.toLowerCase())) return false;
return true;
});
let schoolTeams = {};
sizeTierStudents.forEach(s => {
if (!s.school) return;
if (!schoolTeams[s.school]) schoolTeams[s.school] = [];
schoolTeams[s.school].push(s.score);
});
let teamPool = [];
for (let schoolName in schoolTeams) {
let scores = schoolTeams[schoolName];
scores.sort((a, b) => b - a);
let top3 = scores.slice(0, 3);
let teamTotal = top3.reduce((sum, score) => sum + score, 0);
teamPool.push({ school: schoolName, totalScore: teamTotal, studentCount: scores.length });
}
teamPool.sort((a, b) => b.totalScore - a.totalScore);
const card = document.createElement('div');
card.className = "leaderboard-card";
card.innerHTML = `<h4>${section.label}</h4>`;
if (teamPool.length === 0) {
card.innerHTML += `<p style="color: #999; font-size: 13px; margin: 5px 0 0 0; text-align: center;">Waiting for team calculations...</p>`;
} else {
let tableHTML = `<table class="leaderboard-table">
<thead>
<tr><th class="rank-col">Rank</th><th>School Name</th><th class="text-center">Count</th><th class="text-center">Team Total</th></tr>
</thead>
<tbody>`;
let teamLimit = Math.min(teamPool.length, 5);
for (let t = 0; t < teamLimit; t++) {
let team = teamPool[t];
tableHTML += `<tr>
<td class="rank-col">#${t + 1}</td>
<td>${team.school}</td>
<td class="text-center" style="color: #666;">${team.studentCount}</td>
<td class="text-center"><strong>${team.totalScore}</strong></td>
</tr>`;
}
tableHTML += `</tbody></table>`;
card.innerHTML += tableHTML;
}
container.appendChild(card);
}
});
}
function showFinalizeModal() {
const config = globalSettings.config;
const isPlacingEvent = (String(config.scoringMethod || "").toLowerCase() === "place");
const outstandingTies = document.querySelectorAll('.tie-warning');
if (outstandingTies.length > 0 && config.hasTiebreaker && !isPlacingEvent) {
document.getElementById('modalTitle').innerText = "⚠️ Unresolved Ties Detected";
document.getElementById('modalTitle').style.color = "#dc3545";
document.getElementById('modalMessage').innerText = "You have missing tiebreaker scores highlighted in red inside the top 6 positions. Please resolve them on your entry sheet before finalizing.";
document.getElementById('modalButtonsContainer').innerHTML = `<button class="modal-btn btn-cancel" onclick="closeModal()">Go Back</button>`;
} else {
document.getElementById('modalTitle').innerText = "Lock & Finalize Results?";
document.getElementById('modalTitle').style.color = "#e65100";
document.getElementById('modalMessage').innerText = "Are you sure you want to finalize and lock the results? This will submit your standings to the tab room and disable further score editing.";
document.getElementById('modalButtonsContainer').innerHTML = `
<button class="modal-btn btn-cancel" onclick="closeModal()">Cancel</button>
<button class="modal-btn btn-confirm" style="background-color: #e65100;" onclick="executeFinalize()">Yes, Finalize</button>
`;
}
document.getElementById('customModal').classList.add('active');
}
function closeModal() {
document.getElementById('customModal').classList.remove('active');
}
async function executeFinalize() {
closeModal();
const finalizeBtn = document.getElementById('finalizeBtn');
finalizeBtn.innerText = "Submitting...";
finalizeBtn.disabled = true;
const status = await callBackend("lockAndFinalizeEvent", { eventName: globalSettings.currentEvent });
if (status.success) {
finalizeBtn.innerText = "🔒 Finalized";
document.querySelectorAll('.score-input').forEach(input => input.disabled = true);
document.getElementById('sortSelect').disabled = true;
document.getElementById('modalTitle').innerText = "🎉 Results Submitted!";
document.getElementById('modalTitle').style.color = "#1a73e8";
document.getElementById('modalMessage').innerText = "Success! You have submitted your scores to the Contest Coordinator to publish. You're all set!";
document.getElementById('modalButtonsContainer').innerHTML = `<button class="modal-btn btn-success" onclick="closeModal()">Got It</button>`;
document.getElementById('customModal').classList.add('active');
} else {
finalizeBtn.innerText = "🔒 Finalize Results";
finalizeBtn.disabled = false;
alert("Submission Error: " + status.message);
}
}
document.getElementById('rosterBody').addEventListener('change', async function(e) {
if (e.target.classList.contains('main-score') || e.target.classList.contains('tie-score')) {
const uniqueId = e.target.getAttribute('data-id');
const inputType = e.target.getAttribute('data-type');
const value = e.target.value;
// --- NEW MIN/MAX VALIDATION LOGIC ---
if (inputType === "score" && value !== "") {
const numVal = Number(value);
const min = globalSettings.config.minScore;
const max = globalSettings.config.maxScore;
// Check if min/max exist in the config, and if the value breaks the rules
if ((min !== "" && numVal < min) || (max !== "" && numVal > max)) {
// Trigger the friendly custom modal
document.getElementById('modalTitle').innerText = "⚠️ Out of Range";
document.getElementById('modalTitle').style.color = "#dc3545"; // Red color
document.getElementById('modalMessage').innerText = `The score entered (${numVal}) is outside the allowed range of ${min} to ${max} for this event. Please enter a valid score.`;
document.getElementById('modalButtonsContainer').innerHTML = `<button class="modal-btn btn-cancel" onclick="closeModal()">Got It</button>`;
document.getElementById('customModal').classList.add('active');
// Clear the bad input and flash the box red
e.target.value = "";
e.target.style.backgroundColor = '#f8d7da';
setTimeout(() => { e.target.style.backgroundColor = 'white'; }, 1000);
return; // Stop the function here so the bad score NEVER reaches the spreadsheet
}
}
// --- END VALIDATION LOGIC ---
e.target.style.backgroundColor = '#fff3cd';
const status = await callBackend("saveLiveScore", {
uniqueId: uniqueId,
inputType: inputType,
value: value,
eventName: globalSettings.currentEvent
});
if(status.success) {
e.target.style.backgroundColor = '#d4edda';
setTimeout(() => { e.target.style.backgroundColor = 'white'; }, 1000);
} else {
e.target.style.backgroundColor = '#f8d7da';
alert("Error saving: " + status.message);
}
}
});
// --- ENTER KEY NAVIGATION (Jumps to next row) ---
document.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && e.target.classList.contains('score-input')) {
e.preventDefault();
// Determine if they are in the Main Score column or Tiebreaker column
const selector = e.target.classList.contains('main-score') ? '.main-score' : '.tie-score';
// Get all active inputs in that specific column
const inputs = Array.from(document.querySelectorAll(selector)).filter(input => !input.disabled);
const currentIndex = inputs.indexOf(e.target);
// If there is a next row, move focus down and select the text for easy overwriting
if (currentIndex > -1 && currentIndex < inputs.length - 1) {
inputs[currentIndex + 1].focus();
inputs[currentIndex + 1].select();
}
}
});
// --- ENTER KEY LOGIN ---
document.getElementById('tokenInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
login();
}
});
</script>
</body>
</html>