// 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;
}