UNPKG

@lovebowls/leagueelements

Version:

League Elements package for LoveBowls

1,326 lines (1,117 loc) 106 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 '../leagueDashboard/LeagueDashboard.js'; import { MOBILE_STYLES, DESKTOP_STYLES, DESKTOP_TEMPLATE, MOBILE_TEMPLATE} from './LeagueAdminElement-styles.js'; import { getMobileStyles, getDesktopStyles } from '../shared-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; // New properties for loading state management this._isNewLeagueLoading = false; // Track if New league operation is in progress this._isCopyLeagueLoading = false; // Track if Copy league operation is in progress this._loadingTimeout = null; // Timeout for showing error if operation takes too long this._rightPanelWasVisible = false; // Track previous right panel visibility } get _isMobile() { return this.getAttribute('is-mobile') === 'true'; } get _fontScale() { const scale = parseFloat(this.getAttribute('font-scale')) || 1.0; // Clamp the scale between 0.5 and 2.0 for reasonable bounds return Math.max(0.5, Math.min(2.0, scale)); } static get observedAttributes() { return ['data', 'is-mobile', 'current-league-id', 'lovebowls-teams', 'font-scale']; } _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 { // Check if we're in a test environment (JSDOM) and handle gracefully if (typeof window !== 'undefined' && window.navigator && window.navigator.userAgent && window.navigator.userAgent.includes('jsdom')) { // In JSDOM, just set the style without appending to avoid Node type errors console.warn('[LAD_SWAL_STYLE_INJECT] JSDOM environment detected, skipping style injection'); LeagueAdminElement._globalStylesInjected = true; return; } document.head.appendChild(style); } catch (err) { console.error('[LAD_SWAL_STYLE_INJECT] Failed to append style tag to head:', err); // In case of error, still mark as injected to avoid repeated attempts LeagueAdminElement._globalStylesInjected = true; return; } // 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() { // Cleanup if needed if (this._documentClickHandlerBound) { document.removeEventListener('click', this._documentClickHandlerBound); this._documentClickHandlerBound = null; } // Clear any pending timeouts if (this._loadingTimeout) { clearTimeout(this._loadingTimeout); this._loadingTimeout = 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 === 'font-scale') { needsRender = true; // Font scale change requires re-render to update styles } 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, but NOT the league selection yet this._selectedTeamId = null; // 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 // If we were waiting for a league operation, handle it as an error if (this._isWaitingForNewLeagueConfirmation) { this._handleLeagueOperationError('Failed to save league due to data parsing 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; } 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 fontscale:${this._fontScale}`); let leagueForRender = null; const tempLeague = this._getSelectedLeague(); // This can return undefined if (tempLeague !== undefined && tempLeague !== null) { leagueForRender = tempLeague; } this.shadow.innerHTML = ` <style> ${this._isMobile ? getMobileStyles(this._fontScale) : getDesktopStyles(this._fontScale)} ${this._isMobile ? MOBILE_STYLES : DESKTOP_STYLES} </style> ${this._isMobile ? MOBILE_TEMPLATE : DESKTOP_TEMPLATE} `; // Highlight the New button if no leagues exist const btnNew = this.shadow.querySelector('#new-league-button'); if (btnNew) { if (!this._leagues || this._leagues.length === 0) { btnNew.classList.add('highlight-glow'); } else { btnNew.classList.remove('highlight-glow'); } } // Desktop: Expand left panel and hide right panel/resizer if no leagues or none selected if (!this._isMobile) { const leftPanel = this.shadow.querySelector('.column-leagues'); const rightPanel = this.shadow.querySelector('.column-details'); const resizer = this.shadow.querySelector('#resizer'); const noLeagues = !this._leagues || this._leagues.length === 0; const noSelection = !this._selectedLeagueId; if (leftPanel && rightPanel && resizer) { if (noLeagues || noSelection) { leftPanel.style.width = '100%'; leftPanel.style.flex = '1 1 100%'; rightPanel.style.display = 'none'; resizer.style.display = 'none'; } else { // Only reset width/flex if right panel was previously hidden if (!this._rightPanelWasVisible) { leftPanel.style.width = ''; leftPanel.style.flex = ''; } rightPanel.style.display = ''; resizer.style.display = ''; } // Update tracker for next render this._rightPanelWasVisible = !(noLeagues || noSelection); } } // 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(`.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) { this._updateSchedulePanel(leagueForRender); } // 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.setAttribute('font-scale', this._fontScale); 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.setAttribute('font-scale', this._fontScale); 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(); // Re-render the teams list to reflect the changes this._renderTeamsList(); // 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'); if (!listElement) return; listElement.innerHTML = ''; // Clear existing items if (!this._leagues || this._leagues.length === 0) { const li = document.createElement('li'); li.classList.add('list-item'); li.textContent = '❗ No leagues available. Click New to create one.'; 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('list-item'); const nameSpan = document.createElement('span'); nameSpan.classList.add('list-item-text-primary'); nameSpan.textContent = league.name || 'Unnamed League'; li.appendChild(nameSpan); const actionsContainer = document.createElement('div'); actionsContainer.classList.add('list-item-actions'); li.appendChild(actionsContainer); const leagueId = league._id; li.setAttribute('data-id', leagueId); if (league._id === this._selectedLeagueId) { li.classList.add('selected'); 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; // If the clicked league is the same as the currently selected one, do nothing if (leagueId === this._selectedLeagueId) { return; } // Clear the team filter from the schedule component before changing leagues const scheduleElement = this.shadow.querySelector('#admin-league-schedule'); if (scheduleElement) { scheduleElement.removeAttribute('selected-team'); if (typeof scheduleElement.clearFilterState === 'function') { scheduleElement.clearFilterState(); } } // 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; // Reset selected team BEFORE rendering anything this._selectedTeamId = null; this._renderLeagueList(); // Update the UI to show the selected league // First, remove selected class from all league items const allLeagueItems = this.shadow.querySelectorAll('.list-item'); allLeagueItems.forEach(item => item.classList.remove('selected')); // Find and select the clicked league item const newSelectedElement = this.shadow.querySelector(`.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(); // 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(); //this.render(); // This was causing the full re-render and resetting the panel width } _createAndAppendLeagueActions(container, leagueId) { container.innerHTML = ''; // Clear any previous content // Create a button instead of dropdown select const menuButton = document.createElement('button'); menuButton.textContent = '...'; menuButton.classList.add('button-shared'); menuButton.title = 'More actions'; // Create the dropdown menu (initially hidden) const dropdownMenu = document.createElement('div'); dropdownMenu.classList.add('dropdown-menu'); dropdownMenu.style.display = 'none'; // Find the league object const league = this._leagues.find(l => (l._id || l.name) === leagueId); // Add action options const actions = [ { value: 'view', label: 'Table ↗️' }, { value: 'edit', label: 'Edit..' }, { value: 'teams', label: 'Teams..' }, ]; if (league && Array.isArray(league.teams) && league.teams.length >= 2) { actions.push({ value: 'reset', label: 'Reset..' }); } actions.push({ value: 'delete', label: 'Delete..' }); actions.forEach(action => { const menuItem = document.createElement('div'); menuItem.classList.add('dropdown-menu-item'); menuItem.textContent = action.label; menuItem.dataset.action = action.value; menuItem.addEventListener('click', (e) => { e.stopPropagation(); // Store the league ID for the action handlers this._currentLeagueIdForMenu = leagueId; // Handle the selected action switch (action.value) { case 'view': this._handleViewLeagueTable(); break; case 'edit': this._handleEditLeagueRules(); break; case 'delete': this._handleDeleteLeague(); break; case 'reset': this._handleResetLeague(); break; case 'teams': this._handleAddTeam(); break; } // Hide the menu after action this._hideGlobalLeagueMenu(); }); dropdownMenu.appendChild(menuItem); }); // Handle button click to show/hide menu menuButton.addEventListener('click', (e) => { e.stopPropagation(); // Check if this menu is already visible const isVisible = dropdownMenu.style.display === 'block'; if (isVisible) { // Hide this menu if it's currently visible dropdownMenu.style.display = 'none'; } else { // Hide any other open menus first this._hideGlobalLeagueMenu(); // Show this menu dropdownMenu.style.display = 'block'; // Position the menu using fixed positioning relative to the button const buttonRect = menuButton.getBoundingClientRect(); dropdownMenu.style.top = `${buttonRect.bottom + 2}px`; // 2px gap below button dropdownMenu.style.left = `${buttonRect.right - 120}px`; // Align right edge (120px is min-width) } }); // Store reference to menu for hiding menuButton._dropdownMenu = dropdownMenu; container.appendChild(menuButton); container.appendChild(dropdownMenu); // Add document click listener to hide menu when clicking outside if (!this._documentClickHandlerBound) { this._documentClickHandlerBound = this._handleDocumentClick.bind(this); document.addEventListener('click', this._documentClickHandlerBound); } } _showLeagueSpecificPanels() { const selectedLeague = this._getSelectedLeague(); if (!selectedLeague) { this._hideLeagueSpecificPanels(); return; } // --- Desktop Layout Adjustments --- // This logic is now responsible for showing the right panel and resizer // since render() is no longer called on selection change. if (!this._isMobile) { const detailsColumn = this.shadow.querySelector('.column-details'); const resizer = this.shadow.querySelector('#resizer'); if (detailsColumn) detailsColumn.style.display = ''; if (resizer) resizer.style.display = ''; // If the right panel was previously hidden, we need to reset the left panel // to its default flex state so the resizer can work. if (!this._rightPanelWasVisible) { const leftPanel = this.shadow.querySelector('.column-leagues'); if (leftPanel) { leftPanel.style.width = ''; leftPanel.style.flex = ''; } } this._rightPanelWasVisible = true; // Update state tracker } // Show the dashboard panel and update it const dashboardPanel = this.shadow.querySelector('#league-dashboard-panel'); if (dashboardPanel) { dashboardPanel.style.display = ''; this._updateDashboardPanel(selectedLeague); } else { console.warn(this.LOG_PREFIX + 'Dashboard panel element not found'); } // Show the teams panel const teamsPanel = this.shadow.querySelector('#teams-panel'); if (teamsPanel) { teamsPanel.setAttribute('data', selectedLeague); teamsPanel.style.display = ''; // Render the teams list to populate it with team data this._renderTeamsList(); // Scroll the teams list to the top when a new league is selected this._scrollTeamsListToTop(); } 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() { // --- Desktop Layout Adjustments --- // This logic now hides the right panel and resizer and expands the left panel. if (!this._isMobile) { const leftPanel = this.shadow.querySelector('.column-leagues'); const detailsColumn = this.shadow.querySelector('.column-details'); const resizer = this.shadow.querySelector('#resizer'); if (detailsColumn) detailsColumn.style.display = 'none'; if (resizer) resizer.style.display = 'none'; if (leftPanel) { leftPanel.style.width = '100%'; leftPanel.style.flex = '1 1 100%'; } this._rightPanelWasVisible = false; // Update state tracker } // Hide the dashboard panel const dashboardPanel = this.shadow.querySelector('#league-dashboard-panel'); if (dashboardPanel) { dashboardPanel.style.display = 'none'; } else { console.warn(this.LOG_PREFIX + 'Dashboard panel element not found when trying to hide'); } // 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'; // Reset the teams panel header const teamsPanelHeader = this.shadow.querySelector('#teams-panel .panel-header span'); if (teamsPanelHeader) { teamsPanelHeader.textContent = 'Teams'; } // 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'; } } } _updateButtonStates() { const isLeagueSelected = !!this._selectedLeagueId; // Update New button state const btnNew = this.shadow.querySelector('#new-league-button'); if (btnNew) { btnNew.disabled = this._isNewLeagueLoading || this._isCopyLeagueLoading; if (this._isNewLeagueLoading) { btnNew.innerHTML = '<span class="loading-spinner"></span> Creating...'; } else { btnNew.innerHTML = 'New'; } } // Update Copy button state const btnCopy = this.shadow.querySelector('#copy-league-button'); if (btnCopy) { btnCopy.disabled = !isLeagueSelected || this._isNewLeagueLoading || this._isCopyLeagueLoading; if (this._isCopyLeagueLoading) { btnCopy.innerHTML = '<span class="loading-spinner"></span> Copying...'; } else { btnCopy.innerHTML = 'Copy'; } } // Update Edit button state (if it exists) const btnEdit = this.shadow.querySelector('#edit-league-button'); if (btnEdit) btnEdit.disabled = !isLeagueSelected; // Update Add Team button state (if it exists) const btnAddTeam = this.shadow.querySelector('#add-team-button'); if (btnAddTeam) btnAddTeam.disabled = !isLeagueSelected; // Update View Table button state (if it exists) const btnViewTable = this.shadow.querySelector('#view-table-button'); if (btnViewTable) btnViewTable.disabled = !isLeagueSelected; } _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 || []; // Update the teams panel header to include the count const teamsPanelHeader = this.shadow.querySelector('#teams-panel .panel-header span'); if (teamsPanelHeader) { teamsPanelHeader.textContent = `Teams (${teams.length})`; } if (teams.length === 0) { const li = document.createElement('li'); li.classList.add('list-item'); li.textContent = 'No teams found.'; teamsList.appendChild(li); return; } teams.forEach(team => { const li = document.createElement('li'); li.classList.add('list-item'); // 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'); } const nameSpan = document.createElement('span'); nameSpan.classList.add('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('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); }); } _scrollTeamsListToTop() { // Find the teams list container that has scrolling const teamsListContainer = this.shadow.querySelector('#teams-panel .list-container'); if (teamsListContainer && typeof teamsListContainer.scrollTo === 'function') { // Scroll to the top smoothly teamsListContainer.scrollTo({ top: 0, behavior: 'smooth' }); } else { // Fallback: try to scroll the teams list directly const teamsList = this.shadow.querySelector('#teams-list'); if (teamsList && typeof teamsList.scrollTo === 'function') { teamsList.scrollTo({ top: 0, behavior: 'smooth' }); } } } _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'); 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()); 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()); } // Dashboard event listeners const btnEditLeague = this.shadow.querySelector('#edit-league-button'); if (btnEditLeague) { btnEditLeague.addEventListener('click', () => this._handleEditLeagueRules()); } const btnViewTable = this.shadow.querySelector('#view-table-button'); if (btnViewTable) { btnViewTable.addEventListener('click', () => this._handleViewLeagueTable()); } // 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; } /** * Hides/resets all league action dropdown menus to their default state * This function finds all dropdown menu elements and hides them */ _hideGlobalLeagueMenu() { // Find all dropdown menus in the shadow DOM const dropdownMenus = this.shadow.querySelectorAll('.dropdown-menu'); dropdownMenus.forEach(menu => { // Hide each dropdown menu menu.style.display = 'none'; }); // Clear the current league ID for menu context this._currentLeagueIdForMenu = null; } /** * Handle document clicks to close dropdown menus when clicking outside */ _handleDocumentClick(e) { // Check if the click was inside any dropdown menu or button const isInsideDropdown = e.target.closest('.dropdown-menu') || e.target.closest('.button-shared'); if (!isInsideDropdown) { this._hideGlobalLeagueMenu(); } } _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 );