// lane-management.js - Phase 2: Lane Management System /** * Get all lanes that are currently in use by LIVE matches */ function getUsedLanes() { if (!matches || matches.length === 0) return []; const usedLanes = []; matches.forEach(match => { // Count lanes from ALL matches that have a lane assigned if (match.lane && !match.completed) { usedLanes.push(parseInt(match.lane)); } }); return usedLanes; } /** * Get available lanes for a specific match (excluding its current lane) */ function getAvailableLanes(excludeMatchId) { const maxLanes = (config && config.lanes && config.lanes.maxLanes) || 10; const excludedLanes = (config && config.lanes && config.lanes.excludedLanes) || []; const usedLanes = getUsedLanes(); // If we're updating an existing match, don't count its current lane as used if (excludeMatchId) { const currentMatch = matches.find(m => m.id === excludeMatchId); if (currentMatch && currentMatch.lane) { const currentLane = parseInt(currentMatch.lane); const index = usedLanes.indexOf(currentLane); if (index > -1) { usedLanes.splice(index, 1); } } } // Generate list of available lanes (not used and not excluded) const availableLanes = []; for (let i = 1; i <= maxLanes; i++) { if (!usedLanes.includes(i) && !excludedLanes.includes(i)) { availableLanes.push(i); } } return availableLanes; } /** * Check if a specific lane is currently in use */ function isLaneInUse(laneNumber, excludeMatchId = null) { const usedLanes = getUsedLanes(); const lane = parseInt(laneNumber); // If we're checking for a specific match, don't count its current lane if (excludeMatchId) { const currentMatch = matches.find(m => m.id === excludeMatchId); if (currentMatch && parseInt(currentMatch.lane) === lane) { return false; // Don't consider this match's own lane as "in use" } } return usedLanes.includes(lane); } /** * Validate lane assignments across all matches */ function validateLaneAssignments() { if (!matches || matches.length === 0) return { valid: true, conflicts: [], readyConflicts: [] }; const liveLaneMap = new Map(); const readyLaneMap = new Map(); const liveConflicts = []; const readyConflicts = []; matches.forEach(match => { if (match.lane && getMatchState) { const state = getMatchState(match); const lane = parseInt(match.lane); // Check LIVE matches for conflicts (critical) if (state === 'live') { if (liveLaneMap.has(lane)) { const existingMatches = liveLaneMap.get(lane); liveConflicts.push({ lane: lane, matches: [existingMatches, match.id] }); } else { liveLaneMap.set(lane, match.id); } } // Check READY matches for conflicts (warning) if (state === 'ready') { if (readyLaneMap.has(lane)) { const existingMatches = readyLaneMap.get(lane); // Check if we already have a conflict entry for this lane const existingConflict = readyConflicts.find(c => c.lane === lane); if (existingConflict) { existingConflict.matches.push(match.id); } else { readyConflicts.push({ lane: lane, matches: [existingMatches, match.id] }); } } else { readyLaneMap.set(lane, match.id); } } } }); return { valid: liveConflicts.length === 0, conflicts: liveConflicts, readyConflicts: readyConflicts }; } /** * Generate lane dropdown options HTML with conflict detection */ function generateLaneOptions(currentMatchId, currentLane = null) { const maxLanes = (config && config.lanes && config.lanes.maxLanes) || 10; const excludedLanes = (config && config.lanes && config.lanes.excludedLanes) || []; let options = ''; for (let i = 1; i <= maxLanes; i++) { const isCurrentLane = currentLane && parseInt(currentLane) === i; const isInUse = isLaneInUse(i, currentMatchId); const isExcluded = excludedLanes.includes(i); // Always allow current lane to stay selected if (isCurrentLane || (!isInUse && !isExcluded)) { const selected = isCurrentLane ? 'selected' : ''; options += ``; } else { // Show unavailable lanes as disabled with reason let reason = ''; if (isInUse) reason = ' (in use)'; else if (isExcluded) reason = ' (excluded)'; options += ``; } } return options; } /** * Refresh the lane dropdowns in Match Controls, so a lane in use is not offered twice. * @returns {void} */ function refreshAllLaneDropdowns() { if (!matches || matches.length === 0) return; matches.forEach(match => { // The lane dropdowns live in Match Controls (the bracket's cards have none) const commandCenterElement = document.getElementById(`cc-match-card-${match.id}`); if (commandCenterElement) { const dropdown = commandCenterElement.querySelector('select[onchange*="updateMatchLane"]'); if (dropdown) { const currentValue = dropdown.value; dropdown.innerHTML = generateLaneOptions(match.id, match.lane); // Maintain selection if still valid if (dropdown.querySelector(`option[value="${currentValue}"]`)) { dropdown.value = currentValue; } } } }); } /** * Enhanced toggle active with lane validation */ function toggleActiveWithLaneValidation(matchId) { const match = matches.find(m => m.id === matchId); if (!match) { console.error(`Match ${matchId} not found`); return false; } const currentState = getMatchState ? getMatchState(match) : 'unknown'; // Enforce lane requirement BEFORE starting — checking afterwards would leave // the match live (and a START_MATCH transaction minted) despite the block if (currentState === 'ready' && config.lanes?.requireLaneForStart && !match.lane) { alert('A lane must be assigned before starting this match (required by lane configuration).'); return false; } // Use existing validation first if (typeof toggleActiveWithValidation === 'function') { const stateChangeSuccessful = toggleActiveWithValidation(matchId); if (!stateChangeSuccessful) { return false; } } const newState = getMatchState ? getMatchState(match) : 'unknown'; // If transitioning to LIVE, check for lane conflicts if (newState === 'live' && match.lane && isLaneInUse(match.lane, matchId)) { alert(`Warning: Lane ${match.lane} is already in use by another match`); } // Refresh dropdowns when match state changes setTimeout(() => { refreshAllLaneDropdowns(); refreshAllRefereeDropdowns(); }, 100); return true; } /** * Show current lane usage summary */ function showLaneUsage() { const usedLanes = getUsedLanes(); const maxLanes = (config && config.lanes && config.lanes.maxLanes) || 10; const excludedLanes = (config && config.lanes && config.lanes.excludedLanes) || []; const availableLanes = getAvailableLanes(); const validation = validateLaneAssignments(); let message = `Lane Usage Summary:\n`; message += `Total lanes: 1-${maxLanes}\n`; message += `Excluded lanes: ${excludedLanes.length > 0 ? excludedLanes.join(', ') : 'None'}\n`; message += `Currently in use: ${usedLanes.length > 0 ? usedLanes.join(', ') : 'None'}\n`; message += `Available for assignment: ${availableLanes.length > 0 ? availableLanes.join(', ') : 'None'}\n\n`; if (!validation.valid) { message += `⚠️ CONFLICTS DETECTED:\n`; validation.conflicts.forEach(conflict => { message += `Lane ${conflict.lane}: Used by ${conflict.matches.join(', ')}\n`; }); } else { message += `✅ No lane conflicts detected`; } alert(message); } // Debug function for lane management function debugLaneManagement() { console.log('=== LANE MANAGEMENT DEBUG ==='); console.log('Max lanes:', config.lanes?.maxLanes || 10); console.log('Require lane for start:', config.lanes?.requireLaneForStart || false); console.log('Used lanes:', getUsedLanes()); console.log('Lane validation:', validateLaneAssignments()); // Show lane assignments for all matches matches.forEach(match => { const state = getMatchState ? getMatchState(match) : 'unknown'; console.log(`${match.id}: Lane ${match.lane || 'none'} | State: ${state}`); }); } /** * Get all referees currently assigned to matches (excluding specified match) */ function getAssignedReferees(excludeMatchId = null) { if (!matches || matches.length === 0) return []; const assignedReferees = []; matches.forEach(match => { // Skip the match we're updating if (excludeMatchId && match.id === excludeMatchId) return; // Only count referees assigned to matches that exist and have a referee if (match.referee && !match.completed) { assignedReferees.push(parseInt(match.referee)); } }); return assignedReferees; } /** * Get all players currently in LIVE matches (excluding specified match) */ function getPlayersInLiveMatches(excludeMatchId = null) { if (!matches || matches.length === 0) return []; const playersInLiveMatches = []; matches.forEach(match => { // Skip the match we're updating if (excludeMatchId && match.id === excludeMatchId) return; // Only check LIVE matches if (getMatchState && getMatchState(match) === 'live') { if (match.player1 && match.player1.id && !match.player1.isBye && match.player1.name !== 'TBD') { playersInLiveMatches.push(parseInt(match.player1.id)); } if (match.player2 && match.player2.id && !match.player2.isBye && match.player2.name !== 'TBD') { playersInLiveMatches.push(parseInt(match.player2.id)); } } }); return playersInLiveMatches; } /** * Generate referee dropdown options with conflict detection (replaces existing function) */ function generateRefereeOptionsWithConflicts(currentMatchId, currentRefereeId = null) { let options = ''; if (typeof players !== 'undefined' && Array.isArray(players)) { // Get paid players and sort alphabetically const paidPlayers = players.filter(player => player.paid); const sortedPlayers = paidPlayers.sort((a, b) => { const nameA = a.name.toLowerCase(); const nameB = b.name.toLowerCase(); return nameA.localeCompare(nameB); }); // Get conflict information const assignedReferees = getAssignedReferees(currentMatchId); const playersInLiveMatches = getPlayersInLiveMatches(currentMatchId); sortedPlayers.forEach(player => { const playerId = parseInt(player.id); const isCurrentReferee = currentRefereeId && playerId === parseInt(currentRefereeId); const isAssignedElsewhere = assignedReferees.includes(playerId); const isInLiveMatch = playersInLiveMatches.includes(playerId); // Always allow current referee to stay selected if (isCurrentReferee || (!isAssignedElsewhere && !isInLiveMatch)) { const selected = isCurrentReferee ? 'selected' : ''; const playerName = player.name.length > 10 ? player.name.substring(0, 10) + '...' : player.name; options += ``; } else { // Show unavailable players as disabled with reason const playerName = player.name.length > 10 ? player.name.substring(0, 10) + '...' : player.name; let reason = ''; if (isAssignedElsewhere) reason = ' (assigned)'; else if (isInLiveMatch) reason = ' (playing)'; options += ``; } }); } return options; } /** * Check if a player is available as referee for a specific match */ function isPlayerAvailableAsReferee(playerId, excludeMatchId = null) { const assignedReferees = getAssignedReferees(excludeMatchId); const playersInLiveMatches = getPlayersInLiveMatches(excludeMatchId); const playerIdInt = parseInt(playerId); return !assignedReferees.includes(playerIdInt) && !playersInLiveMatches.includes(playerIdInt); } /** * Refresh the referee dropdowns in Match Controls, so a referee already assigned is not offered twice. * @returns {void} */ function refreshAllRefereeDropdowns() { if (!matches || matches.length === 0) return; matches.forEach(match => { // The referee dropdowns live in Match Controls (the bracket's cards have none) const commandCenterElement = document.getElementById(`cc-match-card-${match.id}`); if (!commandCenterElement) return; const dropdown = commandCenterElement.querySelector('select[onchange*="updateMatchReferee"]'); if (!dropdown) return; const currentValue = dropdown.value; dropdown.innerHTML = generateRefereeOptionsWithConflicts(match.id, match.referee); // Maintain selection if still valid if (dropdown.querySelector(`option[value="${currentValue}"]`)) { dropdown.value = currentValue; } }); } // Make functions globally available if (typeof window !== 'undefined') { window.getAvailableLanes = getAvailableLanes; window.isLaneInUse = isLaneInUse; window.toggleActiveWithLaneValidation = toggleActiveWithLaneValidation; window.showLaneUsage = showLaneUsage; window.debugLaneManagement = debugLaneManagement; window.refreshAllLaneDropdowns = refreshAllLaneDropdowns; window.refreshAllRefereeDropdowns = refreshAllRefereeDropdowns; window.generateRefereeOptionsWithConflicts = generateRefereeOptionsWithConflicts; }