UNPKG

@lovebowls/leagueelements

Version:

League Elements package for LoveBowls

1,296 lines (1,085 loc) 91.5 kB
// Import any necessary LitElement modules or other dependencies here import { League } from '@lovebowls/leaguejs'; import * as Swal from 'sweetalert2'; import { sweetAlertGlobalStyles, sweetAlertMobileOverrides } from '../shared-styles.js'; import '../LeagueMatchesAttention/LeagueMatchesAttention.js'; import '../leagueMatch/leagueMatch.js'; import '../leagueResetModal/leagueResetModal.js'; import '../leagueTeams/leagueTeams.js'; import '../LeagueSchedule/LeagueSchedule.js'; import { BASE_STYLES, MOBILE_STYLES, DESKTOP_STYLES, TEMPLATE_CONTENT} from './LeagueAdminElement-styles.js'; import { Temporal, TemporalUtils } from '../../utils/temporalUtils.js'; // Define custom event types for the new element class LeagueAdminElementEvent extends CustomEvent { constructor(type, detail) { super(type, { // Allow specifying event type detail, bubbles: true, composed: true }); } } class LeagueAdminElement extends HTMLElement { static _globalStylesInjected = false; constructor() { super(); this.LOG_PREFIX = "[LAD_LIFE_CYCLE] "; this.shadow = this.attachShadow({ mode: 'open' }); this._leagues = []; this._selectedLeagueId = null; // Store ID of the selected league this._currentLeagueId = null; // Store ID of the league to be pre-selected this._selectedTeamId = null; // Track selected team by _id for team management panel this._isModalVisible = false; this._modalMode = 'new'; // 'new', 'edit', 'copy' this._data = null; // To store the raw data from attribute // New properties for team management this._teamModalMode = null; // 'new' or 'edit' this._teamBeingEdited = null; // Store the team being edited as {_id, name} this._lovebowlsTeams = []; // CHANGED: Store lovebowls teams data as [{_id, name}] objects // Properties for new leagueTeams modal this.teamModalOpen = false; this.teamModalData = null; this.teamModalMode = 'new'; this.teamModalOptions = {}; // New properties for match management this.matchModalOpen = false; this.matchModalData = null; this.matchModalTeams = []; this.matchModalMode = 'new'; // New properties for reset modal management this.resetModalOpen = false; // Properties for new league creation confirmation this._pendingNewLeagueData = null; // Store league data for post-creation confirmation this._isWaitingForNewLeagueConfirmation = false; this._boundHandleDocumentClickForGlobalMenu = null; // For global menu closing } get _isMobile() { return this.getAttribute('is-mobile') === 'true'; } static get observedAttributes() { return ['data', 'is-mobile', 'current-league-id', 'lovebowls-teams']; } _injectGlobalSwalStyles() { // Injects SweetAlert2 global and mobile-specific styles into document.head. // Styles are imported from shared-styles.js. // This ensures consistent styling for SweetAlert2 dialogs, which are appended // to the document body and thus outside the shadow DOM of individual elements. // Injection is guarded to run only once per page load. const LOG_PREFIX_SWAL = '[LAD_SWAL_STYLE_INJECT] '; const styleId = 'global-swal-mobile-styles'; let existingStyleTag = document.getElementById(styleId); if (LeagueAdminElement._globalStylesInjected) { if (!existingStyleTag) { console.warn(LOG_PREFIX_SWAL + 'Global styles flag is true BUT style tag is MISSING. Will attempt to re-inject.'); } } else { if (existingStyleTag) { console.warn(LOG_PREFIX_SWAL + 'Global styles flag is false BUT style tag ALREADY EXISTS. Setting flag to true and skipping duplicate injection.'); LeagueAdminElement._globalStylesInjected = true; return; } // Proceed to inject if flag is false and tag doesn't exist } const style = document.createElement('style'); style.id = styleId; style.textContent = `${sweetAlertGlobalStyles}\n\n${sweetAlertMobileOverrides}`; try { document.head.appendChild(style); } catch (err) { console.error('[LAD_SWAL_STYLE_INJECT] Failed to append style tag to head:', err); } // Verify after appending const appendedStyleTag = document.getElementById(styleId); if (!appendedStyleTag) { console.error(LOG_PREFIX_SWAL + 'FAILED to find style tag in head AFTER attempting to append. Injection FAILED.'); } LeagueAdminElement._globalStylesInjected = true; } connectedCallback() { this._injectGlobalSwalStyles(); this._currentLeagueId = this.getAttribute('current-league-id') || null; const rawData = this.getAttribute('data'); if (rawData) { this._data = rawData; this._parseAndLoadData(); } this.render(); } disconnectedCallback() { // Ensure global menu handler is cleaned up if active if (this._boundHandleDocumentClickForGlobalMenu) { document.removeEventListener('click', this._boundHandleDocumentClickForGlobalMenu); this._boundHandleDocumentClickForGlobalMenu = null; } } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; let needsRender = false; if (name === 'data') { this._data = newValue; this._parseAndLoadData(); needsRender = true; // Data change always triggers a full re-render of the list } else if (name === 'is-mobile') { needsRender = true; } else if (name === 'current-league-id') { this._currentLeagueId = newValue || null; // Apply selection only if we have leagues loaded already if (this._leagues && this._leagues.length > 0) { const oldSelectedIdBeforeApply = this._selectedLeagueId; needsRender = this._applyCurrentLeagueIdSelection(); } } else if (name === 'lovebowls-teams') { this._parseLovebowlsTeamsData(newValue); // No need to re-render here unless the modal is already open } if (needsRender) { this.render(); } // Dispatch an event about the attribute change this.dispatchEvent(new LeagueAdminElementEvent('attributeChanged', { name, oldValue, newValue })); } _parseAndLoadData() { this.clearError(); // Store the current selection state before any changes const previouslySelectedLeagueId = this._selectedLeagueId; // Reset team selection and menu, but NOT the league selection yet this._selectedTeamId = null; this._hideGlobalLeagueMenu(); // Defensive check - if no data, clear leagues and return if (!this._data) { console.warn(this.LOG_PREFIX + `_parseAndLoadData: No data to parse. Clearing _leagues.`); this._leagues = []; this._selectedLeagueId = null; this.dispatchEvent(new LeagueAdminElementEvent('dataError', { message: 'No data provided' })); return; } try { // Parse the data attribute const parsedData = JSON.parse(this._data); if (Array.isArray(parsedData)) { // Convert each league object into a League instance this._leagues = parsedData.map(leagueData => { // Create a new League instance with the parsed data const league = new League(leagueData); // Ensure teams array always exists and has the proper format if (!Array.isArray(league.teams)) { league.teams = []; } // Generate league table if the method exists if (typeof league.getLeagueTable === 'function') { league.table = league.getLeagueTable(); } else { // Fallback for leagues without getLeagueTable method league.table = { leagueData: [], metaData: { name: league.name } }; } return league; }); // Check if previously selected league still exists in the new data const previousLeagueStillExists = previouslySelectedLeagueId && this._leagues.some(l => (l._id || l.name) === previouslySelectedLeagueId); // If the previously selected league still exists, keep it selected if (previousLeagueStillExists) { this._selectedLeagueId = previouslySelectedLeagueId; } else { this._selectedLeagueId = null; } this.dispatchEvent(new LeagueAdminElementEvent('onReady', { leagues: this._leagues })); // If selection changed due to league being removed, dispatch an event if (previouslySelectedLeagueId !== this._selectedLeagueId) { this.dispatchEvent(new LeagueAdminElementEvent('leagueSelected', { leagueId: this._selectedLeagueId })); } } else { console.error(this.LOG_PREFIX + `_parseAndLoadData: Parsed data is not an array.`); this._leagues = []; this.showError('Invalid data format: Expected an array of leagues.'); this.dispatchEvent(new LeagueAdminElementEvent('dataError', { message: 'Invalid data format: Expected an array of leagues.' })); } } catch (error) { console.error(this.LOG_PREFIX + `_parseAndLoadData: Error parsing data:`, error); this._leagues = []; this._data = null; this.showError(`Failed to parse league data: ${error.message}`); this.dispatchEvent(new LeagueAdminElementEvent('dataError', { message: `Failed to parse league data: ${error.message}`, errorObj: error })); this._selectedLeagueId = null; // Also clear on error } // Apply current league ID selection after data is loaded this._applyCurrentLeagueIdSelection(); // Check if we need to show new league confirmation dialog this._checkForNewLeagueConfirmation(); } // Parse lovebowls teams data from attribute - expects {_id, name} format _parseLovebowlsTeamsData(dataString) { if (!dataString) { this._lovebowlsTeams = []; return; } try { const parsedData = JSON.parse(dataString); if (Array.isArray(parsedData)) { this._lovebowlsTeams = parsedData; } else { console.error('Invalid lovebowls teams data format: Expected an array of teams'); this._lovebowlsTeams = []; } } catch (error) { console.error(`Failed to parse lovebowls teams data: ${error.message}`); this._lovebowlsTeams = []; } } // Helper method to apply currentLeagueId selection _applyCurrentLeagueIdSelection() { if (!this._currentLeagueId || !this._leagues || this._leagues.length === 0) { return false; // No change, no render needed from this } const leagueExists = this._leagues.some(l => (l._id || l.name) === this._currentLeagueId); let selectionChanged = false; if (leagueExists) { if (this._selectedLeagueId !== this._currentLeagueId) { this._selectedLeagueId = this._currentLeagueId; selectionChanged = true; } } else { if (this._selectedLeagueId === this._currentLeagueId) { // This case means _selectedLeagueId was pointing to a league (via current-league-id attribute) that is now gone this._selectedLeagueId = null; selectionChanged = true; } } if (selectionChanged) { this._updateButtonStates(); // This should be called if selection changes } return selectionChanged; // Return whether selection actually changed } showError(message) { const errorElement = this.shadow.querySelector('#error-message'); if (errorElement) { errorElement.textContent = message; errorElement.style.display = 'block'; } else { // Fallback if container not ready, though render should ensure it is. console.error("Error element not found in shadow DOM. Message:", message); } } clearError() { const errorElement = this.shadow.querySelector('#error-message'); if (errorElement) { errorElement.textContent = ''; errorElement.style.display = 'none'; } } render() { console.log('[LeagueAdmin] render() called'); let leagueForRender = null; const tempLeague = this._getSelectedLeague(); // This can return undefined if (tempLeague !== undefined && tempLeague !== null) { leagueForRender = tempLeague; } this.shadow.innerHTML = ` <style> ${this._isMobile ? MOBILE_STYLES : DESKTOP_STYLES} </style> ${TEMPLATE_CONTENT} `; // Setup resizer this._setupResizer(); // Initial UI setup that happens after main template is in place this._renderLeagueList(); this._updateButtonStates(); this._attachBaseEventListeners(); // Listeners for New, Modal Close etc. // If a league was selected, try to re-select it if it still exists if (this._selectedLeagueId) { const selectedElement = this.shadow.querySelector(`.league-list-item[data-id="${this._selectedLeagueId}"]`); if (selectedElement && leagueForRender) { // check leagueForRender exists selectedElement.classList.add('selected'); this._showLeagueSpecificPanels(); // Show the selected league panel (uses _getSelectedLeague() internally) } else { this._selectedLeagueId = null; // It no longer exists or leagueForRender is null this._hideLeagueSpecificPanels(); // Hide the panel this._updateButtonStates(); } } else { this._hideLeagueSpecificPanels(); // Make sure panel is hidden when no league is selected // Hide attention panel if no league selected const attentionPanel = this.shadow.querySelector('#admin-attention-panel'); if (attentionPanel) attentionPanel.style.display = 'none'; } // Update attention panel with selected league's matches this._updateAttentionPanel(leagueForRender); // Update schedule panel with selected league's data if (leagueForRender) { console.log('[LeagueAdmin] About to call _updateSchedulePanel from render()'); this._updateSchedulePanel(leagueForRender); console.log('[LeagueAdmin] Finished _updateSchedulePanel from render()'); } // Render match modal separately to avoid full component re-renders this._renderMatchModal(); // Handle reset modal if (this.resetModalOpen && this._leagueToReset) { let resetModal = this.shadow.querySelector('league-reset-modal'); if (resetModal) resetModal.remove(); resetModal = document.createElement('league-reset-modal'); resetModal.open = true; resetModal.isMobile = this._isMobile; resetModal.data = this._leagueToReset; resetModal.addEventListener('reset-save', (e) => { const { matches, estimatedMatches, dateRange } = e.detail; const leagueToReset = this._leagueToReset; // Store reference before closing modal // Close modal first this._closeResetModal(); // Apply the generated matches to the league this._applyGeneratedMatches(leagueToReset, matches, estimatedMatches, dateRange); }); resetModal.addEventListener('reset-cancel', () => { this._closeResetModal(); }); this.shadow.appendChild(resetModal); } else { let resetModal = this.shadow.querySelector('league-reset-modal'); if (resetModal) resetModal.remove(); } // Handle team modal if (this.teamModalOpen) { let teamModal = this.shadow.querySelector('league-teams'); if (teamModal) teamModal.remove(); teamModal = document.createElement('league-teams'); teamModal.open = true; teamModal.isMobile = this._isMobile; teamModal.data = this._getSelectedLeague(); teamModal.existingTeams = this._lovebowlsTeams; // Set action based on mode const action = {}; if (this.teamModalMode === 'edit' && this.teamModalData) { action.editTeam = this.teamModalData._id; } // Add options from teamModalOptions (e.g., fromNewLeagueWorkflow) if (this.teamModalOptions && Object.keys(this.teamModalOptions).length > 0) { Object.assign(action, this.teamModalOptions); } // For 'manage' mode, don't set any action to show the teams manager without auto-opening editor if (Object.keys(action).length > 0) { teamModal.action = action; } teamModal.addEventListener('teams-save', (e) => { const { league, showFixtureScheduler } = e.detail; // Update the internal _leagues array const leagueIndex = this._leagues.findIndex(l => l._id === league._id); if (leagueIndex > -1) { this._leagues[leagueIndex] = league; } else { console.error('[Admin Teams Save] Selected league index not found in _leagues array.'); } this.dispatchEvent(new LeagueAdminElementEvent('requestSaveLeague', { leagueData: league })); this.closeTeamModal(); // If showFixtureScheduler is true and we have at least 2 teams, open the reset modal if (showFixtureScheduler && league.teams && league.teams.length >= 2) { // Small delay to allow the team modal to close and DOM to update setTimeout(() => { this._openResetModal(league); }, 100); } }); teamModal.addEventListener('teams-cancel', () => { this.closeTeamModal(); }); this.shadow.appendChild(teamModal); } else { let teamModal = this.shadow.querySelector('league-teams'); if (teamModal) teamModal.remove(); } } _renderLeagueList() { const listElement = this.shadow.querySelector('#league-list-ul'); if (!listElement) return; listElement.innerHTML = ''; // Clear existing items if (!this._leagues || this._leagues.length === 0) { const li = document.createElement('li'); li.textContent = 'No leagues available.'; li.style.padding = '0.5rem'; // Basic styling for the message listElement.appendChild(li); return; } // Sort leagues alphabetically by name const sortedLeagues = [...this._leagues].sort((a, b) => { const nameA = (a.name || '').toUpperCase(); const nameB = (b.name || '').toUpperCase(); return nameA.localeCompare(nameB); }); sortedLeagues.forEach(league => { // Ensure the league has an ID if (!league._id) { console.warn('League is missing _id:', league); return; // Skip leagues without an ID } const li = document.createElement('li'); li.classList.add('league-list-item', 'list-item-shared'); // Add caret icon before the league name const caretSpan = document.createElement('span'); caretSpan.textContent = '▶ '; // Unicode right-pointing triangle caretSpan.style.marginRight = '0.5em'; caretSpan.style.fontSize = '0.8em'; caretSpan.style.color = 'var(--lae-text-color-secondary, #666)'; li.appendChild(caretSpan); const nameSpan = document.createElement('span'); nameSpan.classList.add('league-name-text', 'list-item-text-primary'); nameSpan.textContent = league.name || 'Unnamed League'; li.appendChild(nameSpan); const actionsContainer = document.createElement('div'); actionsContainer.classList.add('league-item-actions-container', 'list-item-actions'); li.appendChild(actionsContainer); const leagueId = league._id; li.setAttribute('data-id', leagueId); if (league._id === this._selectedLeagueId) { this._createAndAppendLeagueActions(actionsContainer, league._id); } // Add click handler li.addEventListener('click', (e) => { e.stopPropagation(); this._handleLeagueSelect(leagueId); }); // Add double-click shortcut for editing leagues in desktop mode if (this.getAttribute('is-mobile') !== 'true') { li.addEventListener('dblclick', (e) => { e.stopPropagation(); // Select the league first this._handleLeagueSelect(leagueId); // Then open edit modal this._handleEditLeagueRules(); }); } listElement.appendChild(li); }); } _handleLeagueSelect(leagueId) { this.clearError(); const previouslySelectedId = this._selectedLeagueId; const previouslySelectedElement = this.shadow.querySelector('.league-list-item.selected'); // If the clicked league is the same as the currently selected one, do nothing if (leagueId === this._selectedLeagueId) { return; } // Find the clicked league in our data - only match by _id const selectedLeague = this._leagues.find(league => league._id === leagueId); if (selectedLeague) { // Use the _id as the identifier const effectiveId = selectedLeague._id; // Update the selected league ID this._selectedLeagueId = effectiveId; this._renderLeagueList(); // Update the UI to show the selected league // First, remove selected class from all league items const allLeagueItems = this.shadow.querySelectorAll('.league-list-item'); allLeagueItems.forEach(item => item.classList.remove('selected')); // Find and select the clicked league item const newSelectedElement = this.shadow.querySelector(`.league-list-item[data-id="${effectiveId}"]`); if (newSelectedElement) { newSelectedElement.classList.add('selected'); // Only call scrollIntoView if supported (e.g. avoids errors in jsdom tests) if (typeof newSelectedElement.scrollIntoView === 'function') { newSelectedElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } // Show the league-specific panels this._showLeagueSpecificPanels(); // Reset selected team this._selectedTeamId = null; // Dispatch the leagueSelected event this.dispatchEvent(new LeagueAdminElementEvent('leagueSelected', { leagueId: effectiveId, leagueName: selectedLeague.name })); } else { // League not found in list, effectively deselecting console.warn(this.LOG_PREFIX + `_handleLeagueSelect: League with ID ${leagueId} not found`); this._selectedLeagueId = null; this._selectedTeamId = null; this._hideLeagueSpecificPanels(); this.dispatchEvent(new LeagueAdminElementEvent('leagueSelected', { leagueId: null })); } this._updateButtonStates(); } _createAndAppendLeagueActions(container, leagueId) { container.innerHTML = ''; // Clear any previous content // Only the "..." trigger button remains here const btnActions = document.createElement('button'); btnActions.classList.add('league-action-button', 'actions-dropdown-button'); // Keep styling classes btnActions.textContent = '…'; btnActions.title = 'More actions'; btnActions.addEventListener('click', (e) => { e.stopPropagation(); this._handleOpenGlobalLeagueMenu(leagueId, e.currentTarget); // Call new handler }); container.appendChild(btnActions); } _showLeagueSpecificPanels() { const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) { this._hideLeagueSpecificPanels(); return; } // Show the teams panel const teamsPanel = this.shadow.querySelector('#teams-panel'); if (teamsPanel) { teamsPanel.style.display = ''; this._renderTeamsList(); } else { console.warn(this.LOG_PREFIX + 'Teams panel element not found'); } // Show the attention container and update it const attentionContainer = this.shadow.querySelector('#admin-matches-attention-container'); if (attentionContainer) { attentionContainer.style.display = ''; this._updateAttentionPanel(selectedLeague); } else { console.warn(this.LOG_PREFIX + 'Attention container element not found'); } // Show the schedule panel and update it const schedulePanel = this.shadow.querySelector('#league-schedule-panel'); if (schedulePanel) { schedulePanel.style.display = ''; this._updateSchedulePanel(selectedLeague); } else { console.warn(this.LOG_PREFIX + 'Schedule panel element not found'); } // Ensure the details column is visible (in case it was hidden) const detailsColumn = this.shadow.querySelector('.column-details'); if (detailsColumn) { detailsColumn.style.display = ''; } } _hideLeagueSpecificPanels() { // Hide the attention container const attentionContainer = this.shadow.querySelector('#admin-matches-attention-container'); if (attentionContainer) { attentionContainer.style.display = 'none'; } else { console.warn(this.LOG_PREFIX + 'Attention container element not found when trying to hide'); } // Hide the schedule panel const schedulePanel = this.shadow.querySelector('#league-schedule-panel'); if (schedulePanel) { schedulePanel.style.display = 'none'; } else { console.warn(this.LOG_PREFIX + 'Schedule panel element not found when trying to hide'); } // Reset selected team this._selectedTeamId = null; // Hide the teams panel const teamsPanel = this.shadow.querySelector('#teams-panel'); if (teamsPanel) { teamsPanel.style.display = 'none'; // Clear and reset the teams list const teamsList = this.shadow.querySelector('#teams-list'); if (teamsList) { teamsList.innerHTML = '<li style="padding: 0.5rem;">No league selected.</li>'; } } else { console.warn(this.LOG_PREFIX + 'Teams panel element not found when trying to hide'); } // Hide the details column (if in mobile view) if (this.getAttribute('is-mobile') === 'true') { const detailsColumn = this.shadow.querySelector('.column-details'); if (detailsColumn) { detailsColumn.style.display = 'none'; } } } _renderTeamsList() { const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) return; const teamsList = this.shadow.querySelector('#teams-list'); if (!teamsList) return; teamsList.innerHTML = ''; // Clear existing items const teams = selectedLeague.teams || []; if (teams.length === 0) { const li = document.createElement('li'); li.textContent = 'No teams available.'; li.style.padding = '0.5rem'; teamsList.appendChild(li); return; } teams.forEach(team => { const li = document.createElement('li'); li.classList.add('team-item', 'list-item-shared'); // Use team._id as the identifier and team.name for display li.dataset.teamId = team._id; // CHANGED: Use _id as the identifier // Check if this team is the selected one if (this._selectedTeamId === team._id) { li.classList.add('selected-team'); } const nameSpan = document.createElement('span'); nameSpan.classList.add('team-name', 'list-item-text-primary'); nameSpan.textContent = team.name; // CHANGED: Display the name instead of label // Add a small indicator if this is a lovebowls team (can use another way to identify) const isLovebowlsTeam = this._lovebowlsTeams.some(lbTeam => lbTeam._id === team._id); if (isLovebowlsTeam) { const indicator = document.createElement('small'); indicator.style.marginLeft = '0.5em'; indicator.style.opacity = '0.7'; indicator.textContent = '(LB)'; // Lovebowls indicator nameSpan.appendChild(indicator); } li.appendChild(nameSpan); const actionsDiv = document.createElement('div'); actionsDiv.classList.add('team-actions', 'list-item-actions'); li.appendChild(actionsDiv); // Add click listener to the list item itself li.addEventListener('click', (e) => { e.stopPropagation(); this._handleTeamSelect(team); // Pass the whole team object }); // Add double-click shortcut for editing teams in desktop mode if (this.getAttribute('is-mobile') !== 'true') { li.addEventListener('dblclick', (e) => { e.stopPropagation(); this._handleEditTeam(team); }); } // If this is the selected team, add action buttons if (this._selectedTeamId === team._id) { this._createAndAppendTeamActions(actionsDiv, team); } teamsList.appendChild(li); }); } _updateButtonStates() { const btnCopy = this.shadow.querySelector('#copy-league-button'); const btnUpdate = this.shadow.querySelector('#update-league-button'); const btnDelete = this.shadow.querySelector('#delete-league-button'); const isLeagueSelected = !!this._selectedLeagueId; if (btnCopy) btnCopy.disabled = !isLeagueSelected; if (btnUpdate) btnUpdate.disabled = !isLeagueSelected; if (btnDelete) btnDelete.disabled = !isLeagueSelected; } _attachBaseEventListeners() { const btnNew = this.shadow.querySelector('#new-league-button'); const btnCopy = this.shadow.querySelector('#copy-league-button'); const btnUpdate = this.shadow.querySelector('#update-league-button'); // Remove reference to the deleted button // const btnDelete = this.shadow.querySelector('#delete-league-button'); const btnCloseModal = this.shadow.querySelector('#close-league-modal'); const btnCancelModal = this.shadow.querySelector('#cancel-league-button'); const btnSaveModal = this.shadow.querySelector('#save-league-button'); if (btnNew) btnNew.addEventListener('click', () => this._handleNewLeague()); if (btnCopy) btnCopy.addEventListener('click', () => this._handleCopyLeague()); if (btnUpdate) btnUpdate.addEventListener('click', () => this._handleEditLeagueRules()); // Remove event listener for deleted button // if (btnDelete) btnDelete.addEventListener('click', () => this._handleDeleteLeague()); if (btnCloseModal) btnCloseModal.addEventListener('click', () => this._hideModal()); if (btnCancelModal) btnCancelModal.addEventListener('click', () => this._hideModal()); if (btnSaveModal) btnSaveModal.addEventListener('click', () => this._handleSaveModal()); // Event listeners for View Table, Actions dropdown, and Reset League are now attached dynamically // in _createAndAppendLeagueActions when a league item is selected, because these elements // are no longer static in the template. // The global document click listener for closing any open dropdowns is handled by _handleDocumentClickForActionsDropdown. // Team management event listeners const btnAddTeam = this.shadow.querySelector('#add-team-button'); if (btnAddTeam) { btnAddTeam.addEventListener('click', () => this._handleAddTeam()); } // Team modal event listeners are now handled by the league-teams component } _getSelectedLeague() { if (!this._selectedLeagueId) return null; // Ensure _leagues is an array before trying to find return Array.isArray(this._leagues) ? this._leagues.find(l => l._id === this._selectedLeagueId) : null; } _handleResetLeague() { this.clearError(); // Good practice to clear any existing errors const leagueIdToReset = this._currentLeagueIdForMenu; if (!leagueIdToReset) { this.showError("Cannot reset: league context from menu is missing."); console.error("[Reset League] _currentLeagueIdForMenu is not set during reset attempt."); this._hideGlobalLeagueMenu(); // Hide menu and exit return; } const leagueToResetObject = Array.isArray(this._leagues) ? this._leagues.find(l => (l._id || l.name) === leagueIdToReset) : null; if (leagueToResetObject) { // Open the reset modal instead of SweetAlert2 this._openResetModal(leagueToResetObject); this._hideGlobalLeagueMenu(); // Hide menu when opening modal } else { this.showError(`League with ID "${leagueIdToReset}" not found to reset.`); console.error(`[Reset League] League with ID "${leagueIdToReset}" (from _currentLeagueIdForMenu) not found in this._leagues.`); this._hideGlobalLeagueMenu(); // Hide menu if league not found } } /** * Open the reset modal for a league * @param {Object} league - The league object to reset */ _openResetModal(league) { this.resetModalOpen = true; this._leagueToReset = league; // Store the league for later use this.render(); } /** * Close the reset modal */ _closeResetModal() { this.resetModalOpen = false; this._leagueToReset = null; this.render(); } /** * Reset matches for a league while preserving teams * @param {Object} leagueToReset - The league object to reset matches for * @param {Array} generatedMatches - The generated matches array * @param {number} matchCount - The number of matches generated * @param {Object} dateRange - The date range for scheduling matches */ _applyGeneratedMatches(leagueToReset, generatedMatches, matchCount, dateRange) { try { const leagueName = leagueToReset.name || leagueToReset._id || 'Unknown League'; // Create a deep copy of the league to avoid modifying the original during processing const updatedLeague = JSON.parse(JSON.stringify(leagueToReset)); // Map the generated matches to use the actual team objects from the league const mappedMatches = this._mapGeneratedMatchesToActualTeams(generatedMatches, updatedLeague.teams); // Apply the generated matches updatedLeague.matches = mappedMatches; // Update the league in the internal _leagues array const leagueIndex = this._leagues.findIndex(l => (l._id || l.name) === (leagueToReset._id || leagueToReset.name)); if (leagueIndex !== -1) { this._leagues[leagueIndex] = updatedLeague; // Dispatch event to save the updated league this.dispatchEvent(new LeagueAdminElementEvent('requestSaveLeague', { leagueData: updatedLeague })); // Re-render to reflect changes this.render(); // Show success message with scheduling info let successMessage = `"${leagueName}" matches have been reset. ${matchCount} new matches generated.`; if (dateRange) { successMessage += ` Matches scheduled from ${dateRange.start} to ${dateRange.end}.`; } Swal.default.fire({ customClass: this._getSwalCustomClasses(), title: 'Matches Reset Successfully', text: successMessage, icon: 'success', timer: 4000, showConfirmButton: false }); console.log(`[Reset League] Successfully reset matches for "${leagueName}". Generated ${matchCount} matches.`); } else { throw new Error('League not found in internal array after reset'); } } catch (error) { console.error('[Reset League] Error applying generated matches:', error); this.showError(`Failed to apply generated matches: ${error.message}`); Swal.default.fire({ customClass: this._getSwalCustomClasses(), title: 'Reset Failed', text: `Failed to apply generated matches: ${error.message}`, icon: 'error', timer: 4000, showConfirmButton: false }); } } _mapGeneratedMatchesToActualTeams(generatedMatches, actualTeams) { // Map the temporary team IDs to actual team objects return generatedMatches.map((match, index) => { // Use index-based mapping since temp teams are generated sequentially const homeTeamIndex = parseInt(match.homeTeam._id.replace('temp_team_', '')) - 1; const awayTeamIndex = parseInt(match.awayTeam._id.replace('temp_team_', '')) - 1; return { ...match, _id: `match_${Date.now()}_${index}`, homeTeam: actualTeams[homeTeamIndex] || match.homeTeam, awayTeam: actualTeams[awayTeamIndex] || match.awayTeam }; }); } _handleViewLeagueTable() { const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) return; this.dispatchEvent(new LeagueAdminElementEvent('requestViewLeagueTable', { leagueId: this._selectedLeagueId, leagueName: selectedLeague.name })); this._hideGlobalLeagueMenu(); // ADDED: Hide menu after action } // Team Management Methods _handleAddTeam() { if (!this._selectedLeagueId) return; this.openTeamModal(null, 'manage'); } _handleEditTeam(team) { if (!team) return; this.openTeamModal(team, 'edit'); } _handleRemoveTeam(team) { if (!team) return; const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) return; // Show confirmation dialog using window.Swal Swal.default.fire({ customClass: this._getSwalCustomClasses(), title: 'Remove Team?', text: `Are you sure you want to remove "${team.name}" from the league? This will also remove all matches involving this team.`, icon: 'warning', showCancelButton: true, confirmButtonText: 'Yes, remove team', confirmButtonColor: '#d33' }).then((result) => { if (result.isConfirmed) { // Clear the team selection since we're removing it if (this._selectedTeamId === team._id) { this._selectedTeamId = null; } // Optimistically update the UI by removing the team and its matches locally const leagueIndex = this._leagues.findIndex(l => (l._id || l.name) === this._selectedLeagueId); if (leagueIndex !== -1) { // Remove the team const updatedTeams = this._leagues[leagueIndex].teams.filter(t => t._id !== team._id); // Remove all matches involving this team const updatedMatches = (this._leagues[leagueIndex].matches || []).filter(match => match.homeTeam?._id !== team._id && match.awayTeam?._id !== team._id ); // Update the league data this._leagues[leagueIndex].teams = updatedTeams; this._leagues[leagueIndex].matches = updatedMatches; // Dispatch event to notify parent about team removal this.dispatchEvent(new LeagueAdminElementEvent('requestRemoveTeam', { leagueId: this._selectedLeagueId, teamId: team._id })); // Re-render teams list this._renderTeamsList(); // ADDED: Refresh the attention panel to reflect removed matches const leagueToRefresh = this._getSelectedLeague(); if (leagueToRefresh) { this._updateAttentionPanel(leagueToRefresh); } // Show success message Swal.default.fire({ customClass: this._getSwalCustomClasses(), title: 'Team Removed', text: `${team.name} has been removed from the league`, icon: 'success', timer: 2000, showConfirmButton: false }); } } }); } _showTeamModal(mode, teamData = null, existingTeams = []) { this.clearError(); const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) return; const modal = this.shadow.querySelector('#team-modal'); const modalTitle = this.shadow.querySelector('#team-modal-title'); const modalBody = this.shadow.querySelector('#team-modal-body'); if (!modal || !modalTitle || !modalBody) return; let title = ''; let currentTeamData = {}; switch (mode) { case 'new': title = 'Add Team'; currentTeamData = { _id: '', name: '', useExistingTeam: false }; break; case 'edit': title = 'Edit Team'; // We expect teamData to already be in {_id, name} format currentTeamData = { _id: teamData._id, name: teamData.name, useExistingTeam: this._lovebowlsTeams.some(lbTeam => lbTeam._id === teamData._id) // Check if it's a Lovebowls team }; break; default: return; } this._teamModalMode = mode; this._teamBeingEdited = currentTeamData; modalTitle.textContent = title; this._populateTeamModalForm(modalBody, currentTeamData, existingTeams); modal.style.display = 'block'; } _hideTeamModal() { const modal = this.shadow.querySelector('#team-modal'); if (modal) { modal.style.display = 'none'; } this._teamModalMode = null; this._teamBeingEdited = null; } _populateTeamModalForm(modalBody, teamData, existingTeams = []) { const isEditMode = this._teamModalMode === 'edit'; // Determine if it's a Lovebowls team let isLovebowlsTeam = teamData.useExistingTeam; // Set the checkbox label based on mode const checkboxLabel = (isEditMode && !isLovebowlsTeam) ? "Replace with team from lovebowls.co.uk" : "Team from lovebowls"; // Get the selected league to filter out teams that already exist in it const selectedLeague = this._getSelectedLeague(); // Filter out lovebowls teams that are already part of the league let filteredTeams = existingTeams; if (selectedLeague && selectedLeague.teams && Array.isArray(selectedLeague.teams)) { // If in edit mode, don't filter out the team we're currently editing const teamIdsToExclude = isEditMode ? selectedLeague.teams.filter(t => t._id !== teamData._id).map(t => t._id) : selectedLeague.teams.map(t => t._id); filteredTeams = existingTeams.filter(team => !teamIdsToExclude.includes(team._id)); } let optionsHtml = ''; if (filteredTeams && filteredTeams.length > 0) { optionsHtml = filteredTeams.map(team => `<option value="${team._id}" ${teamData._id === team._id ? 'selected' : ''}>${team.name}</option>` ).join(''); } modalBody.innerHTML = ` <!-- Error banner for the modal --> <div id="team-modal-error" class="form-error-shared" style="display: none; margin-bottom: var(--lae-padding-s); color: var(--lae-text-color-error); background-color: var(--lae-background-color-error); padding: var(--lae-padding-s); border: 1px solid var(--lae-border-color-error); border-radius: var(--lae-border-radius-standard);"></div> <div class="form-group-shared"> <label for="useExistingTeamCheckbox" class="form-label-shared" id="useExistingTeamLabel"> <input type="checkbox" id="useExistingTeamCheckbox" ${isLovebowlsTeam ? 'checked' : ''}> ${checkboxLabel} </label> </div> <div id="existingTeamSelectGroup" class="form-group-shared" style="display: ${isLovebowlsTeam ? 'block' : 'none'};"> <label for="existingTeamSelect" class="form-label-shared">Select Team</label> <select id="existingTeamSelect" class="form-input-shared"> <option value="">-- Select a Team --</option> ${optionsHtml} </select> ${filteredTeams.length === 0 ? '<div style="color: var(--lae-text-color-error); margin-top: 0.5em;">All lovebowls teams are already in this league</div>' : ''} <!-- ADDED: Info message for no available teams --> <div id="noAvailableTeamsMessage" style="display: none; color: var(--lae-text-color-error); margin-top: 0.5em;"> No available teams to select. Please add a new team. </div> </div> <div id="newTeamNameGroup" class="form-group-shared" style="display: ${isLovebowlsTeam ? 'none' : 'block'};"> <label for="teamName" class="form-label-shared">Team Name</label> <input type="text" id="teamName" class="form-input-shared" value="${isLovebowlsTeam ? '' : teamData.name}" ${isLovebowlsTeam ? 'disabled' : ''}> </div> `; const useExistingTeamCheckbox = modalBody.querySelector('#useExistingTeamCheckbox'); const useExistingTeamLabel = modalBody.querySelector('#useExistingTeamLabel'); const existingTeamSelectGroup = modalBody.querySelector('#existingTeamSelectGroup'); const newTeamNameGroup = modalBody.querySelector('#newTeamNameGroup'); const teamNameInput = modalBody.querySelector('#teamName'); const existingTeamSelect = modalBody.querySelector('#existingTeamSelect'); const noAvailableTeamsMessage = modalBody.querySelector('#noAvailableTeamsMessage'); if (useExistingTeamCheckbox && existingTeamSelectGroup && newTeamNameGroup && teamNameInput && existingTeamSelect) { useExistingTeamCheckbox.addEventListener('change', (e) => { const isChecked = e.target.checked; if (filteredTeams && filteredTeams.length > 0) { existingTeamSelectGroup.style.display = isChecked ? 'block' : 'none'; newTeamNameGroup.style.display = isChecked ? 'none' : 'block'; teamNameInput.disabled = isChecked; existingTeamSelect.disabled = !isChecked; if (isChecked) { teamNameInput.value = ''; // Clear manual input when switching } else { existingTeamSelect.value = ''; // Clear selection when switching } } else { // If no available filtered teams, checkbox effectively does nothing to visibility of select existingTeamSelectGroup.style.display = 'none'; newTeamNameGroup.style.display = 'block'; teamNameInput.disabled = false; // Uncheck the box since there are no available teams if (isChecked && filteredTeams.length === 0) { useExistingTeamCheckbox.checked = false; } } }); // Initial state based on mode and available teams if (!filteredTeams || filteredTeams.length === 0) { // Disable checkbox if no Lovebowls teams are available useExistingTeamCheckbox.disabled = true; const tooltip = existingTeams.length === 0 ? "No lovebowls teams available" : "All lovebowls teams are already in this league"; useExistingTeamCheckbox.title = tooltip; if (useExistingTeamLabel) { useExistingTeamLabel.title = tooltip; useExistingTeamLabel.style.cursor = 'not-allowed'; useExistingTeamLabel.style.opacity = '0.6'; } useExistingTeamCheckbox.checked = false; existingTeamSelectGroup.style.display = 'none'; newTeamNameGroup.style.display = 'block'; teamNameInput.disabled = false; } } } // Helper method to show error in the team modal _showTeamModalError(message) { const errorElement = this.shadow.querySelector('#team-modal-error'); if (errorElement) { errorElement.textContent = message; errorElement.style.display = 'block'; } } // Helper method to clear error in the team modal _clearTeamModalError() { const errorElement = this.shadow.querySelector('#team-modal-error'); if (errorElement) { errorElement.textContent = ''; errorElement.style.display = 'none'; } } _handleSaveTeamModal() { // Clear any previous error messages in the modal this._clearTeamModalError(); const modalBody = this.shadow.querySelector('#team-modal-body'); const useExistingTeamCheckbox = modalBody.querySelector('#useExistingTeamCheckbox'); const existingTeamSelect = modalBody.querySelector('#existingTeamSelect'); const teamNameInput = modalBody.querySelector('#teamName'); let teamId = ''; let teamName = ''; if (useExistingTeamCheckbox && useExistingTeamCheckbox.checked && existingTeamSelect) { if (existingTeamSelect.value) { // For lovebowls teams, get both _id and name teamId = existingTeamSelect.value; // Find the name from the selected lovebowls team const selectedTeam = this._lovebowlsTeams.find(t => t._id === teamId); teamName = selectedTeam ? selectedTeam.name : teamId; } else { this._showTeamModalError('Please select a team from the dropdown.'); return; } } else if (teamNameInput && teamNameInput.value.trim()) { // For simple teams, use the input as name and _id teamName = teamNameInput.value.trim(); teamId = teamName; } else { this._showTeamModalError('Team Name is required, either by typing a new name or selecting an existing team.'); return; } if (!teamId || !teamName) { this._showTeamModalError('Team name is required.'); return; } // Check for duplicate team IDs in the current league const selectedLeague = this._getSelectedLeague(); if (selectedLeague && selectedLeague.teams) { const isDuplicate = selectedLeague.teams.some(team => team._id === teamId && teamId !== this._teamBeingEdited._id); if (isDuplicate) { this._showTeamModalError(`A team with identifier "${teamId}" already exists in this league.`); return; } } const teamData = { _id: teamId, name: teamName }; // Update local data first for immediate UI response if (selectedLeague) { const leagueIndex = this._leagues.findIndex(l => l._id === this._selectedLeagueId); if (leagueIndex !== -1) { const updatedTeams = [...selectedLeague.teams]; const existingTeamIndex = updatedTeams.findIndex(t => t._id === this._teamBeingEdited._id); // If we're editing a team and its ID is changing, update match references const oldTeamId = this._teamBeingEdited._id; const isTeamIdChanging = existingTeamIndex !== -1 && oldTeamId !== teamId; // Deep copy the selected le