@lovebowls/leaguejs
Version:
A framework-agnostic JavaScript library for managing leagues, teams, and matches
1 lines • 80.8 kB
Source Map (JSON)
{"version":3,"file":"index.cjs","sources":["../src/utils/validators.js","../src/utils/shared.js","../src/models/Team.js","../src/models/Match.js","../src/models/League.js"],"sourcesContent":["/**\n * Validation utilities for the leagueJS\n */\nexport function validateLeague(data) {\n const errors = [];\n\n if (!data.name) {\n errors.push('League name is required');\n }\n\n if (data.settings) {\n if (typeof data.settings.pointsForWin !== 'undefined') {\n if (typeof data.settings.pointsForWin !== 'number' || data.settings.pointsForWin < 0) {\n errors.push('Invalid points settings');\n }\n }\n if (typeof data.settings.pointsForDraw !== 'undefined') {\n if (typeof data.settings.pointsForDraw !== 'number' || data.settings.pointsForDraw < 0) {\n errors.push('Invalid points settings');\n }\n }\n if (typeof data.settings.pointsForLoss !== 'undefined') {\n if (typeof data.settings.pointsForLoss !== 'number' || data.settings.pointsForLoss < 0) {\n errors.push('Invalid points settings');\n }\n }\n \n // Validate timesTeamsPlayOther\n if (typeof data.settings.timesTeamsPlayOther !== 'undefined') {\n if (typeof data.settings.timesTeamsPlayOther !== 'number' || \n data.settings.timesTeamsPlayOther < 1 || \n data.settings.timesTeamsPlayOther > 10) {\n errors.push('timesTeamsPlayOther must be an integer between 1 and 10');\n }\n }\n }\n\n return {\n isValid: errors.length === 0,\n errors\n };\n}\n\nexport function validateTeam(data) {\n const errors = [];\n\n if (!data._id || !data._id.trim()) {\n errors.push('Team ID is required');\n }\n\n return {\n isValid: errors.length === 0,\n errors\n };\n}\n\nexport function validateMatch(data) {\n const errors = [];\n\n if (!data.homeTeam || !data.homeTeam._id) {\n errors.push('Home team is required');\n }\n\n if (!data.awayTeam || !data.awayTeam._id) {\n errors.push('Away team is required');\n }\n\n if (data.date && !(data.date instanceof Date) && isNaN(new Date(data.date).getTime())) {\n errors.push('Invalid date format');\n }\n\n // Validate result scores if result object exists\n if (data.result) {\n if (typeof data.result.homeScore !== 'number' || data.result.homeScore < 0) {\n errors.push('Home score must be a non-negative number');\n }\n if (typeof data.result.awayScore !== 'number' || data.result.awayScore < 0) {\n errors.push('Away score must be a non-negative number');\n }\n // Optionally, could add validation for rinkScores structure here if needed\n }\n\n return {\n isValid: errors.length === 0,\n errors\n };\n} ","/**\n * Generates a GUID (Globally Unique Identifier)\n * @returns {string} A GUID string in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n */\nexport function generateGUID() {\n // Generate random hex digits\n const hex = () => Math.floor(Math.random() * 16).toString(16);\n \n // Build GUID in format: 8-4-4-4-12\n return [\n // 8 hex digits\n Array(8).fill(0).map(hex).join(''),\n // 4 hex digits\n Array(4).fill(0).map(hex).join(''),\n // 4 hex digits\n Array(4).fill(0).map(hex).join(''),\n // 4 hex digits\n Array(4).fill(0).map(hex).join(''),\n // 12 hex digits\n Array(12).fill(0).map(hex).join('')\n ].join('-');\n}","/**\n * Team model representing a bowls team\n */\nimport { validateTeam } from '../utils/validators.js';\nimport { generateGUID } from '../utils/shared.js';\nexport class Team {\n /**\n * Create a new Team\n * @param {Object} data - Team data\n * @param {string} data._id - Unique identifier for the team\n * @param {string} [data.name] - Name of the team (defaults to _id if not provided)\n * @param {Date} [data.createdAt] - Creation date (defaults to current date)\n * @param {Date} [data.updatedAt] - Last update date (defaults to current date)\n */\n constructor(data) {\n const validationResult = validateTeam(data);\n if (!validationResult.isValid) {\n throw new Error(validationResult.errors[0]);\n }\n\n this._id = data._id || generateGUID();\n this.name = data.name || data._id;\n this.createdAt = data.createdAt || new Date();\n this.updatedAt = data.updatedAt || new Date();\n }\n\n /**\n * Update team details\n * @param {Object} updates - Updated team details\n */\n update(updates) {\n Object.assign(this.details, updates);\n this.updatedAt = new Date();\n }\n\n /**\n * Convert team to JSON\n * @returns {Object} - JSON representation of the team\n */\n toJSON() {\n return {\n _id: this._id,\n name: this.name,\n createdAt: this.createdAt,\n updatedAt: this.updatedAt\n };\n }\n} ","/**\n * Match model representing a bowls match between two teams\n * @typedef {Object} MatchData\n * @property {Object} homeTeam - Home team object with _id property\n * @property {Object} awayTeam - Away team object with _id property\n * @property {string} [_id] - Unique identifier for the match\n * @property {Date|string|null} [date] - Match date (can be null for unscheduled matches)\n * @property {number} [rink] - Assigned rink number for the match\n * @property {Object} [result] - Optional match result data containing scores\n * @property {number} [result.homeScore] - Home team's score\n * @property {number} [result.awayScore] - Away team's score\n * @property {Array<Object>} [result.rinkScores] - Optional individual rink scores\n * @property {Date} [createdAt] - Creation date (defaults to current date)\n * @property {Date} [updatedAt] - Last update date (defaults to current date)\n */\nimport { validateMatch } from '../utils/validators.js';\nimport { generateGUID } from '../utils/shared.js';\n\nexport class Match {\n /**\n * Create a new Match\n * @param {Object} data - Match data\n * @param {Object} data.homeTeam - Home team object with _id property\n * @param {Object} data.awayTeam - Away team object with _id property\n * @param {Date|string|null} [data.date] - Match date (can be null for unscheduled matches)\n * @param {number} [data.rink] - Assigned rink number for the match\n * @param {Object} [data.result] - Optional match result data containing scores\n * @param {number} [data.result.homeScore] - Home team's score\n * @param {number} [data.result.awayScore] - Away team's score\n * @param {Array<Object>} [data.result.rinkScores] - Optional individual rink scores\n * @param {Date} [data.createdAt] - Creation date (defaults to current date)\n * @param {Date} [data.updatedAt] - Last update date (defaults to current date)\n */\n constructor(data) {\n const validationResult = validateMatch(data);\n if (!validationResult.isValid) {\n throw new Error(validationResult.errors[0]);\n }\n\n if (data.homeTeam._id === data.awayTeam._id) {\n throw new Error('Home and away teams must be different');\n }\n \n this._id = data._id || generateGUID();\n this.homeTeam = data.homeTeam;\n this.awayTeam = data.awayTeam;\n this.date = data.date ? new Date(data.date) : null;\n this.rink = data.rink || null;\n this.createdAt = data.createdAt || new Date();\n this.updatedAt = data.updatedAt || new Date();\n\n // Process result if scores are provided\n if (data.result && typeof data.result.homeScore === 'number' && typeof data.result.awayScore === 'number') {\n this.result = {\n homeScore: data.result.homeScore,\n awayScore: data.result.awayScore,\n rinkScores: data.result.rinkScores || null\n };\n } else {\n this.result = null; // Ensure result is null if scores are not provided\n }\n }\n\n /**\n * Get the home team name\n * @returns {string} - The name of the home team\n */\n get homeTeamName() {\n return this.homeTeam.name;\n }\n\n /**\n * Get the away team name\n * @returns {string} - The name of the away team\n */\n get awayTeamName() {\n return this.awayTeam.name;\n }\n\n /**\n * Determines the winner of the match based on scores.\n * @returns {string|null} - The name of the winning team, 'draw', or null if no result is set.\n */\n getWinner() {\n if (!this.result) {\n return null;\n }\n if (this.result.homeScore > this.result.awayScore) {\n return this.homeTeamName;\n }\n if (this.result.awayScore > this.result.homeScore) {\n return this.awayTeamName;\n }\n return 'draw';\n }\n\n /**\n * Checks if the match resulted in a draw.\n * @returns {boolean|null} - True if it's a draw, false otherwise, or null if no result is set.\n */\n isDraw() {\n if (!this.result) {\n return null;\n }\n return this.result.homeScore === this.result.awayScore;\n }\n\n /**\n * Set rink scores for an existing match result\n * @param {Array} rinkScores - Array of rink scores [{homeScore, awayScore}, ...]\n * @returns {boolean} - True if scores were set, false if no result exists\n */\n setRinkScores(rinkScores) {\n if (!this.result) return false;\n \n this.result.rinkScores = rinkScores;\n this.updatedAt = new Date();\n return true;\n }\n\n /**\n * Get rink win/draw counts\n * @returns {Object|null} - Object with rink win counts or null if no rink scores\n */\n getRinkResults() {\n if (!this.result || !this.result.rinkScores) return null;\n \n const rinkResults = {\n homeWins: 0,\n awayWins: 0,\n draws: 0,\n total: this.result.rinkScores.length\n };\n \n this.result.rinkScores.forEach(rink => {\n if (rink.homeScore > rink.awayScore) {\n rinkResults.homeWins++;\n } else if (rink.awayScore > rink.homeScore) {\n rinkResults.awayWins++;\n } else {\n rinkResults.draws++;\n }\n });\n \n return rinkResults;\n }\n\n /**\n * Convert match to JSON\n * @returns {Object} - JSON representation of the match\n */\n toJSON() {\n const jsonResult = this.result ? {\n ...this.result,\n winner: this.getWinner(),\n isDraw: this.isDraw()\n } : null;\n\n return {\n _id: this._id,\n homeTeam: this.homeTeam,\n awayTeam: this.awayTeam,\n date: this.date,\n rink: this.rink,\n result: jsonResult,\n createdAt: this.createdAt,\n updatedAt: this.updatedAt\n };\n }\n} ","/**\n * League model representing a bowls league\n */\nimport { Team } from './Team.js';\nimport { Match } from './Match.js';\nimport { validateLeague } from '../utils/validators.js';\nimport { generateGUID } from '../utils/shared.js';\n\nexport class League {\n /**\n * Create a League instance from JSON data\n * @param {Object|string} jsonData - League data as JSON object or string\n * @returns {League} - New League instance\n */\n static fromJSON(jsonData) {\n try {\n const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;\n return new League(data);\n } catch (error) {\n throw new Error(`Failed to load league: ${error.message}`);\n }\n }\n\n /**\n * Create a new League\n * @param {Object} data - League data\n * @param {string} [data._id] - Unique identifier for the league (auto-generated if not provided)\n * @param {string} data.name - Name of the league\n * @param {Object} [data.settings] - League settings\n * @param {number} [data.settings.pointsForWin=3] - Points awarded for a win\n * @param {number} [data.settings.pointsForDraw=1] - Points awarded for a draw\n * @param {number} [data.settings.pointsForLoss=0] - Points awarded for a loss\n * @param {number} [data.settings.promotionPositions] - Number of teams that get promoted\n * @param {number} [data.settings.relegationPositions] - Number of teams that get relegated\n * @param {number} [data.settings.timesTeamsPlayOther=2] - Number of times teams play against each other (1-10)\n * @param {number} [data.settings.maxRinksPerSession] - Maximum number of rinks available per session\n * @param {Object} [data.settings.rinkPoints] - Settings for rink-based scoring\n * @param {number} [data.settings.rinkPoints.pointsPerRinkWin=2] - Points awarded per winning rink\n * @param {number} [data.settings.rinkPoints.pointsPerRinkDraw=1] - Points awarded per drawn rink\n * @param {number} [data.settings.rinkPoints.defaultRinks=4] - Default number of rinks per match\n * @param {boolean} [data.settings.rinkPoints.enabled=false] - Whether rink points are enabled\n * @param {Array<Team>} [data.teams=[]] - Initial list of teams\n * @param {Array<>} [data.matches=[]] - Initial list of matches\n * @param {Date} [data.createdAt] - Creation date (defaults to current date)\n * @param {Date} [data.updatedAt] - Last update date (defaults to current date)\n */\n constructor(data) {\n const validationResult = validateLeague(data);\n if (!validationResult.isValid) {\n throw new Error(`Invalid league data: ${validationResult.errors.join(', ')}`);\n }\n\n this._id = data._id || generateGUID();\n this.name = data.name;\n this.settings = {\n pointsForWin: data.settings?.pointsForWin || 3,\n pointsForDraw: data.settings?.pointsForDraw || 1,\n pointsForLoss: data.settings?.pointsForLoss || 0,\n promotionPositions: data.settings?.promotionPositions,\n relegationPositions: data.settings?.relegationPositions,\n timesTeamsPlayOther: data.settings?.timesTeamsPlayOther || 2,\n maxRinksPerSession: data.settings?.maxRinksPerSession\n };\n \n if (data.settings?.rinkPoints) {\n this.settings.rinkPoints = {\n pointsPerRinkWin: data.settings.rinkPoints.pointsPerRinkWin || 2,\n pointsPerRinkDraw: data.settings.rinkPoints.pointsPerRinkDraw || 1,\n defaultRinks: data.settings.rinkPoints.defaultRinks || 4,\n enabled: data.settings.rinkPoints.enabled || false\n };\n }\n this.teams = (data.teams || []).map(team => new Team(team));\n this.matches = (data.matches || []).map(match => new Match(match));\n this.createdAt = data.createdAt || new Date();\n this.updatedAt = data.updatedAt || new Date();\n }\n\n /**\n * Add a team to the league\n * @param {Team|Object} team - Team to add (either a Team instance or team data)\n * @returns {boolean} - True if team was added, false if already exists\n * @throws {Error} - If team data is invalid\n */\n addTeam(team) {\n // If team is not already a Team instance, try to create one (which will validate the data)\n const teamInstance = team instanceof Team ? team : new Team(team);\n \n // Check if team with same ID already exists\n if (!this.teams.some(t => t._id === teamInstance._id)) {\n this.teams.push(teamInstance);\n this.updatedAt = new Date();\n return true;\n }\n return false;\n }\n\n /**\n * Remove a team from the league\n * @param {string} teamId - ID of team to remove\n * @returns {boolean} - True if team was removed, false if not found\n */\n removeTeam(teamId) {\n const initialLength = this.teams.length;\n this.teams = this.teams.filter(team => team._id !== teamId);\n \n if (this.teams.length !== initialLength) {\n this.updatedAt = new Date();\n return true;\n }\n return false;\n }\n\n /**\n * Add a match to the league\n * @param {Match|Object} match - Match instance or match data\n * @returns {boolean} - True if match was added, false otherwise\n * @throws {Error} - If match data is invalid\n */\n addMatch(match) {\n // If match is not already a Match instance, try to create one (which will validate the data)\n const matchInstance = match instanceof Match ? match : new Match(match);\n \n // Validate that both teams are in this league using IDs\n const homeTeamExists = this.teams.some(team => team._id === matchInstance.homeTeam._id);\n const awayTeamExists = this.teams.some(team => team._id === matchInstance.awayTeam._id);\n \n if (homeTeamExists && awayTeamExists) {\n // Check if a match with the same _id already exists\n if (this.matches.some(m => m._id === matchInstance._id)) {\n return false; // Match already exists\n }\n \n this.matches.push(matchInstance);\n this.updatedAt = new Date();\n return true;\n }\n return false;\n }\n\n /**\n * Get a team by its ID\n * @param {string} teamId - ID of the team\n * @returns {Team|undefined} - The team if found, undefined otherwise\n */\n getTeam(teamId) {\n return this.teams.find(team => team._id === teamId);\n }\n\n /**\n * Get a match by its ID\n * @param {string} matchId - The ID of the match\n * @returns {Match|undefined} - The match if found, undefined otherwise\n */\n getMatch(matchId) {\n return this.matches.find(match => match._id === matchId);\n }\n\n /**\n * Get all matches for a team\n * @param {string} teamId - ID of the team\n * @returns {Array<Match>} - Array of matches involving the team\n */\n getTeamMatches(teamId) {\n const team = this.getTeam(teamId);\n if (!team) {\n throw new Error(`Team not found with ID: ${teamId}`);\n }\n\n return this.matches.filter(match => \n match.homeTeam._id === teamId || match.awayTeam._id === teamId\n );\n }\n\n /**\n * Get statistics for a team\n * @param {string} teamId - ID of the team\n * @returns {Object} - Team statistics\n */\n getTeamStats(teamId) {\n const team = this.getTeam(teamId);\n if (!team) {\n throw new Error(`Team not found with ID: ${teamId}`);\n }\n\n const matches = this.getTeamMatches(teamId);\n const stats = {\n teamId: teamId,\n teamName: team.name,\n played: 0,\n won: 0,\n drawn: 0,\n lost: 0,\n shotsFor: 0,\n shotsAgainst: 0,\n points: 0,\n inPromotionPosition: false,\n inRelegationPosition: false\n };\n\n // Sort matches by date to determine form/recent matches\n const playedMatches = matches\n .filter(match => match.result && match.date) // Ensure match has result and date\n .sort((a, b) => new Date(a.date) - new Date(b.date)); // Sort oldest to newest\n\n const recentMatchDetails = []; // Array to store details for tooltip\n\n playedMatches.forEach(match => {\n // Ensure result object and scores exist\n if (!match.result || typeof match.result.homeScore !== 'number' || typeof match.result.awayScore !== 'number') {\n console.warn(`Match ${match._id} for team ${team.name} missing valid result or scores.`);\n return; // Skip this match if data is incomplete\n }\n\n const isHome = match.homeTeam._id === teamId;\n const teamScore = isHome ? match.result.homeScore : match.result.awayScore;\n const opponentScore = isHome ? match.result.awayScore : match.result.homeScore;\n // const opponentName = isHome ? match.awayTeamName : match.homeTeamName; // Not needed for description format below\n\n stats.played++;\n stats.shotsFor += teamScore;\n stats.shotsAgainst += opponentScore;\n\n let resultChar = '';\n if (teamScore > opponentScore) {\n stats.won++;\n stats.points += this.settings.pointsForWin;\n resultChar = 'W';\n } else if (teamScore < opponentScore) {\n stats.lost++;\n stats.points += this.settings.pointsForLoss;\n resultChar = 'L';\n } else {\n stats.drawn++;\n stats.points += this.settings.pointsForDraw;\n resultChar = 'D';\n }\n\n // Format date for description - adjust locale/options as needed\n const matchDateStr = new Date(match.date).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n const description = `${matchDateStr}: ${match.homeTeam.name} ${match.result.homeScore} - ${match.result.awayScore} ${match.awayTeam.name}`;\n\n recentMatchDetails.push({\n result: resultChar,\n description: description,\n date: match.date // Keep the original date object if needed elsewhere\n });\n });\n \n // Keep only the details for the last 5 matches\n stats.matches = recentMatchDetails.slice(-5);\n\n // Promotion/relegation positions are set in getLeagueTable after sorting\n return stats;\n }\n\n getLeagueTable() {\n // First, calculate basic stats for all teams\n const teamStats = this.teams.map(team => {\n const stats = this.getTeamStats(team._id);\n return {\n teamId: team._id,\n teamName: team.name,\n ...stats,\n shotDifference: stats.shotsFor - stats.shotsAgainst\n };\n });\n \n // Sort the table\n const sortedTable = teamStats.sort((a, b) => {\n // Sort by points first\n if (b.points !== a.points) {\n return b.points - a.points;\n }\n // Then by shot difference\n if (b.shotDifference !== a.shotDifference) {\n return b.shotDifference - a.shotDifference;\n }\n // Then by shots scored\n if (b.shotsFor !== a.shotsFor) {\n return b.shotsFor - a.shotsFor;\n }\n // Finally by team name\n return a.teamName.localeCompare(b.teamName);\n });\n \n // Now update promotion/relegation positions\n if (this.settings.promotionPositions || this.settings.relegationPositions) {\n sortedTable.forEach((team, index) => {\n const position = index + 1;\n \n if (this.settings.promotionPositions && position <= this.settings.promotionPositions) {\n team.inPromotionPosition = true;\n }\n \n if (this.settings.relegationPositions && position > (sortedTable.length - this.settings.relegationPositions)) {\n team.inRelegationPosition = true;\n }\n });\n }\n \n return {\n leagueData: sortedTable,\n metaData: {\n name: this.name\n }\n };\n }\n\n /*\n * Initialise fixtures for the league using a round-robin algorithm.\n * Each team plays once per match day. Fixtures are scheduled according to the provided scheduling parameters.\n * Teams will play each other `this.settings.timesTeamsPlayOther` times, with home and away fixtures balanced\n * as per the cycles of the round-robin generation (e.g., first cycle A vs B, second cycle B vs A).\n *\n * @param {Date} [startDate] - The start date for the first round of matches. If null, matches will have null dates.\n * @param {Object} [schedulingParams] - Advanced scheduling parameters\n * @param {string} [schedulingParams.schedulingPattern='interval'] - 'interval' or 'dayOfWeek'\n * @param {number} [schedulingParams.intervalNumber=1] - Number of interval units between match days\n * @param {string} [schedulingParams.intervalUnit='weeks'] - 'days' or 'weeks'\n * @param {Array<number>} [schedulingParams.selectedDays] - Array of day numbers (0=Sunday, 1=Monday, etc.) for dayOfWeek pattern\n * @param {number} [schedulingParams.maxMatchesPerDay] - Maximum matches per day (defaults to maxRinksPerSession if not provided)\n * @returns {boolean} - True if fixtures were successfully created, false if there are fewer than 2 teams or if a match fails to be added.\n */\n initialiseFixtures(startDate, schedulingParams = {}) {\n this.matches = []; // Clear any existing matches\n\n if (this.teams.length < 2) {\n return false; // Not enough teams to create fixtures\n }\n\n // Set default scheduling parameters\n const {\n schedulingPattern = 'interval',\n intervalNumber = 1,\n intervalUnit = 'weeks',\n selectedDays = [],\n maxMatchesPerDay = this.settings.maxRinksPerSession || null\n } = schedulingParams;\n\n // Prepare team list for scheduling. Add a dummy \"BYE\" team if the number of teams is odd.\n let scheduleTeams = [...this.teams];\n const BYE_TEAM_MARKER = { _id: `__BYE_TEAM_INTERNAL_${Date.now()}__`, name: 'BYE' }; // Unique marker for the bye team\n if (scheduleTeams.length % 2 !== 0) {\n scheduleTeams.push(BYE_TEAM_MARKER);\n }\n const numEffectiveTeams = scheduleTeams.length; // This will now always be even\n\n // Calculate the number of rounds needed for all unique pairings once\n const roundsPerCycle = numEffectiveTeams - 1;\n\n // Generate all matches first, then assign dates\n const allMatches = [];\n\n // The round-robin algorithm fixes one team and rotates the others.\n // We'll fix the last team in the `scheduleTeams` list.\n const fixedTeam = scheduleTeams[numEffectiveTeams - 1];\n // The list of teams that will be rotated.\n const initialRotatingTeams = scheduleTeams.slice(0, numEffectiveTeams - 1);\n\n // Loop for each full set of fixtures (e.g., once for \"home\" games, once for \"away\" games if timesTeamsPlayOther is 2)\n for (let cycle = 0; cycle < this.settings.timesTeamsPlayOther; cycle++) {\n // For each cycle, re-initialize the rotating teams to ensure the same sequence of pairings.\n let currentRotatingTeams = [...initialRotatingTeams];\n\n for (let roundNum = 0; roundNum < roundsPerCycle; roundNum++) {\n const conceptualPairsForThisRound = []; // Stores {team1, team2} for this specific round\n\n // 1. Match involving the fixed team (e.g., scheduleTeams[n-1])\n // Its opponent is the first team in the current rotated list.\n const opponentForFixed = currentRotatingTeams[0];\n if (fixedTeam._id !== BYE_TEAM_MARKER._id && opponentForFixed._id !== BYE_TEAM_MARKER._id) {\n conceptualPairsForThisRound.push({ team1: fixedTeam, team2: opponentForFixed });\n }\n\n // 2. Other matches from the `currentRotatingTeams` list.\n // The list has `numEffectiveTeams - 1` elements (an odd number).\n // The first element `currentRotatingTeams[0]` is already paired with `fixedTeam`.\n // The remaining elements `currentRotatingTeams[1]` to `currentRotatingTeams[length-1]` are paired up.\n const rotatingListLength = currentRotatingTeams.length;\n for (let j = 1; j <= (rotatingListLength - 1) / 2; j++) {\n const teamA_idx = j;\n const teamB_idx = rotatingListLength - j; // Pairs j-th from start with j-th from end (0-indexed list)\n \n const teamA = currentRotatingTeams[teamA_idx];\n const teamB = currentRotatingTeams[teamB_idx];\n \n if (teamA._id !== BYE_TEAM_MARKER._id && teamB._id !== BYE_TEAM_MARKER._id) {\n conceptualPairsForThisRound.push({ team1: teamA, team2: teamB });\n }\n }\n\n // Store matches for this round\n for (const pair of conceptualPairsForThisRound) {\n let homeTeam, awayTeam;\n // For even cycles (0, 2, ...), team1 is home. For odd cycles (1, 3, ...), team2 is home.\n // This ensures that if pair (X,Y) is generated, cycle 0 schedules X vs Y, cycle 1 schedules Y vs X.\n if (cycle % 2 === 0) {\n homeTeam = pair.team1;\n awayTeam = pair.team2;\n } else {\n homeTeam = pair.team2;\n awayTeam = pair.team1;\n }\n\n allMatches.push({ homeTeam, awayTeam, roundKey: `${cycle}-${roundNum}` });\n }\n\n // Rotate `currentRotatingTeams` for the next round: the last element moves to the front.\n if (currentRotatingTeams.length > 1) { // Rotation only makes sense for 2+ teams\n const lastTeamInRotatingList = currentRotatingTeams.pop();\n currentRotatingTeams.unshift(lastTeamInRotatingList);\n }\n }\n }\n\n // Now assign dates to matches based on scheduling pattern\n if (startDate) {\n this._assignMatchDates(allMatches, startDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays, maxMatchesPerDay);\n } else {\n // No start date provided - create matches without dates but with rinks if configured\n const assignedRinks = this._assignRinksToMatches(allMatches);\n \n for (let i = 0; i < allMatches.length; i++) {\n const matchData = allMatches[i];\n const match = new Match({\n homeTeam: matchData.homeTeam,\n awayTeam: matchData.awayTeam,\n date: null,\n rink: assignedRinks[i]\n });\n\n if (!this.addMatch(match)) {\n return false; // If addMatch fails (e.g., duplicate _id), abort.\n }\n }\n }\n\n return true;\n }\n\n /**\n * Private helper method to assign dates to matches based on scheduling parameters\n */\n _assignMatchDates(allMatches, startDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays, maxMatchesPerDay) {\n // Group matches by round for scheduling\n const matchesByRound = {};\n for (const matchData of allMatches) {\n if (!matchesByRound[matchData.roundKey]) {\n matchesByRound[matchData.roundKey] = [];\n }\n matchesByRound[matchData.roundKey].push(matchData);\n }\n\n // Sort round keys to ensure consistent scheduling order\n const sortedRoundKeys = Object.keys(matchesByRound).sort();\n \n let currentDate = new Date(startDate);\n let isFirstRound = true;\n \n for (const roundKey of sortedRoundKeys) {\n const roundMatches = matchesByRound[roundKey];\n \n if (maxMatchesPerDay && roundMatches.length > maxMatchesPerDay) {\n // Split matches across multiple days if there's a limit\n let matchIndex = 0;\n \n while (matchIndex < roundMatches.length) {\n const matchesForThisDate = roundMatches.slice(matchIndex, matchIndex + maxMatchesPerDay);\n \n // Find the next valid date\n const validDate = this._findNextValidDate(currentDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays, isFirstRound);\n \n // Assign rinks for matches on this date\n const assignedRinks = this._assignRinksToMatches(matchesForThisDate);\n \n // Create matches for this date\n for (let i = 0; i < matchesForThisDate.length; i++) {\n const matchData = matchesForThisDate[i];\n const match = new Match({\n homeTeam: matchData.homeTeam,\n awayTeam: matchData.awayTeam,\n date: new Date(validDate),\n rink: assignedRinks[i]\n });\n\n if (!this.addMatch(match)) {\n return false; // If addMatch fails, abort\n }\n }\n \n matchIndex += maxMatchesPerDay;\n isFirstRound = false;\n \n // Move to next date for remaining matches\n if (matchIndex < roundMatches.length) {\n currentDate = this._getNextSchedulingDate(validDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays);\n }\n }\n \n // Set current date for next round\n if (matchIndex >= roundMatches.length) {\n currentDate = this._getNextSchedulingDate(currentDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays);\n }\n } else {\n // All matches in this round can fit on one day\n const validDate = this._findNextValidDate(currentDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays, isFirstRound);\n \n // Assign rinks for matches on this date\n const assignedRinks = this._assignRinksToMatches(roundMatches);\n \n // Create matches for this date\n for (let i = 0; i < roundMatches.length; i++) {\n const matchData = roundMatches[i];\n const match = new Match({\n homeTeam: matchData.homeTeam,\n awayTeam: matchData.awayTeam,\n date: new Date(validDate),\n rink: assignedRinks[i]\n });\n\n if (!this.addMatch(match)) {\n return false; // If addMatch fails, abort\n }\n }\n \n isFirstRound = false;\n // Move to next date for next round\n currentDate = this._getNextSchedulingDate(validDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays);\n }\n }\n \n return true;\n }\n\n /**\n * Private helper method to assign rinks to matches\n * @param {Array} matches - Array of match data\n * @returns {Array} - Array of assigned rink numbers (or null if no rinks assigned)\n */\n _assignRinksToMatches(matches) {\n if (!this.settings.maxRinksPerSession || this.settings.maxRinksPerSession < 1) {\n // No rink assignment if maxRinksPerSession is not set or invalid\n return matches.map(() => null);\n }\n\n const maxRinks = this.settings.maxRinksPerSession;\n const assignedRinks = [];\n \n // Create an array of available rink numbers\n const availableRinks = Array.from({ length: maxRinks }, (_, i) => i + 1);\n \n for (let i = 0; i < matches.length; i++) {\n if (availableRinks.length === 0) {\n // If we've run out of available rinks, reset the pool\n availableRinks.push(...Array.from({ length: maxRinks }, (_, i) => i + 1));\n }\n \n // Randomly select a rink from available rinks\n const randomIndex = Math.floor(Math.random() * availableRinks.length);\n const selectedRink = availableRinks.splice(randomIndex, 1)[0];\n assignedRinks.push(selectedRink);\n }\n \n return assignedRinks;\n }\n\n /**\n * Find the next valid date based on scheduling pattern\n */\n _findNextValidDate(fromDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays, isFirstRound = false) {\n if (isFirstRound) {\n // For the first round, use the start date as-is if it's valid, otherwise find the next valid date\n if (schedulingPattern === 'dayOfWeek' && selectedDays.length > 0) {\n const currentDay = fromDate.getDay();\n if (selectedDays.includes(currentDay)) {\n return new Date(fromDate);\n }\n // Start date is not a valid day, find the next one\n for (let i = 1; i < 7; i++) {\n const checkDay = (currentDay + i) % 7;\n if (selectedDays.includes(checkDay)) {\n const nextDate = new Date(fromDate);\n nextDate.setDate(nextDate.getDate() + i);\n return nextDate;\n }\n }\n }\n return new Date(fromDate);\n }\n \n if (schedulingPattern === 'dayOfWeek' && selectedDays.length > 0) {\n // Find the next occurrence of one of the selected days\n const currentDay = fromDate.getDay();\n \n // Find the next selected day (starting from tomorrow)\n for (let i = 1; i <= 7; i++) {\n const checkDay = (currentDay + i) % 7;\n if (selectedDays.includes(checkDay)) {\n const nextDate = new Date(fromDate);\n nextDate.setDate(nextDate.getDate() + i);\n return nextDate;\n }\n }\n }\n \n // Use interval pattern (or fallback)\n return new Date(fromDate);\n }\n\n /**\n * Get the next scheduling date after a match date\n */\n _getNextSchedulingDate(currentDate, schedulingPattern, intervalNumber, intervalUnit, selectedDays) {\n const nextDate = new Date(currentDate);\n \n if (schedulingPattern === 'interval') {\n // Add interval\n if (intervalUnit === 'weeks') {\n nextDate.setDate(nextDate.getDate() + (intervalNumber * 7));\n } else {\n nextDate.setDate(nextDate.getDate() + intervalNumber);\n }\n } else if (schedulingPattern === 'dayOfWeek' && selectedDays.length > 0) {\n // Move to next occurrence of selected days\n const currentDay = currentDate.getDay();\n let daysToAdd = 1; // Start from tomorrow\n \n // Find the next selected day\n for (let i = 1; i <= 7; i++) {\n const checkDay = (currentDay + i) % 7;\n if (selectedDays.includes(checkDay)) {\n daysToAdd = i;\n break;\n }\n }\n \n nextDate.setDate(nextDate.getDate() + daysToAdd);\n } else {\n // Fallback to weekly\n nextDate.setDate(nextDate.getDate() + 7);\n }\n \n return nextDate;\n }\n\n /**\n * Returns the filtered list of matches requiring attention, sorted by priority.\n * @returns {Array}\n */\n getMatchesRequiringAttention() {\n if (!this.matches || !Array.isArray(this.matches)) return [];\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const todayTimestamp = today.getTime();\n // Scheduling conflict detection\n const conflictingIds = this.getConflictingMatchIds();\n const getPriority = (match) => {\n const matchDateObj = match.date ? new Date(match.date) : null;\n let matchTimestamp = null;\n if (matchDateObj) {\n matchDateObj.setHours(0, 0, 0, 0);\n matchTimestamp = matchDateObj.getTime();\n }\n if (conflictingIds.has(match._id)) return 1;\n if (match.result && matchTimestamp && matchTimestamp > todayTimestamp) return 2;\n if (!match.result && matchTimestamp && matchTimestamp < todayTimestamp) return 3;\n if (!match.date && !match.result) return 4;\n return 5;\n };\n return this.matches\n .filter(match => {\n const matchDateObj = match.date ? new Date(match.date) : null;\n let matchTimestamp = null;\n if (matchDateObj) {\n matchDateObj.setHours(0, 0, 0, 0);\n matchTimestamp = matchDateObj.getTime();\n }\n if (match.result && matchTimestamp && matchTimestamp > todayTimestamp) return true;\n if (conflictingIds.has(match._id)) return true;\n if (!match.result && matchTimestamp && matchTimestamp < todayTimestamp) return true;\n if (!match.date && !match.result) return true;\n return false;\n })\n .sort((a, b) => {\n const priorityA = getPriority(a);\n const priorityB = getPriority(b);\n if (priorityA !== priorityB) return priorityA - priorityB;\n const homeTeamA = a.homeTeam?.name || '';\n const homeTeamB = b.homeTeam?.name || '';\n const awayTeamA = a.awayTeam?.name || '';\n const awayTeamB = b.awayTeam?.name || '';\n const homeCompare = homeTeamA.localeCompare(homeTeamB);\n if (homeCompare !== 0) return homeCompare;\n return awayTeamA.localeCompare(awayTeamB);\n });\n }\n\n /**\n * Returns a set of match IDs that are in scheduling conflict.\n * @returns {Set<string>}\n */\n getConflictingMatchIds() {\n if (!this.matches) return new Set();\n const today = new Date();\n today.setHours(0, 0, 0, 0);\n const todayTimestamp = today.getTime();\n const futureFixtures = this.matches.filter(match => {\n if (match.result || !match.date) return false;\n const matchDate = new Date(match.date);\n matchDate.setHours(0, 0, 0, 0);\n return matchDate.getTime() >= todayTimestamp;\n });\n const matchesByDate = futureFixtures.reduce((acc, match) => {\n const matchDate = new Date(match.date);\n matchDate.setHours(0, 0, 0, 0);\n const dateKey = matchDate.getTime();\n if (!acc[dateKey]) acc[dateKey] = [];\n acc[dateKey].push(match);\n return acc;\n }, {});\n const conflictingIds = new Set();\n for (const dateKey in matchesByDate) {\n const matchesOnDay = matchesByDate[dateKey];\n if (matchesOnDay.length < 2) continue;\n \n // Check for team conflicts\n const teamCounts = {};\n matchesOnDay.forEach(match => {\n const homeTeamId = match.homeTeam?._id;\n const awayTeamId = match.awayTeam?._id;\n if (homeTeamId) teamCounts[homeTeamId] = (teamCounts[homeTeamId] || 0) + 1;\n if (awayTeamId) teamCounts[awayTeamId] = (teamCounts[awayTeamId] || 0) + 1;\n });\n const conflictingTeams = Object.keys(teamCounts).filter(teamId => teamCounts[teamId] > 1);\n \n // Check for rink conflicts\n const rinkCounts = {};\n const matchesWithRinks = matchesOnDay.filter(match => match.rink != null);\n matchesWithRinks.forEach(match => {\n rinkCounts[match.rink] = (rinkCounts[match.rink] || 0) + 1;\n });\n const conflictingRinks = Object.keys(rinkCounts).filter(rink => rinkCounts[rink] > 1);\n \n // Mark matches with team conflicts\n if (conflictingTeams.length > 0) {\n matchesOnDay.forEach(match => {\n const homeTeamId = match.homeTeam?._id;\n const awayTeamId = match.awayTeam?._id;\n if ((homeTeamId && conflictingTeams.includes(homeTeamId)) || \n (awayTeamId && conflictingTeams.includes(awayTeamId))) {\n conflictingIds.add(match._id);\n }\n });\n }\n \n // Mark matches with rink conflicts\n if (conflictingRinks.length > 0) {\n matchesOnDay.forEach(match => {\n if (match.rink != null && conflictingRinks.includes(match.rink.toString())) {\n conflictingIds.add(match._id);\n }\n });\n }\n }\n return conflictingIds;\n }\n \n /**\n * Convert league to JSON\n * @returns {Object} - JSON representation of the league\n */\n toJSON() {\n return {\n _id: this._id,\n name: this.name,\n teams: this.teams.map(team => team.toJSON ? team.toJSON() : team),\n matches: this.matches.map(match => match.toJSON ? match.toJSON() : match),\n settings: this.settings,\n createdAt: this.createdAt,\n updatedAt: this.updatedAt\n };\n }\n} "],"names":["validateLeague","data","errors","name","push","settings","pointsForWin","pointsForDraw","pointsForLoss","timesTeamsPlayOther","isValid","length","validateTeam","_id","trim","validateMatch","homeTeam","awayTeam","date","Date","isNaN","getTime","result","homeScore","awayScore","generateGUID","hex","Math","floor","random","toString","Array","fill","map","join","Team","constructor","validationResult","Error","createdAt","updatedAt","update","updates","Object","assign","details","toJSON","Match","rink","rinkScores","homeTeamName","awayTeamName","getWinner","isDraw","setRinkScores","getRinkResults","rinkResults","homeWins","awayWins","draws","total","forEach","jsonResult","winner","League","fromJSON","jsonData","JSON","parse","error","message","_data$settings","_data$settings2","_data$settings3","_data$settings4","_data$settings5","_data$settings6","_data$settings7","_data$settings8","promotionPositions","relegationPositions","maxRinksPerSession","rinkPoints","pointsPerRinkWin","pointsPerRinkDraw","defaultRinks","enabled","teams","team","matches","match","addTeam","teamInstance","some","t","removeTeam","teamId","initialLength","filter","addMatch","matchInstance","homeTeamExists","awayTeamExists","m","getTeam","find","getMatch","matchId","getTeamMatches","getTeamStats","stats","teamName","played","won","drawn","lost","shotsFor","shotsAgainst","points","inPromotionPosition","inRelegationPosition","playedMatches","sort","a","b","recentMatchDetails","console","warn","isHome","teamScore","opponentScore","resultChar","matchDateStr","toLocaleDateString","undefined","year","month","day","description","slice","getLeagueTable","teamStats","shotDifference","sortedTable","localeCompare","index","position","leagueData","metaData","initialiseFixtures","startDate","schedulingParams","schedulingPattern","intervalNumber","intervalUnit","selectedDays","maxMatchesPerDay","scheduleTeams","BYE_TEAM_MARKER","now","numEffectiveTeams","roundsPerCycle","allMatches","fixedTeam","initialRotatingTeams","cycle","currentRotatingTeams","roundNum","conceptualPairsForThisRound","opponentForFixed","team1","team2","rotatingListLength","j","teamA_idx","teamB_idx","teamA","teamB","pair","roundKey","lastTeamInRotatingList","pop","unshift","_assignMatchDates","assignedRinks","_assignRinksToMatches","i","matchData","matchesByRound","sortedRoundKeys","keys","currentDate","isFirstRound","roundMatches","matchIndex","matchesForThisDate","validDate","_findNextValidDate","_getNextSchedulingDate","maxRinks","availableRinks","from","_","randomIndex","selectedRink","splice","fromDate","currentDay","getDay","includes","checkDay","nextDate","setDate","getDate","daysToAdd","getMatchesRequiringAttention","isArray","today","setHours","todayTimestamp","conflictingIds","getConflictingMatchIds","getPriority","matchDateObj","matchTimestamp","has","_a$homeTeam","_b$homeTeam","_a$awayTeam","_b$awayTeam","priorityA","priorityB","homeTeamA","homeTeamB","awayTeamA","awayTeamB","homeCompare","Set","futureFixtures","matchDate","matchesByDate","reduce","acc","dateKey","matchesOnDay","teamCounts","_match$homeTeam","_match$awayTeam","homeTeamId","awayTeamId","conflictingTeams","rinkCounts","matchesWithRinks","conflictingRinks","_match$homeTeam2","_match$awayTeam2","add"],"mappings":"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