UNPKG

@lovebowls/leagueelements

Version:

League Elements package for LoveBowls

755 lines (639 loc) 25.1 kB
// leagueTeams.js // Comprehensive League Teams Manager import { BASE_STYLES } from './leagueTeams-styles.js'; import * as Swal from 'sweetalert2'; import { sweetAlertGlobalStyles, sweetAlertMobileOverrides } from '../shared-styles.js'; class LeagueTeamsEvent extends CustomEvent { constructor(type, detail) { super(type, { detail, bubbles: true, composed: true }); } } class LeagueTeams extends HTMLElement { static _globalStylesInjected = false; static get observedAttributes() { return ['open', 'is-mobile', 'data', 'action', 'existing-teams']; } constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); // League data this._league = null; // Full league object this._workingLeague = null; // Working copy for modifications this._existingTeams = []; // Available teams from lovebowls // UI state this._open = false; this._isMobile = false; this._selectedTeamId = null; this._showEditor = false; this._editorMode = 'new'; // 'new' or 'edit' this._editingTeam = null; // Action configuration this._action = null; // {createTeam: boolean, editTeam: guid, fromNewLeagueWorkflow: boolean} // Error handling this._error = ''; // Workflow state this._showFixtureScheduler = true; // Default to checked // Event bindings this._boundOnKeydown = this._onKeydown.bind(this); } static get observedAttributes() { return ['open', 'is-mobile', 'data', 'action', 'existing-teams']; } // Properties /** * @param {Object} value - Full league object */ set data(value) { try { this._league = typeof value === 'string' ? JSON.parse(value) : value; this._workingLeague = this._league ? JSON.parse(JSON.stringify(this._league)) : null; } catch (error) { console.error('Failed to parse league data:', error); this._league = null; this._workingLeague = null; } this.render(); } get data() { return this._league; } /** * @param {Object} value - Action configuration {createTeam: boolean, editTeam: guid} */ set action(value) { try { this._action = typeof value === 'string' ? JSON.parse(value) : value; this._processAction(); } catch (error) { console.error('Failed to parse action:', error); this._action = null; } } get action() { return this._action; } /** * @param {Array} value - Array of available teams from lovebowls */ set existingTeams(value) { try { this._existingTeams = typeof value === 'string' ? JSON.parse(value) : (Array.isArray(value) ? value : []); } catch (error) { console.error('Failed to parse existing teams:', error); this._existingTeams = []; } this.render(); } get existingTeams() { return this._existingTeams; } /** * @returns {boolean} Whether we're in the new league workflow */ get _isNewLeagueWorkflow() { return this._action && this._action.fromNewLeagueWorkflow === true; } /** * @param {boolean} value */ set open(value) { const shouldRender = this._open !== !!value; this._open = !!value; this.setAttribute('open', this._open.toString()); if (shouldRender) { this.render(); } if (this._open) { this.shadowRoot.addEventListener('keydown', this._boundOnKeydown); } else { this.shadowRoot.removeEventListener('keydown', this._boundOnKeydown); } } get open() { return this._open; } /** * @param {boolean} value */ set isMobile(value) { const shouldRender = this._isMobile !== !!value; this._isMobile = !!value; if (shouldRender) { this.render(); } } get isMobile() { return this._isMobile; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue === newValue) return; switch (name) { case 'open': this.open = newValue === 'true'; break; case 'is-mobile': this.isMobile = newValue === 'true'; break; case 'data': this.data = newValue; break; case 'action': this.action = newValue; break; case 'existing-teams': this.existingTeams = newValue; break; } } connectedCallback() { this._injectGlobalSwalStyles(); this.render(); } _injectGlobalSwalStyles() { if (LeagueTeams._globalStylesInjected) return; const styleId = 'global-swal-mobile-styles-teams'; let existingStyleTag = document.getElementById(styleId); if (existingStyleTag) { LeagueTeams._globalStylesInjected = true; return; } const style = document.createElement('style'); style.id = styleId; style.textContent = `${sweetAlertGlobalStyles}\n\n${sweetAlertMobileOverrides}`; try { document.head.appendChild(style); } catch (err) { console.error('[LeagueTeams] Failed to append style tag to head:', err); } LeagueTeams._globalStylesInjected = true; } _processAction() { if (!this._action) return; if (this._action.createTeam) { this._showCreateTeam(); } else if (this._action.editTeam) { this._showEditTeam(this._action.editTeam); } } _showCreateTeam() { this._selectedTeamId = null; this._showEditor = true; this._editorMode = 'new'; this._editingTeam = null; this.render(); } _showEditTeam(teamId) { if (!this._workingLeague?.teams) return; const team = this._workingLeague.teams.find(t => t._id === teamId); if (!team) return; this._selectedTeamId = teamId; this._showEditor = true; this._editorMode = 'edit'; this._editingTeam = { ...team }; this.render(); } _hideEditor() { this._showEditor = false; this._editorMode = 'new'; this._editingTeam = null; this.clearError(); this.render(); } showError(msg) { this._error = msg || ''; const errorElement = this.shadow.querySelector('#teams-error'); if (errorElement) { errorElement.textContent = this._error; errorElement.style.display = this._error ? 'block' : 'none'; } } clearError() { this.showError(''); } render() { const mobileClass = this._isMobile ? 'mobile-view' : ''; const leagueName = this._workingLeague?.name || 'League'; const teams = this._workingLeague?.teams || []; this.shadow.innerHTML = ` <style> ${BASE_STYLES} </style> <div class="modal-shared-overlay" style="display: ${this._open ? 'flex' : 'none'};"> <div class="teams-manager-content ${mobileClass}"> <div class="teams-manager-header"> <h3>Manage Teams - ${leagueName}</h3> <button type="button" class="modal-close-button" id="close-teams-manager" aria-label="Close">&times;</button> </div> <div class="teams-manager-body"> <!-- Error Display --> <div id="teams-error" class="form-error-shared" style="display: none;"></div> <!-- Action Buttons --> <div class="teams-action-buttons ${this._showEditor ? 'disabled' : ''}"> <button type="button" class="button-shared button-primary" id="add-team-btn" ${this._showEditor ? 'disabled' : ''}>Add Team</button> </div> <!-- Teams List Panel --> <div class="teams-list-panel ${this._showEditor ? 'disabled' : ''}"> <h4>Teams in League (${teams.length})</h4> <div class="teams-list-container"> ${this._renderTeamsList(teams)} </div> </div> <!-- Team Editor Panel --> ${this._showEditor ? this._renderTeamEditor() : ''} </div> <div class="teams-manager-footer"> ${this._isNewLeagueWorkflow && teams.length >= 2 ? ` <div class="footer-options"> <label class="checkbox-label"> <input type="checkbox" id="show-fixture-scheduler" ${this._showFixtureScheduler ? 'checked' : ''}> Next: Fixture Scheduler </label> </div> ` : ''} <div class="footer-buttons"> <button type="button" class="button-shared" id="cancel-teams-manager" ${this._showEditor ? 'disabled' : ''}>Cancel</button> <button type="button" class="button-shared button-primary" id="save-teams-manager" ${this._showEditor ? 'disabled' : ''}>OK</button> </div> </div> </div> </div> `; this._attachEventListeners(); } _renderTeamsList(teams) { if (teams.length === 0) { return '<div class="no-teams-message">No teams in this league yet.</div>'; } return ` <ul class="teams-list"> ${teams.map(team => ` <li class="team-list-item ${this._selectedTeamId === team._id ? 'selected' : ''}" data-team-id="${team._id}"> <div class="team-info"> <span class="team-name">${team.name}</span> ${this._isLovebowlsTeam(team._id) ? '<small class="team-source">(LB)</small>' : ''} </div> ${this._selectedTeamId === team._id && !this._showEditor ? ` <div class="team-actions"> <button type="button" class="button-shared button-sm" data-action="edit" data-team-id="${team._id}">Edit</button> <button type="button" class="button-shared button-sm button-danger" data-action="remove" data-team-id="${team._id}">Remove</button> </div> ` : ''} </li> `).join('')} </ul> `; } _renderTeamEditor() { const title = this._editorMode === 'edit' ? 'Edit Team' : 'Add Team'; const currentTeamData = this._editingTeam || { _id: '', name: '' }; // Determine if it's a Lovebowls team const isLovebowlsTeam = this._isLovebowlsTeam(currentTeamData._id); // Set the checkbox label based on mode const checkboxLabel = (this._editorMode === 'edit' && !isLovebowlsTeam) ? "Replace with team from lovebowls.co.uk" : "Team from lovebowls"; // Filter out lovebowls teams that are already part of the league const filteredTeams = this._getFilteredLovebowlsTeams(); let optionsHtml = ''; if (filteredTeams && filteredTeams.length > 0) { optionsHtml = filteredTeams.map(team => `<option value="${team._id}" ${currentTeamData._id === team._id ? 'selected' : ''}>${team.name}</option>` ).join(''); } return ` <div class="team-editor-panel"> <div class="team-editor-header"> <h4>${title}</h4> </div> <div class="team-editor-body"> <div id="team-editor-error" class="form-error-shared" style="display: none;"></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(--le-text-color-error); margin-top: 0.5em;">All lovebowls teams are already in this league</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 ? '' : currentTeamData.name}" ${isLovebowlsTeam ? 'disabled' : ''}> </div> </div> <div class="team-editor-footer"> <button type="button" class="button-shared" id="cancel-team-editor">Cancel</button> <button type="button" class="button-shared button-update" id="update-team">Update</button> </div> </div> `; } _isLovebowlsTeam(teamId) { return this._existingTeams.some(lbTeam => lbTeam._id === teamId); } _getFilteredLovebowlsTeams() { if (!this._workingLeague?.teams) return this._existingTeams; // If in edit mode, don't filter out the team we're currently editing const teamIdsToExclude = this._editorMode === 'edit' ? this._workingLeague.teams.filter(t => t._id !== this._editingTeam?._id).map(t => t._id) : this._workingLeague.teams.map(t => t._id); return this._existingTeams.filter(team => !teamIdsToExclude.includes(team._id)); } _attachEventListeners() { // Main buttons const closeBtn = this.shadow.querySelector('#close-teams-manager'); const cancelBtn = this.shadow.querySelector('#cancel-teams-manager'); const saveBtn = this.shadow.querySelector('#save-teams-manager'); if (closeBtn) closeBtn.addEventListener('click', () => this._onCancel()); if (cancelBtn && !cancelBtn.disabled) cancelBtn.addEventListener('click', () => this._onCancel()); if (saveBtn && !saveBtn.disabled) saveBtn.addEventListener('click', () => this._onSave()); // Fixture scheduler checkbox (only in new league workflow) if (this._isNewLeagueWorkflow) { const fixtureSchedulerCheckbox = this.shadow.querySelector('#show-fixture-scheduler'); if (fixtureSchedulerCheckbox && !fixtureSchedulerCheckbox.disabled) { fixtureSchedulerCheckbox.addEventListener('change', (e) => { this._showFixtureScheduler = e.target.checked; }); } } // Action buttons const addTeamBtn = this.shadow.querySelector('#add-team-btn'); if (addTeamBtn && !this._showEditor) { addTeamBtn.addEventListener('click', () => this._handleAddTeam()); } // Team list clicks const teamItems = this.shadow.querySelectorAll('.team-list-item'); teamItems.forEach(item => { if (!this._showEditor) { item.addEventListener('click', (e) => { // Don't handle clicks on action buttons if (e.target.closest('.team-actions')) return; const teamId = e.currentTarget.dataset.teamId; this._handleTeamSelect(teamId); }); } }); // Team action button clicks const editButtons = this.shadow.querySelectorAll('.team-actions button[data-action="edit"]'); editButtons.forEach(button => { if (!this._showEditor) { button.addEventListener('click', (e) => { e.stopPropagation(); const teamId = e.target.dataset.teamId; this._handleEditTeam(teamId); }); } }); const removeButtons = this.shadow.querySelectorAll('.team-actions button[data-action="remove"]'); removeButtons.forEach(button => { if (!this._showEditor) { button.addEventListener('click', (e) => { e.stopPropagation(); const teamId = e.target.dataset.teamId; this._handleRemoveTeam(teamId); }); } }); // Editor panel event listeners if (this._showEditor) { this._attachEditorEventListeners(); } // Click outside to close const overlay = this.shadow.querySelector('.modal-shared-overlay'); if (overlay) { overlay.addEventListener('click', (e) => { if (e.target === overlay) { this._onCancel(); } }); } } _attachEditorEventListeners() { const cancelEditorBtn = this.shadow.querySelector('#cancel-team-editor'); const updateTeamBtn = this.shadow.querySelector('#update-team'); if (cancelEditorBtn) cancelEditorBtn.addEventListener('click', () => this._hideEditor()); if (updateTeamBtn) updateTeamBtn.addEventListener('click', () => this._handleUpdateTeam()); // Form field event listeners const useExistingTeamCheckbox = this.shadow.querySelector('#useExistingTeamCheckbox'); const existingTeamSelectGroup = this.shadow.querySelector('#existingTeamSelectGroup'); const newTeamNameGroup = this.shadow.querySelector('#newTeamNameGroup'); const teamNameInput = this.shadow.querySelector('#teamName'); const existingTeamSelect = this.shadow.querySelector('#existingTeamSelect'); if (useExistingTeamCheckbox && existingTeamSelectGroup && newTeamNameGroup && teamNameInput && existingTeamSelect) { useExistingTeamCheckbox.addEventListener('change', (e) => { const isChecked = e.target.checked; const filteredTeams = this._getFilteredLovebowlsTeams(); 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 = ''; } else { existingTeamSelect.value = ''; } } else { existingTeamSelectGroup.style.display = 'none'; newTeamNameGroup.style.display = 'block'; teamNameInput.disabled = false; if (isChecked && filteredTeams.length === 0) { useExistingTeamCheckbox.checked = false; } } }); // Initial state setup this._updateEditorFormState(); } } _updateEditorFormState() { const useExistingTeamCheckbox = this.shadow.querySelector('#useExistingTeamCheckbox'); const useExistingTeamLabel = this.shadow.querySelector('#useExistingTeamLabel'); if (!useExistingTeamCheckbox) return; const filteredTeams = this._getFilteredLovebowlsTeams(); if (!filteredTeams || filteredTeams.length === 0) { useExistingTeamCheckbox.disabled = true; const tooltip = this._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'; } } } _handleAddTeam() { this._showCreateTeam(); } _handleEditTeam(teamId) { this._showEditTeam(teamId); } _handleRemoveTeam(teamId) { if (!teamId || !this._workingLeague?.teams) return; const team = this._workingLeague.teams.find(t => t._id === teamId); if (!team) return; 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) { // Remove team from working league this._workingLeague.teams = this._workingLeague.teams.filter(t => t._id !== teamId); // Remove matches involving this team if (this._workingLeague.matches) { this._workingLeague.matches = this._workingLeague.matches.filter(match => match.homeTeam?._id !== teamId && match.awayTeam?._id !== teamId ); } // Clear selection if this was the selected team if (this._selectedTeamId === teamId) { this._selectedTeamId = null; this._hideEditor(); } this.render(); // Re-render to update the list Swal.default.fire({ customClass: this._getSwalCustomClasses(), title: 'Team Removed', text: `${team.name} has been removed from the league`, icon: 'success', timer: 2000, showConfirmButton: false }); } }); } _handleTeamSelect(teamId) { if (this._showEditor) { // Don't allow selection when editor is open return; } if (this._selectedTeamId === teamId) { // If already selected, deselect it this._selectedTeamId = null; } else { // Single-click behavior: select the team this._selectedTeamId = teamId; } this.render(); } _handleUpdateTeam() { this.clearError(); const useExistingTeamCheckbox = this.shadow.querySelector('#useExistingTeamCheckbox'); const existingTeamSelect = this.shadow.querySelector('#existingTeamSelect'); const teamNameInput = this.shadow.querySelector('#teamName'); let teamId = ''; let teamName = ''; if (useExistingTeamCheckbox && useExistingTeamCheckbox.checked && existingTeamSelect) { if (existingTeamSelect.value) { teamId = existingTeamSelect.value; const selectedTeam = this._existingTeams.find(t => t._id === teamId); teamName = selectedTeam ? selectedTeam.name : teamId; } else { this._showEditorError('Please select a team from the dropdown.'); return; } } else if (teamNameInput && teamNameInput.value.trim()) { teamName = teamNameInput.value.trim(); teamId = teamName; } else { this._showEditorError('Team Name is required, either by typing a new name or selecting an existing team.'); return; } if (!teamId || !teamName) { this._showEditorError('Team name is required.'); return; } // Check for duplicate team IDs in the working league const isDuplicate = this._workingLeague.teams.some(team => team._id === teamId && teamId !== this._editingTeam?._id ); if (isDuplicate) { this._showEditorError(`A team with identifier "${teamId}" already exists in this league.`); return; } const teamData = { _id: teamId, name: teamName }; // Update working league if (!this._workingLeague.teams) { this._workingLeague.teams = []; } if (this._editorMode === 'edit' && this._editingTeam) { const teamIndex = this._workingLeague.teams.findIndex(t => t._id === this._editingTeam._id); if (teamIndex !== -1) { // Update existing team const oldTeamId = this._editingTeam._id; this._workingLeague.teams[teamIndex] = teamData; // If team ID is changing, update match references if (oldTeamId !== teamId && this._workingLeague.matches) { this._workingLeague.matches = this._workingLeague.matches.map(match => { if (match.homeTeam?._id === oldTeamId) { return { ...match, homeTeam: { ...match.homeTeam, _id: teamId, name: teamName } }; } if (match.awayTeam?._id === oldTeamId) { return { ...match, awayTeam: { ...match.awayTeam, _id: teamId, name: teamName } }; } return match; }); } } } else { // Add new team this._workingLeague.teams.push(teamData); } this._selectedTeamId = teamId; this._hideEditor(); } _showEditorError(message) { const errorElement = this.shadow.querySelector('#team-editor-error'); if (errorElement) { errorElement.textContent = message; errorElement.style.display = 'block'; } } _getSwalCustomClasses() { const mobilePopup = this._isMobile ? 'lae-swal-popup-mobile' : ''; const mobileTitle = this._isMobile ? 'lae-swal-title-mobile' : ''; const mobileHtmlContainer = this._isMobile ? 'lae-swal-html-container-mobile' : ''; const mobileActions = this._isMobile ? 'lae-swal-actions-mobile' : ''; const mobileStyled = this._isMobile ? 'lae-swal-styled-mobile' : ''; return { popup: mobilePopup, title: mobileTitle, htmlContainer: mobileHtmlContainer, actions: mobileActions, confirmButton: mobileStyled, cancelButton: mobileStyled, }; } _onCancel() { this.dispatchEvent(new LeagueTeamsEvent('teams-cancel', {})); } _onSave() { const eventDetail = { league: this._workingLeague }; // Include fixture scheduler flag if we're in the new league workflow if (this._isNewLeagueWorkflow) { eventDetail.showFixtureScheduler = this._showFixtureScheduler; } this.dispatchEvent(new LeagueTeamsEvent('teams-save', eventDetail)); } _onKeydown(e) { if (e.key === 'Escape') { this._onCancel(); return; } } } // Register the custom element import { safeDefine } from '../../utils/elementRegistry.js'; safeDefine('league-teams', LeagueTeams); export default LeagueTeams;