/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {

// UNUSED EXPORTS: default

;// ./static/js/lib/validation.js
class Validation {
  /*
  isNullOrWhitespace(v) {
      let txt = JSON.stringify(v).replace('/\s\g', '');
      return (txt == '' || 'null');
  }
  */

  static isEmpty(object) {
    let isEmpty = true;
    if (object !== null && object !== "null" && object !== undefined && object !== "undefined") {
      if (object.length == undefined) {
        isEmpty = false; // object exists but isn't a collection
      } else if (typeof object === "function") {
        isEmpty = false; // object is reference
      } else {
        // string or collection

        let isString = typeof object == "string";
        if (isString) object = object.trim();
        if (object.length > 0) {
          if (isString) {
            isEmpty = false; // String greater than length 0
          } else {
            if (typeof object[0] != "string") {
              isEmpty = false;
            } else {
              for (let i = 0; i < object.length; i++) {
                if (object[i] != "") {
                  isEmpty = false;
                  break;
                }
              }
            }
          }
        }
      }
    }
    return isEmpty;
  }
  static isValidNumber(value, positiveOnly) {
    return !Validation.isEmpty(value) && !isNaN(value) && (!positiveOnly || parseFloat(value) > 0);
  }
  static getDataContentType(params) {
    var data = null;
    var contentType = '';
    if (!Validation.isEmpty(params)) {
      if (typeof params === "string") {
        data = params;
        contentType = "application/x-www-form-urlencoded; charset=UTF-8";
      } else {
        data = JSON.stringify(params);
        contentType = "application/json; charset=UTF-8";
      }
    }
    return {
      Data: data,
      ContentType: contentType
    };
  }
  static arrayContainsItem(array, itemValue) {
    var hasItem = false;
    if (!Validation.isEmpty(array) && !Validation.isEmpty(itemValue)) {
      var isJQueryElementArray = array[0] instanceof jQuery;
      if (isJQueryElementArray) {
        for (let i = 0; i < array.length; i++) {
          if (document.querySelectorAll(array[i]).is(itemValue)) {
            hasItem = true;
            break;
          }
        }
      } else {
        var isDate = array[0] instanceof Date;
        if (isDate) {
          for (let i = 0; i < array.length; i++) {
            if (array[i].getTime() === itemValue.getTime()) {
              hasItem = true;
              break;
            }
          }
        } else {
          for (let i = 0; i < array.length; i++) {
            if (array[i] == itemValue) {
              hasItem = true;
              break;
            }
          }
        }
      }
    }
    return hasItem;
  }
  static dictHasKey(d, k) {
    return k in d;
  }
  static areEqualDicts(dict1, dict2) {
    const keys1 = Object.keys(dict1);
    const keys2 = Object.keys(dict2);
    if (keys1.length !== keys2.length) {
      return false;
    }
    for (let key of keys1) {
      if (dict1[key] !== dict2[key]) {
        return false;
      }
    }
    return true;
  }
  static imageExists(url, callback) {
    var img = new Image();
    img.onload = function () {
      callback(true);
    };
    img.onerror = function () {
      callback(false);
    };
    img.src = url;
  }
  static toFixedOrDefault(value, decimalPlaces, defaultValue = null) {
    return Validation.isValidNumber(value) ? parseFloat(value).toFixed(decimalPlaces) : defaultValue;
  }
}
;// ./static/js/dom.js


class DOM {
  static setElementAttributesValuesCurrentAndPrevious(element, data) {
    DOM.setElementAttributeValueCurrent(element, data);
    DOM.setElementAttributeValuePrevious(element, data);
  }
  static setElementAttributeValueCurrent(element, data) {
    element.setAttribute(attrValueCurrent, data);
  }
  static setElementAttributeValuePrevious(element, data) {
    element.setAttribute(attrValuePrevious, data);
  }
  static setElementValuesCurrentAndPrevious(element, data) {
    DOM.setElementValueCurrent(element, data);
    DOM.setElementAttributeValuePrevious(element, data);
  }
  static setElementValueCurrent(element, data) {
    DOM.setElementAttributeValueCurrent(element, data);
    let tagName = element.tagName.toUpperCase();
    if (element.type === "checkbox") {
      element.checked = data;
    } else if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') {
      element.value = data;
    } else {
      element.textContent = data;
    }
  }
  static setElementValueCurrentIfEmpty(element, data) {
    if (Validation.isEmpty(DOM.getElementValueCurrent(element))) {
      DOM.setElementValueCurrent(element, data);
    }
  }
  static getCellFromElement(element) {
    return element.closest('td');
  }
  static getRowFromElement(element, flagRow) {
    let selector = Validation.isEmpty(flagRow) ? 'tr' : 'tr.' + flagRow;
    return element.closest(selector);
  }
  static getClosestParent(element, parentSelector) {
    let parent = element.parentElement;
    while (parent) {
      if (parent.matches(parentSelector)) {
        return parent;
      }
      parent = parent.parentElement;
    }
    return null;
  }
  static convertForm2JSON(elementForm) {
    let dataForm = {};
    if (Validation.isEmpty(elementForm)) {
      return dataForm;
    }
    let containersFilter = elementForm.querySelectorAll('.' + flagContainerInput + '.' + flagFilter);
    let containerFilter, labelFilter, keyFilter, filter;
    for (let indexFilter = 0; indexFilter < containersFilter.length; indexFilter++) {
      containerFilter = containersFilter[indexFilter];
      labelFilter = containerFilter.querySelector('label');
      keyFilter = labelFilter.getAttribute('for');
      filter = containerFilter.querySelector(`#${keyFilter}`);
      dataForm[keyFilter] = DOM.getElementValueCurrent(filter);
    }
    return dataForm;
  }
  static loadPageBody(contentNew) {
    let pageBody = document.querySelector(idPageBody);
    pageBody.innerHTML = contentNew;
  }
  static getHashPageCurrent() {
    const hashPageCurrent = document.body.dataset.page;
    return hashPageCurrent;
  }
  static updateAndCheckIsElementDirty(element) {
    element.setAttribute(attrValueCurrent, DOM.getElementValueCurrent(element));
    return DOM.isElementDirty(element);
  }
  static isElementDirty(element) {
    let isDirty = element.getAttribute(attrValuePrevious) != element.getAttribute(attrValueCurrent);
    DOM.handleDirtyElement(element, isDirty);
    return isDirty;
  }
  static handleDirtyElement(element, isDirty) {
    DOM.toggleElementHasClassnameFlag(element, isDirty, flagDirty);
  }
  static toggleElementHasClassnameFlag(element, elementHasFlag, flag) {
    let elementAlreadyHasFlag = element.classList.contains(flag);
    if (elementHasFlag == elementAlreadyHasFlag) return;
    if (elementHasFlag) {
      element.classList.add(flag);
    } else {
      element.classList.remove(flag);
    }
  }
  static hasDirtyChildrenContainer(container) {
    if (container == null) return false;
    return container.querySelector('.' + flagDirty) != null;
  }
  static hasDirtyChildrenNotDeletedContainer(container) {
    if (container == null || container.classList.contains(flagDelete)) return false;
    return container.querySelector('.' + flagDirty + ':not(.' + flagDelete + ', .' + flagDelete + ' *)') != null;
  }
  static getElementValueCurrent(element) {
    let returnVal = '';
    if (!Validation.isEmpty(element)) {
      let tagName = element.tagName.toUpperCase();
      if (element.type === "checkbox") {
        returnVal = element.checked;
      }
      /*
      else if (element.classList.contains(flagIsDatePicker)) {
          returnVal = getDatePickerDate(element, adjust4DayLightSavings);
      }
      */else if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT') {
        returnVal = element.value;
      } else if (element.classList.contains(flagButton) && element.classList.contains(flagActive)) {
        // tagName === 'BUTTON'
        returnVal = element.classList.contains(flagDelete);
      } else if (tagName === 'TD') {
        returnVal = DOM.getElementAttributeValueCurrent(element);
      } else if (tagName == 'SVG' && element.classList.contains(flagCheckbox)) {
        returnVal = element.classList.contains(flagIsChecked);
      } else {
        returnVal = element.textContent;
      }
    }
    if (Validation.isEmpty(returnVal)) returnVal = '';
    return returnVal;
  }
  static getElementAttributeValueCurrent(element) {
    if (Validation.isEmpty(element)) return null;
    return element.getAttribute(attrValueCurrent);
  }
  static getElementAttributeValuePrevious(element) {
    if (Validation.isEmpty(element)) return null;
    return element.getAttribute(attrValuePrevious);
  }
  /* base_table.handleChangeElementCellTable
  static updateAndCheckIsTableElementDirty(element) {
      let wasDirty = DOM.isElementDirty(element);
      let row = DOM.getRowFromElement(element);
      let wasDirtyRow = DOM.hasDirtyChildrenNotDeletedContainer(row);
      let isDirty = DOM.updateAndCheckIsElementDirty(element);
      let cell = DOM.getCellFromElement(element);
      Utils.consoleLogIfNotProductionEnvironment({element, row, cell, isDirty, wasDirty});
      if (isDirty != wasDirty) {
          DOM.handleDirtyElement(cell, isDirty);
          let isDirtyRow = DOM.hasDirtyChildrenNotDeletedContainer(row);
          Utils.consoleLogIfNotProductionEnvironment({isDirtyRow, wasDirtyRow});
          if (isDirtyRow != wasDirtyRow) {
              DOM.handleDirtyElement(row, isDirtyRow);
          }
      }
  }
  */
  static scrollToElement(parent, element) {
    // REQUIRED: parent has scroll-bar
    parent.scrollTop(parent.scrollTop() + (element.offset().top - parent.offset().top));
  }
  static isElementInContainer(container, element) {
    if (typeof jQuery === 'function') {
      if (container instanceof jQuery) container = container[0];
      if (element instanceof jQuery) element = element[0];
    }
    var containerBounds = container.getBoundingClientRect();
    var elementBounds = element.getBoundingClientRect();
    return containerBounds.top <= elementBounds.top && containerBounds.left <= elementBounds.left && elementBounds.top + elementBounds.height <= containerBounds.top + containerBounds.height && elementBounds.left + elementBounds.width <= containerBounds.left + containerBounds.width;
  }
  static alertError(errorType, errorText) {
    alert(errorType + '\n' + errorText);
  }
  static createOptionUnselectedProductVariation() {
    return {
      [flagProductVariationType]: {
        [flagNameAttrOptionText]: [flagName],
        [flagNameAttrOptionValue]: [attrIdProductVariationType],
        [flagName]: 'Select Variation Type',
        [attrIdProductVariationType]: 0
      },
      [flagProductVariation]: {
        [flagNameAttrOptionText]: [flagName],
        [flagNameAttrOptionValue]: [attrIdProductVariation],
        [flagName]: 'Select Variation',
        [attrIdProductVariation]: 0
      }
    };
  }
  static createOption(optionJson) {
    if (Validation.isEmpty(optionJson)) optionJson = {
      text: 'Select',
      value: 0
    };
    let option = document.createElement('option');
    option.value = optionJson.value;
    option.textContent = optionJson.text;
    option.selected = optionJson.selected;
    return option;
  }
  static escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
  static unescapeHtml(html) {
    const div = document.createElement('div');
    div.innerHTML = html;
    return div.textContent || div.innerText || '';
  }
}
;// ./static/js/api.js

class API {
  static getCsrfToken() {
    return document.querySelector(idCSRFToken).getAttribute('content');
  }
  static async request(hashEndpoint, method = 'GET', data = null, params = null) {
    const url = API.getUrlFromHash(hashEndpoint, params);
    const csrfToken = API.getCsrfToken();
    const options = {
      method,
      headers: {
        'Content-Type': 'application/json',
        [flagCsrfToken]: csrfToken
      }
    };
    if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      data = {
        ...data,
        [flagCsrfToken]: csrfToken
      };
      options.body = JSON.stringify(data);
    }
    try {
      const response = await fetch(url, options);
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return await response.json();
    } catch (error) {
      console.error('API request failed:', error);
      throw error;
    }
  }
  static getUrlFromHash(hash, params = null) {
    if (hash == null) hash = hashPageHome;
    let url = API.parameteriseUrl(_pathHost + hash, params);
    return url;
  }
  static parameteriseUrl(url, params) {
    if (params) {
      url += '?' + new URLSearchParams(params).toString();
    }
    return url;
  }
  static goToUrl(url) {
    window.location.href = url;
  }
  static goToHash(hash, params = null) {
    const url = API.getUrlFromHash(hash, params);
    API.goToUrl(url);
  }

  // specific api calls
  /* Example:
  getUsers: () => request('/users'),
  getUserById: (id) => request(`/users/${id}`),
  createUser: (userData) => request('/users', 'POST', userData),
  updateUser: (id, userData) => request(`/users/${id}`, 'PUT', userData),
  deleteUser: (id) => request(`/users/${id}`, 'DELETE'),
  */

  // User
  // user
  static async loginUser() {
    let callback = {};
    callback[flagCallback] = DOM.getHashPageCurrent();
    return await API.request(hashPageUserLogin, 'POST', callback);
  }
  static async saveUsers(users, formFilters, comment) {
    let dataRequest = {};
    dataRequest[flagFormFilters] = DOM.convertForm2JSON(formFilters);
    dataRequest[flagUser] = users;
    dataRequest[flagComment] = comment;
    return await API.request(hashSaveUserUser, 'POST', dataRequest);
  }

  // MTG Game API methods
  static async getIsGameDataOutdated(game) {
    const url = `/mtg/api/game/${game[attrGameId]}/is-outdated`;
    let dataRequest = {};
    dataRequest[flagGame] = game;
    return await API.request(url, 'POST', dataRequest);
  }
  static async saveGame(game, formFilters, comment) {
    let dataRequest = {};
    dataRequest[flagFormFilters] = DOM.convertForm2JSON(formFilters);
    dataRequest[flagGame] = game;
    dataRequest[flagComment] = comment;
    return await API.request(hashSaveGame, 'POST', dataRequest);
  }
  static async getGamePlayers(gameId) {
    const url = `/mtg/api/game/${gameId}/players`;
    return await API.request(url, 'GET');
  }
  static async saveGamePlayers(players, formFilters, comment) {
    let dataRequest = {};
    dataRequest[flagFormFilters] = DOM.convertForm2JSON(formFilters);
    dataRequest[flagPlayer] = players;
    dataRequest[flagComment] = comment;
    return await API.request(hashSaveGamePlayer, 'POST', dataRequest);
  }
  static async getGameRounds(gameId) {
    const url = `/mtg/api/game/${gameId}/rounds`;
    return await API.request(url, 'GET');
  }
  static async getGameDamageRecords(gameId) {
    const url = `/mtg/api/game/${gameId}/damage-records`;
    return await API.request(url, 'GET');
  }
  static async saveGameRoundPlayerDamages(rounds, damages, formFilters, comment) {
    let dataRequest = {};
    dataRequest[flagFormFilters] = DOM.convertForm2JSON(formFilters);
    dataRequest[flagDamage] = damages;
    dataRequest[flagRound] = rounds;
    dataRequest[flagComment] = comment;
    return await API.request(hashSaveGameRoundPlayerDamage, 'POST', dataRequest);
  }
}
;// ./static/js/lib/business_objects/business_objects.js

class BusinessObjects {
  static getOptionJsonFromObjectJsonAndKeys(objectJson, keyText, keyValue, valueSelected = null) {
    return {
      text: objectJson[keyText],
      value: objectJson[keyValue],
      selected: objectJson[keyValue] == valueSelected
    };
  }
  static getOptionJsonFromObjectJson(objectJson, valueSelected = null) {
    let keyText = objectJson[flagNameAttrOptionText];
    let keyValue = objectJson[flagNameAttrOptionValue];
    // Utils.consoleLogIfNotProductionEnvironment({objectJson, keyText, keyValue});
    return BusinessObjects.getOptionJsonFromObjectJsonAndKeys(objectJson, keyText, keyValue, valueSelected);
  }
  static getObjectText(objectJson) {
    return objectJson == null ? '' : objectJson[objectJson[flagNameAttrOptionText]];
  }
  static getListObjectsFromIdDictAndCsv(idDict, idCsv) {
    let listObjects = [];
    let ids = idCsv.split(',');
    for (let id of ids) {
      listObjects.push(idDict[id]);
    }
    return listObjects;
  }
}
;// ./static/js/lib/events.js
class Events {
  static initialiseEventHandler(selectorElement, classInitialised, eventHandler) {
    document.querySelectorAll(selectorElement).forEach(function (element) {
      if (element.classList.contains(classInitialised)) return;
      eventHandler(element);
      element.classList.add(classInitialised);
    });
  }
  static hookupEventHandler(eventType, selector, callback) {
    Events.initialiseEventHandler(selector, flagInitialised, element => {
      element.addEventListener(eventType, event => {
        event.stopPropagation();
        callback(event, element);
      });
    });
  }
}
;// ./static/js/lib/local_storage.js

class LocalStorage {
  /*
  function getPageLocalStorage(pageHash) {
  
      let ls;
      try {
          ls = JSON.parse(localStorage.getItem(pageHash));
      } catch {
  
      }
  
      if (Validation.isEmpty(ls)) return {}
  
      return ls;
  }
  function getPageLocalStorageCurrent() {
  
      return JSON.parse(localStorage.getItem(hashPageCurrent));
  }
  
  function setPageLocalStorage(pageHash, newLS) {
  
      localStorage.setItem(pageHash, JSON.stringify(newLS));
  }
  
  function clearPageLocalStorage(pageHash) {
      localStorage.removeItem(pageHash);
  }
  
  function setupPageLocalStorage(pageHash) {
  
      let ls = getPageLocalStorage(pageHash);
  
      if (Validation.isEmpty(ls)) ls = {};
  
      setPageLocalStorage(pageHash, ls);
  }
  */

  static getLocalStorage(key) {
    return JSON.parse(localStorage.getItem(key));
  }
  static setLocalStorage(key, newLS) {
    localStorage.setItem(key, JSON.stringify(newLS));
  }

  /*
  function setupPageLocalStorageNext(pageHashNext) {
      let lsOld = getPageLocalStorage(hashPageCurrent);
      hashPageCurrent = pageHashNext;
      clearPageLocalStorage(hashPageCurrent);
      setupPageLocalStorage(hashPageCurrent);
      let lsNew = getPageLocalStorage(hashPageCurrent);
      lsNew[keyBasket] = (keyBasket in lsOld) ? lsOld[keyBasket] : {'items': []};
      setPageLocalStorage(hashPageCurrent, lsNew);
  }
  */
}
;// ./static/js/lib/utils.js
// Utility functions
/*
function $(selector) {
    return document.querySelector(selector);
}

function $$(selector) {
    return document.querySelectorAll(selector);
}
*/
class utils_Utils {
  static getListFromDict(dict) {
    let list = [];
    for (let key in dict) {
      list.push(dict[key]);
    }
    return list;
  }
  static consoleLogIfNotProductionEnvironment(message) {
    if (environment.is_production != "true") {
      console.log(message);
    }
  }
}
;// ./static/js/components/common/temporary/overlay_confirm.js

class OverlayConfirm {
  static hookup(callbackSuccess) {
    Events.initialiseEventHandler(idOverlayConfirm + ' button.' + flagCancel, flagInitialised, buttonCancel => {
      buttonCancel.addEventListener('click', () => {
        let overlay = document.querySelector(idOverlayConfirm);
        overlay.style.visibility = 'hidden';
      });
    });
    Events.initialiseEventHandler(idOverlayConfirm + ' button.' + flagSubmit, flagInitialised, buttonConfirm => {
      buttonConfirm.addEventListener('click', () => {
        let overlay = document.querySelector(idOverlayConfirm);
        let textarea = overlay.querySelector('textarea');
        overlay.style.visibility = 'hidden';
        callbackSuccess(textarea.value);
      });
    });
  }
  static show() {
    let overlay = document.querySelector(idOverlayConfirm);
    overlay.classList.remove(flagIsCollapsed);
    overlay.style.visibility = 'visible';
  }
}
;// ./static/js/pages/base.js









class BasePage {
  constructor(router) {
    if (!router) {
      throw new Error("Router is required");
    } else {
      utils_Utils.consoleLogIfNotProductionEnvironment("initialising with router: ", router);
    }
    this.router = router;
    this.title = titlePageCurrent;
    if (this.constructor === BasePage) {
      throw new Error("Cannot instantiate abstract class");
    }
    if (!this.constructor.hash) {
      throw new Error(`Class ${this.constructor.name} must have a static hash attribute.`);
    }
  }
  initialize() {
    throw new Error("Method 'initialize()' must be implemented.");
  }
  sharedInitialize() {
    this.logInitialisation();
    this.hookupCommonElements();
  }
  logInitialisation() {
    utils_Utils.consoleLogIfNotProductionEnvironment('Initialising ' + this.title + ' page');
  }
  hookupCommonElements() {
    // hookupVideos();
    this.hookupLogos();
    this.hookupNavigation();
    this.hookupOverlays();
  }
  hookupLogos() {
    Events.hookupEventHandler("click", "." + flagImageLogo + "," + "." + flagLogo, (event, element) => {
      utils_Utils.consoleLogIfNotProductionEnvironment('clicking logo');
      this.router.navigateToHash(hashPageHome);
    });
  }
  /*
  hookupEventHandler(eventType, selector, callback) {
      Events.initialiseEventHandler(selector, flagInitialised, (element) => {
          element.addEventListener(eventType, (event) => {
              event.stopPropagation();
              callback(event, element);
          });
      });
  }
  */
  hookupNavigation() {
    Events.hookupEventHandler("click", idButtonHamburger, (event, element) => {
      let overlayHamburger = document.querySelector(idOverlayHamburger);
      if (overlayHamburger.classList.contains(flagIsCollapsed)) {
        overlayHamburger.classList.remove(flagIsCollapsed);
        overlayHamburger.classList.add(flagExpanded);
      } else {
        overlayHamburger.classList.remove(flagExpanded);
        overlayHamburger.classList.add(flagIsCollapsed);
      }
    });
    this.hookupButtonsNavUserAccount();
    this.hookupButtonsNavUserLogout();
    this.hookupButtonsNavUserLogin();
  }
  hookupButtonsNav(buttonSelector) {
    Events.hookupEventHandler("click", buttonSelector, (event, button) => {
      let pageHash = buttonSelector.getAttribute('href');
      this.router.navigateToHash(pageHash);
    });
  }
  hookupButtonsNavUserAccount() {
    // this.hookupButtonsNav('.' + flagNavUserAccount);
  }
  hookupButtonsNavUserLogout() {
    // this.hookupButtonsNav('.' + flagNavUserLogout);
  }
  hookupButtonsNavUserLogin() {
    Events.hookupEventHandler("click", '.' + flagNavUserLogin, (event, navigator) => {
      event.preventDefault();
      event.stopPropagation();
      this.leave();
      API.loginUser().then(response => {
        if (response.Success) {
          window.location.href = response[flagCallback];
        } else {
          DOM.alertError("Error", response.Message);
        }
      });
    });
  }
  hookupOverlays() {
    this.hookupOverlayFromId(idOverlayConfirm);
    this.hookupOverlayFromId(idOverlayError);
  }
  hookupOverlayFromId(idOverlay) {
    Events.initialiseEventHandler(idOverlay, flagInitialised, overlay => {
      overlay.querySelector('button.' + flagCancel).addEventListener("click", event => {
        event.stopPropagation();
        overlay.style.display = 'none';
      });
    });
  }
  hookupButtonSave() {
    Events.initialiseEventHandler('.' + flagContainer + '.' + flagSave + '.' + flagCancel + ' button.' + flagSave, flagInitialised, button => {
      button.addEventListener("click", event => {
        event.stopPropagation();
        button = event.target;
        if (button.classList.contains(flagIsCollapsed)) return;
        utils_Utils.consoleLogIfNotProductionEnvironment('saving page: ', this.title);
        OverlayConfirm.show();
      });
    });
  }
  leave() {
    utils_Utils.consoleLogIfNotProductionEnvironment('Leaving ' + this.title + ' page');
    if (this.constructor === BasePage) {
      throw new Error("Must implement leave() method.");
    }
  }
  setLocalStoragePage(dataPage) {
    LocalStorage.setLocalStorage(this.hash, dataPage);
  }
  getLocalStoragePage() {
    return LocalStorage.getLocalStorage(this.hash);
  }
  toggleShowButtonsSaveCancel(show, buttonContainerSelector = null) {
    // , buttonSave = null, buttonCancel = null
    if (Validation.isEmpty(buttonContainerSelector)) buttonContainerSelector = '.' + flagContainer + '.' + flagSave + '.' + flagCancel;
    let buttonSave = document.querySelector(buttonContainerSelector + ' ' + idButtonSave);
    if (buttonSave == null) return;
    let buttonCancel = document.querySelector(buttonContainerSelector + ' ' + idButtonCancel);
    utils_Utils.consoleLogIfNotProductionEnvironment({
      show,
      buttonContainerSelector,
      buttonCancel,
      buttonSave
    });
    if (show) {
      buttonCancel.classList.remove(flagIsCollapsed);
      buttonSave.classList.remove(flagIsCollapsed);
      utils_Utils.consoleLogIfNotProductionEnvironment('showing buttons');
    } else {
      buttonCancel.classList.add(flagIsCollapsed);
      buttonSave.classList.add(flagIsCollapsed);
      utils_Utils.consoleLogIfNotProductionEnvironment('hiding buttons');
    }
  }
  static isDirtyFilter(filter) {
    let isDirty = DOM.updateAndCheckIsElementDirty(filter);
    if (isDirty) document.querySelectorAll(idTableMain + ' tbody tr').remove();
    return isDirty;
  }
}
;// ./static/js/components/common/temporary/overlay_error.js

class OverlayError {
  static hookup() {
    Events.initialiseEventHandler(idOverlayError + ' button.' + flagCancel, flagInitialised, buttonCancel => {
      buttonCancel.addEventListener('click', () => {
        let overlay = document.querySelector(idOverlayError);
        overlay.style.visibility = 'hidden';
      });
    });
  }
  static show(msgError) {
    let overlay = document.querySelector(idOverlayError);
    let labelError = overlay.querySelector(idLabelError);
    labelError.innerText = msgError;
    overlay.style.visibility = 'visible';
  }
}
;// ./static/js/pages/base_table.js










class TableBasePage extends BasePage {
  // static hash
  // static attrIdRowObject
  // callSaveTableContent

  constructor(router) {
    super(router);
    this.cursorYInitial = null;
    this.rowInitial = null;
    this.placeholder = null;
    this.dragSrcEl = null;
    this.dragSrcRow = null;
    this.hookupTableCellDdls = this.hookupTableCellDdls.bind(this);
  }
  initialize(isPopState = false) {
    throw new Error("Must implement initialize() method.");
  }
  sharedInitialize(isPopState = false, isSinglePageApp = false) {
    if (!isPopState) {
      super.sharedInitialize();
      this.hookupFilters();
      this.hookupButtonsSaveCancel();
      this.hookupTableMain();
      OverlayConfirm.hookup(() => {
        if (isSinglePageApp) {
          this.saveRecordsTableDirtySinglePageApp();
        } else {
          this.saveRecordsTableDirty();
        }
      });
    } else {
      let dataPage = this.getLocalStoragePage();
      let filters = dataPage[flagFormFilters];
      let formFilters = TableBasePage.getFormFilters();
      let filtersDefault = DOM.convertForm2JSON(formFilters);
      if (!Validation.areEqualDicts(filters, filtersDefault)) {
        this.callFilterTableContent();
      }
    }
  }
  hookupFilters() {
    if (this.constructor === TableBasePage) {
      throw new Error("Subclass of TableBasePage must implement method hookupFilters().");
    }
  }
  sharedHookupFilters() {
    this.hookupButtonApplyFilters();
    this.hookupSearchTextFilter();
  }
  hookupFilterActive() {
    let filterSelector = idFormFilters + ' #' + flagActiveOnly;
    let filterActiveOld = document.querySelector(filterSelector);
    filterActiveOld.removeAttribute('id');
    let parentDiv = filterActiveOld.parentElement;
    let isChecked = DOM.getElementAttributeValuePrevious(parentDiv) == "True";
    let filterActiveNew = document.querySelector(idFormFilters + ' div.' + flagActiveOnly + '.' + flagContainerInput + ' svg.' + flagActiveOnly);
    filterActiveNew.setAttribute('id', flagActiveOnly);
    if (isChecked) filterActiveNew.classList.add(flagIsChecked);
    Events.hookupEventHandler("click", filterSelector, (event, filterActive) => {
      utils_Utils.consoleLogIfNotProductionEnvironment({
        filterActive
      });
      utils_Utils.consoleLogIfNotProductionEnvironment({
        [filterActive.tagName]: filterActive.tagName
      });
      let svgElement = filterActive.tagName.toUpperCase() == 'SVG' ? filterActive : filterActive.parentElement;
      let wasChecked = svgElement.classList.contains(flagIsChecked);
      if (wasChecked) {
        svgElement.classList.remove(flagIsChecked);
      } else {
        svgElement.classList.add(flagIsChecked);
      }
      return this.handleChangeFilter(event, filterActive);
    });
    let filter = document.querySelector(filterSelector);
    let filterValuePrevious = DOM.getElementValueCurrent(filter);
    filter.setAttribute(attrValueCurrent, filterValuePrevious);
    filter.setAttribute(attrValuePrevious, filterValuePrevious);
  }
  hookupFilter(filterFlag, handler = (event, filter) => {
    return this.handleChangeFilter(event, filter);
  }) {
    let filterSelector = idFormFilters + ' #' + filterFlag;
    Events.hookupEventHandler("change", filterSelector, handler);
    let filter = document.querySelector(filterSelector);
    let filterValuePrevious = DOM.getElementValueCurrent(filter);
    filter.setAttribute(attrValueCurrent, filterValuePrevious);
    filter.setAttribute(attrValuePrevious, filterValuePrevious);
  }
  handleChangeFilter(event, filter) {
    let isDirtyFilter = DOM.updateAndCheckIsElementDirty(filter);
    let formFilters = TableBasePage.getFormFilters();
    let areDirtyFilters = isDirtyFilter || DOM.hasDirtyChildrenContainer(formFilters);
    let tbody = document.querySelector(idTableMain + ' tbody');
    let rows = tbody.querySelectorAll(':scope > tr');
    rows.forEach(row => {
      if (areDirtyFilters && !row.classList.contains(flagIsCollapsed)) row.classList.add(flagIsCollapsed);
      if (!areDirtyFilters && row.classList.contains(flagIsCollapsed)) {
        row.classList.remove(flagIsCollapsed);
        let dirtyInputs = row.querySelectorAll('input.' + flagDirty);
        dirtyInputs.forEach(dirtyInput => {
          dirtyInput.value = DOM.getElementAttributeValueCurrent(dirtyInput);
        });
      }
    });
    if (areDirtyFilters) {
      /*
      tbody.querySelectorAll('tr').forEach((tr) => { 
          if (!DOM.hasDirtyChildrenContainer(tr)) tr.remove(); 
      });
      */
      tbody.innerHTML = '<div>Press "Apply Filters" to refresh the table.</div>' + tbody.innerHTML;
      if (!tbody.classList.contains(flagIsCollapsed)) tbody.classList.add(flagIsCollapsed);
    } else {
      let isDirtyLabel = tbody.querySelector(":scope > div");
      if (isDirtyLabel != null) isDirtyLabel.remove();
      if (tbody.classList.contains(flagIsCollapsed)) tbody.classList.remove(flagIsCollapsed);
      let initialisedElements = tbody.querySelectorAll('.' + flagInitialised);
      initialisedElements.forEach(initialisedElement => {
        initialisedElement.classList.remove(flagInitialised);
      });
      this.hookupTableMain();
    }
    this.updateAndToggleShowButtonsSaveCancel();
  }
  hookupFilterIsNotEmpty() {
    this.hookupFilter(flagIsNotEmpty);
  }
  hookupButtonApplyFilters() {
    Events.hookupEventHandler("click", idButtonApplyFilters, (event, button) => {
      event.stopPropagation();
      this.callFilterTableContent();
    });
  }
  hookupSearchTextFilter() {
    this.hookupFilter(flagSearch);
  }
  hookupFilterCommandCategory() {
    this.hookupFilter(attrIdCommandCategory, (event, filterCommandCategory) => {
      this.handleChangeFilter();
      let isDirtyFilter = filterCommandCategory.classList.contains(flagDirty);
      let idCommandCategory = DOM.getElementValueCurrent(filterCommandCategory);
      console.log("filter commands unsorted");
      console.log(utils_Utils.getListFromDict(filterCommands));
      let commandsInCategory = utils_Utils.getListFromDict(filterCommands).filter(command => command[attrIdCommandCategory] == idCommandCategory);
      let sortedCommands = commandsInCategory.sort((a, b) => a[flagName].localeCompare(b[flagName]));
      let filterCommand = document.querySelector(idFormFilters + ' .' + flagCommand);
      let idCommandPrevious = DOM.getElementAttributeValuePrevious(filterCommand);
      filterCommand.innerHTML = '';
      let optionJson, option;
      option = DOM.createOption(null);
      filterCommand.appendChild(option);
      sortedCommands.forEach(command => {
        optionJson = BusinessObjects.getOptionJsonFromObjectJson(command, idCommandPrevious);
        option = DOM.createOption(optionJson);
        filterCommand.appendChild(option);
      });
      filterCommand.dispatchEvent(new Event('change'));
      return isDirtyFilter;
    });
  }
  hookupFilterCommand() {
    this.hookupFilter(attrIdCommand);
  }
  hookupFilterLocation() {
    this.hookupFilter(attrIdLocation);
  }
  /*
  getAndLoadFilteredTableContent = () => {
      this.callFilterTableContent()
          .catch(error => console.error('Error:', error));
  }
  */
  static getFormFilters() {
    return document.querySelector(idFormFilters);
  }
  callFilterTableContent() {
    let formFilters = TableBasePage.getFormFilters();
    let filtersJson = DOM.convertForm2JSON(formFilters);
    utils_Utils.consoleLogIfNotProductionEnvironment("callFilterTableContent");
    utils_Utils.consoleLogIfNotProductionEnvironment("formFilters");
    utils_Utils.consoleLogIfNotProductionEnvironment(formFilters);
    utils_Utils.consoleLogIfNotProductionEnvironment("filtersJson");
    utils_Utils.consoleLogIfNotProductionEnvironment(filtersJson);
    this.leave();
    API.goToHash(this.constructor.hash, filtersJson);
  }
  callbackLoadTableContent(response) {
    let table = TableBasePage.getTableMain();
    let bodyTable = table.querySelector('tbody');
    bodyTable.querySelectorAll('tr').forEach(function (row) {
      row.remove();
    });
    let rowsJson = response.data[flagRows];
    if (!Validation.isEmpty(rowsJson) && rowsJson.every(row => row.hasOwnProperty('display_order'))) {
      rowsJson = rowsJson.sort((a, b) => a.display_order - b.display_order);
    }
    rowsJson.forEach(this.loadRowTable.bind(this));
    this.hookupTableMain();
  }
  static getTableMain() {
    return document.querySelector(idTableMain);
  }
  loadRowTable(rowJson) {
    throw new Error("Subclass of TableBasePage must implement method loadRowTable().");
  }
  getAndLoadFilteredTableContentSinglePageApp() {
    this.callFilterTableContent().then(data => {
      utils_Utils.consoleLogIfNotProductionEnvironment('Table data received:', data);
      this.callbackLoadTableContent(data);
    }).catch(error => console.error('Error:', error));
  }
  hookupButtonsSaveCancel() {
    this.hookupButtonSave();
    this.hookupButtonCancel();
    this.toggleShowButtonsSaveCancel(false);
  }
  saveRecordsTableDirty() {
    let records = this.getTableRecords(true);
    if (records.length == 0) {
      OverlayError.show('No records to save');
      return;
    }
    let formElement = TableBasePage.getFormFilters();
    let comment = DOM.getElementValueCurrent(document.querySelector(idTextareaConfirm));
    this.callSaveTableContent(records, formElement, comment).then(data => {
      if (data[flagStatus] == flagSuccess) {
        if (_verbose) {
          utils_Utils.consoleLogIfNotProductionEnvironment('Records saved!');
          utils_Utils.consoleLogIfNotProductionEnvironment('Data received:', data);
        }
        this.callFilterTableContent();
      } else {
        utils_Utils.consoleLogIfNotProductionEnvironment("error: ", data[flagMessage]);
        OverlayError.show(data[flagMessage]);
      }
    }).catch(error => console.error('Error:', error));
  }
  getTableRecords(dirtyOnly = false) {
    let records = [];
    let record;
    document.querySelectorAll(idTableMain + ' > tbody > tr').forEach(row => {
      if (dirtyOnly && !DOM.hasDirtyChildrenContainer(row)) return;
      record = this.getJsonRow(row);
      records.push(record);
    });
    return records;
  }
  getJsonRow(row) {
    throw new Error("Subclass of TableBasePage must implement method getJsonRow().");
  }
  saveRecordsTableDirtySinglePageApp() {
    let records = this.getTableRecords(true);
    if (records.length == 0) {
      OverlayError.show('No records to save');
      return;
    }
    let formElement = TableBasePage.getFormFilters();
    let comment = DOM.getElementValueCurrent(document.querySelector(idTextareaConfirm));
    this.callSaveTableContent(records, formElement, comment).then(data => {
      if (data[flagStatus] == flagSuccess) {
        if (_verbose) {
          utils_Utils.consoleLogIfNotProductionEnvironment('Records saved!');
          utils_Utils.consoleLogIfNotProductionEnvironment('Data received:', data);
        }
        this.callbackLoadTableContent(data);
      } else {
        utils_Utils.consoleLogIfNotProductionEnvironment("error: ", data[flagMessage]);
        OverlayError.show(data[flagMessage]);
      }
    }).catch(error => console.error('Error:', error));
  }
  hookupButtonCancel() {
    Events.initialiseEventHandler('.' + flagContainer + '.' + flagSave + '.' + flagCancel + ' button.' + flagCancel, flagInitialised, button => {
      button.addEventListener("click", event => {
        event.stopPropagation();
        button = event.target;
        if (button.classList.contains(flagIsCollapsed)) return;
        this.callFilterTableContent();
      });
      button.classList.add(flagIsCollapsed);
    });
  }
  handleClickAddRowTable(event, button) {
    event.stopPropagation();
    _rowBlank.setAttribute(this.constructor.attrIdRowObject, -1 - _rowBlank.getAttribute(this.constructor.attrIdRowObject));
    let tbody = document.querySelector(idTableMain + ' tbody');
    if (tbody.classList.contains(flagIsCollapsed)) return;
    let row = _rowBlank.cloneNode(true);
    row.classList.remove(flagInitialised);
    row.querySelectorAll('.' + flagInitialised).forEach(function (element) {
      element.classList.remove(flagInitialised);
    });
    let countRows = document.querySelectorAll(idTableMain + ' > tbody > tr').length;
    row.setAttribute(this.constructor.attrIdRowObject, -1 - countRows);
    this.initialiseRowNew(tbody, row);
    tbody.prepend(row);
    tbody.scrollTop = 0;
    this.hookupTableMain();
    this.postInitialiseRowNewCallback(tbody);
  }
  initialiseRowNew(tbody, row) {
    if (this.constructor === TableBasePage) {
      throw new Error("Subclass of TableBasePage must implement method initialiseRowNew().");
    }
    // row.classList.remove(flagRowNew);
  }
  hookupTableMain() {
    if (this.constructor === TableBasePage) {
      throw new Error("Must implement hookupTableMain() method.");
    }
    Events.initialiseEventHandler(idTableMain, flagInitialised, table => {
      this.cacheRowBlank();
    });
  }
  cacheRowBlank() {
    let selectorRowNew = idTableMain + ' tbody tr.' + flagRowNew;
    let rowBlankTemp = document.querySelector(selectorRowNew);
    if (rowBlankTemp == null) return;
    utils_Utils.consoleLogIfNotProductionEnvironment("row blank temp: ", rowBlankTemp);
    let countRows = document.querySelectorAll(idTableMain + ' > tbody > tr').length;
    _rowBlank = rowBlankTemp.cloneNode(true);
    document.querySelectorAll(selectorRowNew).forEach(function (row) {
      row.remove();
    });
    _rowBlank.setAttribute(this.constructor.attrIdRowObject, -1 - countRows);
  }
  postInitialiseRowNewCallback(tbody) {
    if (this.constructor === TableBasePage) {
      throw new Error("Subclass of TableBasePage must implement method postInitialiseRowNewCallback(tbody).");
    }
  }
  initialiseSliderDisplayOrderRowNew(tbody, row) {
    // let tdSelector = ':scope > tr > td.' + flagDisplayOrder;
    // let tbody = document.querySelector('table' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > tbody');
    let slidersDisplayOrder = tbody.querySelectorAll(':scope > tr > td.' + flagDisplayOrder + ' input.' + flagSlider);
    let maxDisplayOrder = 0;
    slidersDisplayOrder.forEach(slider => {
      maxDisplayOrder = Math.max(maxDisplayOrder, parseFloat(DOM.getElementValueCurrent(slider)));
    });
    let sliderDisplayOrder = row.querySelector('td.' + flagDisplayOrder + ' .' + flagSlider);
    DOM.setElementValuesCurrentAndPrevious(sliderDisplayOrder, maxDisplayOrder + 1);
  }
  hookupSlidersDisplayOrderTable() {
    let selectorDisplayOrder = idTableMain + ' tbody tr td.' + flagDisplayOrder + ' input.' + flagSlider + '.' + flagDisplayOrder;
    this.hookupChangeHandlerTableCells(selectorDisplayOrder);
  }
  hookupChangeHandlerTableCells(inputSelector, handler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    Events.initialiseEventHandler(inputSelector, flagInitialised, input => {
      input.addEventListener("change", event => {
        handler(event, input);
      });
      handler(null, input);
    });
  }
  handleChangeNestedElementCellTable(event, element) {
    let wasDirtyParentRows = this.getAllIsDirtyRowsInParentTree(element);
    let wasDirtyElement = element.classList.contains(flagDirty);
    let isDirtyElement = DOM.updateAndCheckIsElementDirty(element);
    // Utils.consoleLogIfNotProductionEnvironment({isDirtyElement, wasDirtyElement, wasDirtyParentRows});
    // let td = DOM.getCellFromElement(element);
    // DOM.setElementAttributeValueCurrent(td, DOM.getElementAttributeValueCurrent(element));
    if (isDirtyElement != wasDirtyElement) {
      // DOM.handleDirtyElement(td, isDirtyElement);
      this.updateAndToggleShowButtonsSaveCancel();
      this.cascadeChangedIsDirtyNestedElementCellTable(element, isDirtyElement, wasDirtyParentRows);
    }
  }
  getAllIsDirtyRowsInParentTree(element) {
    let rows = [];
    let parent = element;
    let isDirty;
    while (parent) {
      if (parent.tagName.toUpperCase() == 'TR') {
        isDirty = parent.classList.contains(flagDirty);
        rows.push(isDirty);
      }
      parent = parent.parentElement;
    }
    return rows;
  }
  cascadeChangedIsDirtyNestedElementCellTable(element, isDirtyElement, wasDirtyParentRows) {
    if (Validation.isEmpty(wasDirtyParentRows)) return;
    let tr = DOM.getRowFromElement(element);
    let isDirtyRow = isDirtyElement || DOM.hasDirtyChildrenContainer(tr);
    let wasDirtyRow = wasDirtyParentRows.shift();
    utils_Utils.consoleLogIfNotProductionEnvironment({
      isDirtyRow,
      wasDirtyRow
    });
    if (isDirtyRow != wasDirtyRow) {
      DOM.handleDirtyElement(tr, isDirtyRow);
      this.updateAndToggleShowButtonsSaveCancel();
      this.cascadeChangedIsDirtyNestedElementCellTable(tr.parentElement, isDirtyRow, wasDirtyParentRows);
    }
  }
  hookupChangeHandlerTableCellsWhenNotCollapsed(inputSelector, handler = (event, element) => {
    if (!element.classList.contains(flagIsCollapsed)) this.handleChangeNestedElementCellTable(event, element);
  }) {
    Events.hookupEventHandler("change", inputSelector, handler);
  }
  hookupFieldsCodeTable() {
    this.hookupChangeHandlerTableCells(idTableMain + ' > tbody > tr > td.' + flagCode + ' > .' + flagCode);
  }
  hookupFieldsNameTable() {
    this.hookupChangeHandlerTableCells(idTableMain + ' > tbody > tr > td.' + flagName + ' > .' + flagName);
  }
  hookupFieldsDescriptionTable() {
    this.hookupChangeHandlerTableCells(idTableMain + ' > tbody > tr > td.' + flagDescription + ' > .' + flagDescription);
  }
  hookupFieldsNotesTable() {
    this.hookupChangeHandlerTableCells(idTableMain + ' > tbody > tr > td.' + flagNotes + ' > .' + flagNotes);
  }
  hookupFieldsActive(flagTable = '', handleClickRowNew = (event, element) => {
    this.handleClickAddRowTable(event, element);
  }) {
    let selectorButton = 'table.table-main' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > tbody > tr > td.' + flagActive + ' .' + flagButton + '.' + flagActive;
    let selectorButtonDelete = selectorButton + '.' + flagDelete;
    let selectorButtonUndelete = selectorButton + ':not(.' + flagDelete + ')';
    utils_Utils.consoleLogIfNotProductionEnvironment("hookupFieldsActive: ", selectorButtonDelete, selectorButtonUndelete);
    this.hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete);
    this.hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete);
    Events.hookupEventHandler("click", 'table.table-main' + (Validation.isEmpty(flagTable) ? '' : '.' + flagTable) + ' > thead > tr > th.' + flagActive + ' .' + flagButton + '.' + flagActive, (event, button) => {
      handleClickRowNew(event, button);
    });
  }
  hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    Events.hookupEventHandler("click", selectorButtonDelete, (event, element) => {
      this.handleClickButtonRowDelete(event, element, selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => {
        changeHandler(changeEvent, changeElement);
      });
    });
  }
  handleClickButtonRowDelete(event, element, selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    if (element.tagName.toUpperCase() != 'SVG') element = element.parentElement;
    let valuePrevious = DOM.getElementAttributeValuePrevious(element);
    let wasDirty = element.classList.contains(flagDirty);
    let row = DOM.getRowFromElement(element);
    if (row.classList.contains(flagRowNew) && !DOM.hasDirtyChildrenContainer(row)) {
      row.parentNode.removeChild(row);
    } else {
      let buttonAddTemplate = document.querySelector(idContainerTemplateElements + ' .' + flagButton + '.' + flagActive + '.' + flagAdd);
      let buttonAdd = buttonAddTemplate.cloneNode(true);
      DOM.setElementAttributeValuePrevious(buttonAdd, valuePrevious);
      DOM.setElementAttributeValueCurrent(buttonAdd, false);
      if (wasDirty) buttonAdd.classList.add(flagDirty);
      element.replaceWith(buttonAdd);
      changeHandler(null, buttonAdd);
      this.hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => {
        changeHandler(changeEvent, changeElement);
      });
    }
    this.updateAndToggleShowButtonsSaveCancel();
  }
  hookupButtonsRowUndelete(selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    Events.hookupEventHandler("click", selectorButtonUndelete, (event, element) => {
      this.handleClickButtonRowUndelete(event, element, selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => {
        changeHandler(changeEvent, changeElement);
      });
    });
  }
  handleClickButtonRowUndelete(event, element, selectorButtonDelete, selectorButtonUndelete, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    if (element.tagName.toUpperCase() != 'SVG') element = element.parentElement;
    let valuePrevious = DOM.getElementAttributeValuePrevious(element);
    let wasDirty = DOM.isElementDirty(element);
    let buttonDeleteTemplate = document.querySelector(idContainerTemplateElements + ' .' + flagButton + '.' + flagActive + '.' + flagDelete);
    let buttonDelete = buttonDeleteTemplate.cloneNode(true);
    DOM.setElementAttributeValuePrevious(buttonDelete, valuePrevious);
    DOM.setElementAttributeValueCurrent(buttonDelete, true);
    if (wasDirty) buttonDelete.classList.add(flagDirty);
    element.replaceWith(buttonDelete);
    changeHandler(null, buttonDelete);
    this.hookupButtonsRowDelete(selectorButtonDelete, selectorButtonUndelete, (changeEvent, changeElement) => {
      changeHandler(changeEvent, changeElement);
    });
    this.updateAndToggleShowButtonsSaveCancel();
  }
  hookupTdsAccessLevel() {
    this.hookupTableCellDdlPreviews(flagAccessLevel, utils_Utils.getListFromDict(accessLevels));
  }
  hookupTableCellDdlPreviews(fieldFlag, optionList, cellSelector = null, ddlHookup = ddlSelector => {
    this.hookupTableCellDdls(ddlSelector);
  }, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    if (cellSelector == null) cellSelector = idTableMain + ' > tbody > tr > td.' + fieldFlag;
    Events.hookupEventHandler("click", cellSelector + ' div.' + fieldFlag, (event, div) => {
      this.handleClickTableCellDdlPreview(event, div, fieldFlag, optionList, cellSelector, ddlSelector => {
        ddlHookup(ddlSelector, (event, element) => {
          changeHandler(event, element);
        });
      });
    });
    ddlHookup(cellSelector + ' select.' + fieldFlag);
  }
  hookupTableCellDdls(ddlSelector, changeHandler = (event, element) => {
    this.handleChangeNestedElementCellTable(event, element);
  }) {
    this.hookupChangeHandlerTableCells(ddlSelector, (event, element) => {
      changeHandler(event, element);
    });
  }
  handleClickTableCellDdlPreview(event, div, fieldFlag, optionObjectList, cellSelector = null, ddlHookup = cellSelector => {
    this.hookupTableCellDdls(cellSelector);
  }) {
    if (Validation.isEmpty(cellSelector)) cellSelector = idTableMain + ' > tbody > tr > td.' + fieldFlag;
    let idSelected = DOM.getElementAttributeValueCurrent(div);
    let td = DOM.getCellFromElement(div);
    td.innerHTML = '';
    let ddl = document.createElement('select');
    ddl.classList.add(fieldFlag);
    DOM.setElementValuesCurrentAndPrevious(ddl, idSelected);
    let optionJson, option;
    if (_verbose) {
      utils_Utils.consoleLogIfNotProductionEnvironment("click table cell ddl preview");
      utils_Utils.consoleLogIfNotProductionEnvironment({
        optionObjectList,
        cellSelector
      });
    }
    option = DOM.createOption(null);
    ddl.appendChild(option);
    optionObjectList.forEach(optionObjectJson => {
      optionJson = BusinessObjects.getOptionJsonFromObjectJson(optionObjectJson, idSelected);
      option = DOM.createOption(optionJson);
      ddl.appendChild(option);
    });
    td.appendChild(ddl);
    let ddlSelector = cellSelector + ' select.' + fieldFlag;
    ddlHookup(ddlSelector);
  }
  /*
  hookupTableCellDDlPreviewsWhenNotCollapsed(cellSelector, optionList, ddlHookup = (event, element) => { this.hookupTableCellDdls(event, element); }) {
      Events.hookupEventHandler("click", cellSelector + ' div', (event, div) => {
          this.handleClickTableCellDdlPreview(event, div, optionList, cellSelector, (event, element) => { ddlHookup(event, element); });
      });
  }
  */
  toggleColumnCollapsed(flagColumn, isCollapsed) {
    this.toggleColumnHasClassnameFlag(flagColumn, isCollapsed, flagIsCollapsed);
  }
  toggleColumnHeaderCollapsed(flagColumn, isCollapsed) {
    this.toggleColumnHasClassnameFlag(flagColumn, isCollapsed, flagIsCollapsed);
  }
  hookupFieldsCommandCategory(idTable = null) {
    if (idTable == null) idTable = idTableMain;
    this.hookupTableCellDdlPreviews(flagCommandCategory, utils_Utils.getListFromDict(filterCommandCategories).sort((a, b) => a[flagName].localeCompare(b[flagName])), idTable + ' > tbody > tr > td.' + flagCommandCategory // + ' .' + flagCommandCategory
    , cellSelector => {
      this.hookupCommandCategoryDdls(cellSelector);
    });
  }
  hookupCommandCategoryDdls(ddlSelector) {
    this.hookupChangeHandlerTableCells(ddlSelector, (event, element) => {
      this.handleChangeCommandCategoryDdl(event, element);
    });
  }
  handleChangeCommandCategoryDdl(event, ddlCategory) {
    let row = DOM.getRowFromElement(ddlCategory);
    let idCommandCategoryRowOld = this.getIdCommandCategoryRow(row); // DOM.getElementAttributeValueCurrent(ddlCategory);
    this.handleChangeNestedElementCellTable(event, ddlCategory);
    let idCommandCategoryRowNew = this.getIdCommandCategoryRow(row); // DOM.getElementAttributeValueCurrent(ddlCategory);
    if (idCommandCategoryRowOld == idCommandCategoryRowNew || idCommandCategoryRowNew == 0) return;
    console.log({
      idCommandCategoryRowNew,
      idCommandCategoryRowOld
    });
    let idCommandCategoryFilter = this.getIdCommandCategoryFilter();
    let tdCommand = row.querySelector('td.' + flagCommand);
    tdCommand.dispatchEvent(new Event('click'));
    let ddlCommand = row.querySelector('td.' + flagCommand + ' select.' + flagCommand);
    ddlCommand.innerHTML = '';
    ddlCommand.appendChild(DOM.createOption(null));
    let optionJson, option;
    let commandsInCategory = utils_Utils.getListFromDict(filterCommands).filter(command => (command[attrIdCommandCategory] == idCommandCategoryRowNew || idCommandCategoryRowNew == 0) && (command[attrIdCommandCategory] == idCommandCategoryFilter || idCommandCategoryFilter == 0));
    let sortedCommands = commandsInCategory.sort((a, b) => a[flagName].localeCompare(b[flagName]));
    sortedCommands.forEach(command => {
      optionJson = BusinessObjects.getOptionJsonFromObjectJson(command);
      option = DOM.createOption(optionJson);
      ddlCommand.appendChild(option);
    });
    this.handleChangeNestedElementCellTable(event, ddlCommand);
  }
  hookupFieldsCommand(idTable = null) {
    if (idTable == null) idTable = idTableMain;
    Events.hookupEventHandler("click", idTable + ' > tbody > tr > td.' + flagCommand + ' div.' + flagCommand, (event, div) => {
      utils_Utils.consoleLogIfNotProductionEnvironment(div);
      let parentTr = DOM.getRowFromElement(div);
      utils_Utils.consoleLogIfNotProductionEnvironment({
        div,
        parentTr
      });
      let tdCommandCategory = parentTr.querySelector('td.' + flagCommandCategory);
      let idCommandCategoryRow = this.getIdCommandCategoryRow(parentTr); // DOM.getElementAttributeValueCurrent(tdCommandCategory);
      let idCommandCategoryFilter = this.getIdCommandCategoryFilter();
      let filterCommandList = utils_Utils.getListFromDict(filterCommands);
      let commandsInCategory = filterCommandList.filter(command => (command[attrIdCommandCategory] == idCommandCategoryRow || idCommandCategoryRow == 0) && (command[attrIdCommandCategory] == idCommandCategoryFilter || idCommandCategoryFilter == 0));
      let sortedCommands = commandsInCategory.sort((a, b) => a[flagName].localeCompare(b[flagName]));
      utils_Utils.consoleLogIfNotProductionEnvironment({
        tdCommandCategory,
        idCommandCategoryRow,
        idCommandCategoryFilter,
        filterCommandList,
        commandsInCategory
      });
      utils_Utils.consoleLogIfNotProductionEnvironment(filterCommandList);
      this.handleClickTableCellDdlPreview(event, div, flagCommand // fieldFlag
      , sortedCommands // optionList
      , idTable + ' > tbody > tr > td.' + flagCommand // cellSelector
      , cellSelector => {
        this.hookupTableCellDdls(cellSelector, (event, element) => {
          this.handleChangeCommandDdl(event, element);
        });
      });
    });
    this.hookupTableCellDdls(idTable + ' > tbody > tr > td.' + flagCommand + ' select.' + flagCommand, (event, element) => {
      this.handleChangeCommandDdl(event, element);
    });
  }
  handleChangeCommandDdl(event, ddlCommand) {
    // console.log("handle change command ddl");
    let row = DOM.getRowFromElement(ddlCommand);
    this.handleChangeNestedElementCellTable(event, ddlCommand);
    let idCommandCategoryRowOld = this.getIdCommandCategoryRow(row);
    let idCommandNew = this.getIdCommandRow(row);
    let commandNew = filterCommands[idCommandNew];
    // console.log({ idCommandCategoryRowOld, commandNew });
    if (commandNew == null || idCommandCategoryRowOld == commandNew[attrIdCommandCategory]) return;
    let divCommandCategory = row.querySelector('td.' + flagCommandCategory + ' div');
    if (divCommandCategory) divCommandCategory.dispatchEvent(new Event('click'));
    let ddlCommandCategory = row.querySelector('td.' + flagCommandCategory + ' select.' + flagCommandCategory);
    DOM.setElementValueCurrent(ddlCommandCategory, commandNew[attrIdCommandCategory]);
    // console.log({ ddlCommandCategory, commandNew });
    this.handleChangeNestedElementCellTable(event, ddlCommandCategory);
  }
  getIdCommandCategoryRow(tr) {
    let elementCommandCategory = tr.querySelector('td.' + flagCommandCategory + ' .' + flagCommandCategory);
    return DOM.getElementAttributeValueCurrent(elementCommandCategory);
  }
  getIdCommandCategoryFilter() {
    let formFilters = TableBasePage.getFormFilters();
    let idCommandCategory = 0;
    if (formFilters == null) return idCommandCategory;
    let commandCategoryFilter = formFilters.querySelector('#' + attrIdCommandCategory);
    let commandFilter = formFilters.querySelector('#' + attrIdCommand);
    let valueCurrentCommandCategoryFilter = DOM.getElementAttributeValueCurrent(commandCategoryFilter);
    utils_Utils.consoleLogIfNotProductionEnvironment({
      valueCurrentCommandCategoryFilter
    });
    if (valueCurrentCommandCategoryFilter == "") {
      let valueCurrentCommandFilter = DOM.getElementAttributeValueCurrent(commandFilter);
      utils_Utils.consoleLogIfNotProductionEnvironment({
        valueCurrentCommandFilter
      });
      if (valueCurrentCommandFilter != "") {
        let command = filterCommands[valueCurrentCommandFilter];
        idCommandCategory = command[attrIdCommandCategory];
      }
    } else {
      idCommandCategory = parseInt(valueCurrentCommandCategoryFilter);
    }
    return idCommandCategory;
  }
  getHasCommandCategoryFilter() {
    let idCommandCategoryFilter = this.getIdCommandCategoryFilter();
    return !(Validation.isEmpty(idCommandCategoryFilter) || idCommandCategoryFilter == 0);
  }
  getIdCommandRow(tr) {
    let elementCommand = tr.querySelector('td.' + flagCommand + ' .' + flagCommand);
    return DOM.getElementAttributeValueCurrent(elementCommand);
  }
  getIdCommandFilter() {
    let formFilters = TableBasePage.getFormFilters();
    let commandFilter = formFilters.querySelector('#' + attrIdCommand);
    let valueCurrentCommandFilter = DOM.getElementAttributeValueCurrent(commandFilter);
    let idCommand = parseInt(valueCurrentCommandFilter);
    return idCommand;
  }
  getHasCommandFilter() {
    let idCommandFilter = this.getIdCommandFilter();
    return !(Validation.isEmpty(idCommandFilter) || idCommandFilter == 0);
  }
  /*
  createTdActive(isActive) {
      let tdActive = document.createElement("td");
      tdActive.classList.add(flagActive);
      let buttonActive = document.createElement("button");
      buttonActive.classList.add(flagActive);
      buttonActive.classList.add(isActive ? flagDelete : flagAdd);
      buttonActive.textContent = isActive ? 'x' : '+';
      DOM.setElementAttributesValuesCurrentAndPrevious(buttonActive, isActive);
      tdActive.appendChild(buttonActive);
      return tdActive;
  }
  */
  leave() {
    if (this.constructor === TableBasePage) {
      throw new Error("Must implement leave() method.");
    }
    super.leave();
    let formFilters = TableBasePage.getFormFilters();
    let dataPage = {};
    dataPage[flagFormFilters] = DOM.convertForm2JSON(formFilters);
    this.setLocalStoragePage(dataPage);
  }
  toggleColumnHasClassnameFlag(columnFlag, isRequiredFlag, classnameFlag) {
    let table = TableBasePage.getTableMain();
    let columnTh = table.querySelector('th.' + columnFlag);
    let columnThHasFlag = columnTh.classList.contains(classnameFlag);
    if (isRequiredFlag == columnThHasFlag) return;
    DOM.toggleElementHasClassnameFlag(columnTh, isRequiredFlag, classnameFlag);
  }
  toggleColumnHeaderHasClassnameFlag(columnFlag, isRequiredFlag, classnameFlag) {
    let table = TableBasePage.getTableMain();
    let columnTh = table.querySelector('th.' + columnFlag);
    DOM.toggleElementHasClassnameFlag(columnTh, isRequiredFlag, classnameFlag);
  }
  updateAndToggleShowButtonsSaveCancel() {
    // let pageBody = document.querySelector(idPageBody);
    let isDirty = DOM.hasDirtyChildrenContainer(pageBody);
    let buttonContainerSelector = '.' + flagContainer + '.' + flagSave + '.' + flagCancel;
    let buttonSave = document.querySelector(buttonContainerSelector + ' ' + idButtonSave);
    let areVisibleSaveCancelButtons = !buttonSave.classList.contains(flagIsCollapsed);
    console.log({
      pageBody,
      isDirty,
      areVisibleSaveCancelButtons
    });
    this.toggleShowButtonsSaveCancel(isDirty || areVisibleSaveCancelButtons);
  }
}
;// ./static/js/pages/tcg/mtg_decks.js




class PageMtgDecks extends TableBasePage {
  static hash = hashPageMtgDecks;
  static attrIdRowObject = attrDeckId;
  callSaveTableContent = API.saveDeck;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  hookupFilters() {
    /*
    this.sharedHookupFilters();
    this.hookupFilterActive();
    */
  }
  loadRowTable(rowJson) {
    if (rowJson == null) return;
    if (_verbose) {
      utils_Utils.consoleLogIfNotProductionEnvironment("applying data row: ", rowJson);
    }
  }
  getJsonRow(row) {
    return;
  }
  initialiseRowNew(tbody, row) {}
  postInitialiseRowNewCallback(tbody) {
    let newRows = tbody.querySelectorAll('tr.' + flagRowNew);
    let newestRow = newRows[0];
    let clickableElementsSelector = ['td.' + attrCommanderBracketId + ' div.' + attrCommanderBracketId].join('');
    newestRow.querySelectorAll(clickableElementsSelector).forEach(clickableElement => {
      clickableElement.click();
    });
  }
  hookupTableMain() {
    super.hookupTableMain();
    this.hookupTableMainRows();
    this.hookupFieldsNameTable();
    this.hookupTableMainIsCommanderCheckboxes();
    this.hookupTableMainCommanderBracketPreviews();
    this.hookupFieldsActive();
  }
  hookupTableMainRows() {
    return;
    // removed by dead control flow

    // removed by dead control flow

  }
  static toggleShowDeckStatisticsSection(showSection) {
    let statisticsSectionTableBody = document.querySelector('table.' + flagStatistics + ' tbody');
    if (showSection) {
      statisticsSectionTableBody.classList.remove(flagIsCollapsed);
    } else {
      statisticsSectionTableBody.classList.add(flagIsCollapsed);
    }
  }
  hookupTableMainIsCommanderCheckboxes() {
    this.hookupChangeHandlerTableCells(idTableMain + ' td.' + flagIsCommander + ' .' + flagIsCommander);
  }
  hookupTableMainCommanderBracketPreviews() {
    this.hookupTableCellDdlPreviews(attrCommanderBracketId, utils_Utils.getListFromDict(commanderBrackets));
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/tcg/mtg_game.js





class PageMtgGame extends TableBasePage {
  static hash = hashPageMtgGame;
  static attrIdRowObject = attrGameId;
  callSaveTableContent = API.saveGame;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
    this.hookupTcgGame();
  }
  hookupFilters() {
    // this.sharedHookupFilters();
  }
  callFilterTableContent() {
    window.location.reload();
  }
  loadRowTable(rowJson) {
    return;
  }
  getJsonRow(row) {
    return;
  }
  initialiseRowNew(tbody, row) {}
  postInitialiseRowNewCallback(tbody) {}
  hookupTableMain() {
    super.hookupTableMain();
  }
  hookupTcgGame() {
    this.initGamePage();
    let pageHeading = document.querySelector('.container.company-name .tcg-title.company-name');
    pageHeading.innerText = `MTG Game #${gameId}`;
  }
  initGamePage() {
    // Load existing game state from API or show setup
    PageMtgGame.updatePlayerSetup();
    if (typeof gameId !== 'undefined' && gameId) {
      this.loadGameFromServer();
    }
    /*
    else {
        PageMtgGame.updatePlayerSetup();
    }
    */

    PageMtgGame.hookupResetButton();
    PageMtgGame.hookupPlayerCountInput();
    this.hookupStartGameButton();
    /*
    this.hookupCommanderDeathIncrementButtons();
    this.hookupEliminateCommanderButtons();
    this.hookupPlayerLifeIncrementButtons();
    this.hookupCommanderDamageIncrementButtons();
    */
    PageMtgGame.hookupOverlayGameOutdated();
    PageMtgGame.hookupGameUpdateCheck();
  }
  static hookupResetButton() {
    const resetGameButton = document.querySelector('header.game-header .header-right .btn-tcg.btn-tcg-secondary');
    if (resetGameButton) {
      resetGameButton.addEventListener('click', PageMtgGame.resetGame);
    }
  }
  static hookupPlayerCountInput() {
    const playerCountInput = document.getElementById('playerCount');
    if (playerCountInput) {
      playerCountInput.addEventListener('change', PageMtgGame.updatePlayerSetup);
    }
  }
  hookupStartGameButton() {
    const startGameButton = document.querySelector('.setup-section .setup-actions .btn-tcg');
    if (startGameButton) {
      startGameButton.addEventListener('click', () => {
        this.startGame();
      });
    }
  }
  /*
  hookupCommanderDeathIncrementButtons() {
      const commanderDeathIncremementButtons = document.querySelectorAll('#players-grid .player-card .commander-deaths .death-btn');
      if (commanderDeathIncremementButtons) {
          commanderDeathIncremementButtons.forEach((button) => {
              button.addEventListener('click', PageMtgGame.changeCommanderDeaths);
          });
      }
  }
  hookupEliminateCommanderButtons() {
      const eliminateCommanderButtons = document.querySelector('#players-grid .player-card .eliminate-btn');
      if (eliminateCommanderButtons) {
          eliminateCommanderButtons.forEach((button) => {
              button.addEventListener('click', PageMtgGame.toggleEliminate);
          });
      }
  }
  hookupPlayerLifeIncrementButtons() {
      const playerLifeIncrementButtons = document.querySelector('#players-grid .player-card .eliminate-btn');
      if (playerLifeIncrementButtons) {
          playerLifeIncrementButtons.forEach((button) => {
              button.addEventListener('click', PageMtgGame.changeLife);
          });
      }
  }
  hookupCommanderDamageIncrementButtons() {
      const commanderDamageIncrementButtons = document.querySelector('#players-grid .player-card .eliminate-btn');
      if (commanderDamageIncrementButtons) {
          commanderDamageIncrementButtons.forEach((button) => {
              button.addEventListener('click', PageMtgGame.changeCommanderDamage);
          });
      }
  }
  */
  static hookupGameUpdateCheck() {
    const intervalId = setInterval(async () => {
      const isGameDataOutdated = await PageMtgGame.checkLastGameUpdate();
      if (isGameDataOutdated) {
        clearInterval(intervalId);
      }
    }, 30000);
  }
  static async checkLastGameUpdate() {
    try {
      // Fetch players, rounds, and damage records from API
      const isGameDataOutdatedResponse = await API.getIsGameDataOutdated(game);
      const isGameDataOutdated = isGameDataOutdatedResponse[flagData];
      console.log({
        isGameDataOutdatedResponse,
        isGameDataOutdated
      });
      if (isGameDataOutdated) {
        // const gameDataOutdatedLabel = document.querySelector(gameDataOutdatedLabelId);
        // gameDataOutdatedLabel.classList.remove(flagIsCollapsed);
        const pageTitleElement = document.querySelector('.topnav .tcg-title');
        pageTitleElement.classList.add(flagDirty);
        PageMtgGame.showOverlayGameOutdated();
      }
      return isGameDataOutdated;
    } catch (error) {
      console.error('Error checking for game update from server:', error);
    }
  }
  static hookupOverlayGameOutdated() {
    Events.initialiseEventHandler(idOverlayGameOutdated + ' button.' + flagCancel, flagInitialised, buttonCancel => {
      buttonCancel.addEventListener('click', () => {
        let overlay = document.querySelector(idOverlayGameOutdated);
        overlay.style.visibility = 'hidden';
      });
    });
    Events.initialiseEventHandler(idOverlayGameOutdated + ' button.' + flagSubmit, flagInitialised, buttonConfirm => {
      buttonConfirm.addEventListener('click', () => {
        let overlay = document.querySelector(idOverlayGameOutdated);
        overlay.style.visibility = 'hidden';
        window.location.reload();
      });
    });
  }
  static showOverlayGameOutdated() {
    let overlay = document.querySelector(idOverlayGameOutdated);
    overlay.classList.remove(flagIsCollapsed);
    overlay.style.visibility = 'visible';
  }
  async loadGameFromServer() {
    console.log("loading game from server");
    try {
      // Fetch players, rounds, and damage records from API
      const [playersResponse, roundsResponse, damageResponse] = await Promise.all([API.getGamePlayers(gameId), API.getGameRounds(gameId), API.getGameDamageRecords(gameId)]);
      console.log({
        playersResponse,
        damageResponse
      });
      let setupSection = document.getElementById('setupSection');
      let gameSection = document.getElementById('gameSection');
      setupSection.classList.remove('hidden');
      gameSection.classList.add('hidden');
      if (playersResponse.status !== 'success') {
        console.error('Failed to load players:', playersResponse.message);
        return;
      }
      const savedPlayers = playersResponse.data || [];
      const savedRounds = roundsResponse.status === 'success' ? roundsResponse.data || [] : [];
      const savedDamageRecords = damageResponse.status === 'success' ? damageResponse.data || [] : [];
      players = savedPlayers;
      rounds = savedRounds;
      damageRecords = savedDamageRecords;
      if (savedPlayers.length === 0) {
        // No players yet, show setup section
        return;
      }

      // Hide setup, show game
      setupSection.classList.add('hidden');
      gameSection.classList.remove('hidden');
      console.log({
        savedPlayers,
        damageRecords
      });

      // Render players to DOM
      const latestRoundId = PageMtgGame.getLatestRoundId();
      const latestRound = rounds.filter(round => round[attrRoundId] == latestRoundId)[0];
      const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
      DOM.setElementValuesCurrentAndPrevious(roundDisplayOrderLabel, latestRound[flagDisplayOrder]);
      this.renderPlayers();
    } catch (error) {
      console.error('Error loading game from server:', error);
    }
  }
  static getRoundDisplayOrderLabel() {
    return document.querySelector(['#gameSection', ' > .', flagRow, '.', flagRound, ' > .', flagRow, '.', flagRound, ' > .', flagRound, '.', flagDisplayOrder, ' > span.', flagRound, '.', flagDisplayOrder].join(''));
  }
  renderPlayers() {
    const grid = document.getElementById('playersGrid');
    grid.innerHTML = '';

    // let activeRoundId = PageMtgGame.getActiveRoundId();
    const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
    const currentRoundDisplayOrder = parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
    let activeRound = rounds.filter(round => round[flagDisplayOrder] == currentRoundDisplayOrder)[0];
    if (activeRound == null) {
      activeRound = PageMtgGame.makeDefaultGameRound(currentRoundDisplayOrder);
      rounds.push(activeRound);
    }
    DOM.setElementValueCurrent(roundDisplayOrderLabel, activeRound[flagDisplayOrder]);
    const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= currentRoundDisplayOrder).map(round => round[attrRoundId]);
    players.forEach((player, index) => {
      // Build display name: prefer user_name + deck_name, fallback to player name
      const playerId = player[attrPlayerId];
      let displayName = PageMtgGame.makePlayerDisplayName(playerId, index);
      let damagePlayerPairs = [...players, {
        [attrPlayerId]: null
      }];
      let maxCommanderDamageReceived = 0;
      damagePlayerPairs.forEach(damagePlayerPair => {
        const sourceId = damagePlayerPair[attrPlayerId];
        const filteredPlayerDamages = damageRecords.filter(damage => damage[attrRoundId] == activeRound[attrRoundId]
        //    previousRoundIds.includes(damage[attrRoundId])
        && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId); //[playerId] || {};
        if (filteredPlayerDamages.length == 0) {
          damageRecords.push(PageMtgGame.makeDefaultGameRoundPlayerDamage(playerId, sourceId));
        }
        maxCommanderDamageReceived = Math.max(maxCommanderDamageReceived, damageRecords.filter(damage => damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId && damage[attrReceivedFromCommanderPlayerId] != null && previousRoundIds.includes(damage[attrRoundId])).map(damage => damage[flagLifeLoss]).reduce((a, b) => a + b, 0));
      });
      const totalDamage = damageRecords.filter(damage => damage[attrPlayerId] == playerId && previousRoundIds.includes(damage[attrRoundId])).map(damage => damage[flagLifeLoss] - damage[flagLifeGain]).reduce((a, b) => a + b, 0);
      let life = game[flagStartingLife] - totalDamage;
      let isEliminatedByForce = damageRecords.filter(damage => damage[attrPlayerId] == playerId && previousRoundIds.includes(damage[attrRoundId])).map(damage => damage[flagIsEliminated]).some(Boolean);
      console.log("renderPlayers");
      console.log({
        isEliminatedByForce,
        player,
        life,
        maxCommanderDamageReceived
      });
      const isEliminated = isEliminatedByForce || !player[flagActive] || life < 1 || maxCommanderDamageReceived >= 21;
      const totalCommanderDeaths = damageRecords.filter(damage => damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null && damage[attrRoundId] == activeRound[attrRoundId]).map(damage => damage[flagCommanderDeaths]).reduce((a, b) => a + b, 0);
      const card = document.createElement('div');
      card.className = `player-card ${isEliminated ? 'eliminated' : ''}`;
      card.style.animationDelay = `${index * 0.1}s`;
      card.dataset.playerId = playerId;
      card.dataset.userName = player.user_name || '';
      card.dataset.deckName = player.deck_name || '';
      card.innerHTML = `
                <div class="player-header">
                    <div class="player-info">
                        <div class="player-name">${displayName}</div>
                        <div class="commander-deaths">
                            <span>Commander Deaths:</span>
                            <div class="death-counter">
                                <button class="death-btn death-minus" data-player-id="${playerId}">&minus;</button>
                                <span class="death-display" data-player-id="${playerId}" ${attrValuePrevious}="${totalCommanderDeaths}">${totalCommanderDeaths}</span>
                                <button class="death-btn death-plus" data-player-id="${playerId}">+</button>
                            </div>
                        </div>
                    </div>
                    <button class="eliminate-btn" data-player-id="${playerId}" ${attrValuePrevious}="${isEliminated}">
                        ${isEliminated ? 'Revive' : 'Eliminate'}
                    </button>
                </div>

                <div class="life-total">
                    <input type="hidden" class="life-value" data-player-id="${playerId}" value="${life}">
                    <div class="life-display" data-player-id="${playerId}" ${attrValuePrevious}="${life}">${life}</div>
                    <label>Gain</label>
                    <div class="life-gain-controls">
                        <button class="life-gain-btn" data-player-id="${playerId}" data-amount="-5">-5</button>
                        <button class="life-gain-btn" data-player-id="${playerId}" data-amount="-1">-1</button>
                        <button class="life-gain-btn" data-player-id="${playerId}" data-amount="1">+1</button>
                        <button class="life-gain-btn" data-player-id="${playerId}" data-amount="5">+5</button>
                    </div>
                    <label>Loss</label>
                    <div class="life-loss-controls">
                        <button class="life-loss-btn" data-player-id="${playerId}" data-amount="-5">-5</button>
                        <button class="life-loss-btn" data-player-id="${playerId}" data-amount="-1">-1</button>
                        <button class="life-loss-btn" data-player-id="${playerId}" data-amount="1">+1</button>
                        <button class="life-loss-btn" data-player-id="${playerId}" data-amount="5">+5</button>
                    </div>
                </div>

                <div class="commander-damage-section">
                    <div class="section-title">Commander Damage Taken</div>
                    <div class="damage-grid" data-player-id="${playerId}">
                        ${PageMtgGame.renderCommanderDamageRows(playerId // playerId
      , player[attrDeckId] // deckId
      )}
                    </div>
                </div>
            `;
      grid.appendChild(card);
    });
    this.reorderPlayerCards();
    PageMtgGame.renderCommanderDamageLog();

    // Hookup all event handlers
    this.hookupGameRoundEvents();
    this.hookupPlayerCardEvents();
  }
  static renderCommanderDamageLog() {
    const currentRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
    const damageTableBody = document.querySelector('.' + flagDamageLog + '.' + flagContainer + ' table tbody');
    damageTableBody.innerHTML = '';
    const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= currentRoundDisplayOrder).map(round => round[attrRoundId]);
    let newTableBodyHtml = '';
    damageRecords.forEach(damage => {
      if (damage[flagActive] && (damage[flagCommanderDeaths] > 0 || damage[flagLifeGain] != 0 || damage[flagLifeLoss] != 0 || damage[flagIsEliminated])
      // &&  rounds.filter(r => r[attrRoundId] == damage[attrRoundId])[0][flagDisplayOrder] <= currentRoundDisplayOrder
      && previousRoundIds.includes(damage[attrRoundId])) {
        let round = rounds.filter(r => r[attrRoundId] == damage[attrRoundId])[0];
        let player = players.filter(p => p[attrPlayerId] == damage[attrPlayerId])[0];
        let receivedFromPlayer = damage[attrReceivedFromCommanderPlayerId] == null ? {
          [flagName]: ''
        } : players.filter(p => p[attrPlayerId] == damage[attrReceivedFromCommanderPlayerId])[0];
        newTableBodyHtml += `
                    <tr ${attrDamageId}="${damage[attrDamageId]}">
                        <td class="${attrRoundId}">${round[flagDisplayOrder]}</td>
                        <td class="${attrPlayerId}">${player[flagName]}</td>
                        <td class="${attrReceivedFromCommanderPlayerId}">${receivedFromPlayer[flagName]}</td>
                        <td class="${flagLifeGain}">${damage[flagLifeGain]}</td>
                        <td class="${flagLifeLoss}">${damage[flagLifeLoss]}</td>
                        <td class="${flagCommanderDeaths}">${damage[flagCommanderDeaths]}</td>
                        <td class="${flagIsEliminated}">${damage[flagIsEliminated]}</td>
                    </tr>
                `;
      }
    });
    damageTableBody.innerHTML = newTableBodyHtml;
  }
  static makeDefaultGameRoundPlayerDamage(playerId, receivedFromCommanderPlayerId) {
    let roundId = PageMtgGame.getActiveRoundId();
    return {
      [attrDamageId]: -1 - damageRecords.length,
      [attrRoundId]: roundId,
      [attrPlayerId]: playerId,
      [attrReceivedFromCommanderPlayerId]: receivedFromCommanderPlayerId,
      [flagLifeGain]: 0,
      [flagLifeLoss]: 0,
      [flagCommanderDeaths]: 0,
      [flagIsEliminated]: false,
      [flagActive]: true
    };
  }
  static getLatestRoundId() {
    let roundId = -1;
    if (rounds.length > 0) {
      const highestRoundDisplayOrder = rounds.map(round => {
        return round[flagDisplayOrder];
      }).reduce((acc, cur) => Math.max(acc, cur), 0);
      const filteredRounds = rounds.filter(round => round[flagDisplayOrder] == highestRoundDisplayOrder);
      if (filteredRounds.length > 0) {
        roundId = filteredRounds[0][attrRoundId];
      }
      console.log({
        "method": "getLatestRoundId",
        highestRoundDisplayOrder,
        filteredRounds,
        roundId
      });
    }
    return roundId;
  }
  static getActiveRoundDisplayOrder() {
    const roundDisplayOrderLabel = PageMtgGame.getRoundDisplayOrderLabel();
    return parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
  }
  static getActiveRoundId() {
    const currentRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
    let roundId = 0;
    if (rounds.length > 0) {
      let filteredRounds = rounds.filter(round => round[flagDisplayOrder] == currentRoundDisplayOrder);
      if (filteredRounds.length > 0) roundId = filteredRounds[0][attrRoundId];
      console.log({
        "method": "getActiveRoundId",
        filteredRounds,
        roundId
      });
    }
    return roundId;
  }
  static makePlayerDisplayName(playerId, index) {
    if (playerId == null) {
      return `Player ${index + 1}`;
    }
    const player = players.filter(player => player[attrPlayerId] == playerId)[0];
    const deckId = player[attrDeckId];
    const deck = deckId == null ? null : decks.filter(deck => deck[attrDeckId] == deckId)[0];
    const user = playerId == null ? null : users[player[attrUserId]];
    return player[flagName] || `${user == null ? 'Error' : user[flagName]} - ${deck == null ? 'Error' : deck[flagName]}`;
  }
  static renderCommanderDamageRows(playerId) {
    const activeRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
    const previousRoundIds = rounds.filter(round => round[flagDisplayOrder] <= activeRoundDisplayOrder).map(round => round[attrRoundId]);
    return players.filter(otherPlayer => otherPlayer[attrPlayerId] !== playerId).map(otherPlayer => {
      const sourceId = otherPlayer[attrPlayerId];
      let otherPlayerDisplayName = PageMtgGame.makePlayerDisplayName(sourceId);
      const totalDamage = damageRecords.filter(damage => damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == sourceId
      // &&  damage[attrRoundId] == roundId
      && previousRoundIds.includes(damage[attrRoundId])).map(damage => damage[flagLifeLoss]).reduce((acc, curr) => acc + curr, 0);
      const isLethal = totalDamage >= 21;
      return `
                    <div class="damage-row" data-player-id="${playerId}" data-source-id="${sourceId}">
                        <span class="damage-source">from ${otherPlayerDisplayName}</span>
                        <div class="damage-controls">
                            <button class="damage-btn damage-minus" data-player-id="${playerId}" data-source-id="${sourceId}">&minus;</button>
                            <input type="hidden" class="damage-value" data-player-id="${playerId}" data-source-id="${sourceId}" value="${totalDamage}">
                            <span class="damage-display ${isLethal ? 'lethal' : ''}" data-player-id="${playerId}" data-source-id="${sourceId}" ${attrValuePrevious}="${totalDamage}">${totalDamage}</span>
                            <button class="damage-btn damage-plus" data-player-id="${playerId}" data-source-id="${sourceId}">+</button>
                        </div>
                    </div>
                `;
    }).join('');
  }
  hookupGameRoundEvents() {
    let incrementRoundButtonSelector = '#gameSection .' + flagRow + '.' + flagRound + ' button.' + flagRoundDisplayOrderButton;
    Events.hookupEventHandler("click", incrementRoundButtonSelector, (event, button) => {
      const amount = button.classList.contains(flagRoundDisplayOrderPlus) ? 1 : -1;
      const roundDisplayOrderButtonContainer = button.parentElement;
      const roundDisplayOrderLabel = roundDisplayOrderButtonContainer.querySelector('span.' + flagRound + '.' + flagDisplayOrder);
      const currentRoundDisplayOrder = parseInt(DOM.getElementValueCurrent(roundDisplayOrderLabel));
      const newDisplayOrder = currentRoundDisplayOrder + amount;
      DOM.setElementValueCurrent(roundDisplayOrderLabel, newDisplayOrder);
      DOM.isElementDirty(roundDisplayOrderLabel);
      this.updateAndToggleShowButtonsSaveCancel();
      this.renderPlayers();
    });
  }
  static makeDefaultGameRound(displayOrder) {
    const newDisplayOrder = displayOrder != null ? displayOrder : 1 + Math.max(rounds.map(round => round[flagDisplayOrder]));
    return {
      [attrRoundId]: -newDisplayOrder,
      [attrGameId]: gameId,
      [flagNotes]: null,
      [flagDisplayOrder]: newDisplayOrder,
      [flagActive]: true
    };
  }
  hookupPlayerCardEvents() {
    // Life gain buttons
    let lifeGainButtonSelector = '.life-gain-btn';
    Events.hookupEventHandler("click", lifeGainButtonSelector, (event, button) => {
      const playerId = parseInt(button.dataset.playerId);
      const amount = parseInt(button.dataset.amount);
      const activeRoundId = PageMtgGame.getActiveRoundId();
      const damageIndex = damageRecords.findIndex(damage => damage[attrRoundId] == activeRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null);
      this.changeLife(playerId // playerId
      , amount // amount
      , true // isLifeGainNotLoss
      , true // updateDamage
      , damageIndex // damageIndex
      );
    });

    // Life loss buttons
    let lifeLossButtonSelector = '.life-loss-btn';
    Events.hookupEventHandler("click", lifeLossButtonSelector, (event, button) => {
      const playerId = parseInt(button.dataset.playerId);
      const amount = parseInt(button.dataset.amount);
      const activeRoundId = PageMtgGame.getActiveRoundId();
      const damageIndex = damageRecords.findIndex(damage => damage[attrRoundId] == activeRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null);
      this.changeLife(playerId // playerId
      , amount // amount
      , false // isLifeGainNotLoss
      , true // updateDamage
      , damageIndex // damageIndex
      );
    });

    // Commander death buttons
    let commanderDeathButtonSelector = '.death-btn';
    Events.hookupEventHandler("click", commanderDeathButtonSelector, (event, button) => {
      const playerId = parseInt(button.dataset.playerId);
      const isMinusButton = button.classList.contains('death-minus');
      const amount = isMinusButton ? -1 : 1;
      this.changeCommanderDeaths(playerId, amount);
    });

    // Commander damage buttons
    let commmanderDamageButtonSelector = '.damage-btn';
    Events.hookupEventHandler("click", commmanderDamageButtonSelector, (event, button) => {
      const playerId = parseInt(button.dataset.playerId);
      const sourceId = parseInt(button.dataset.sourceId);
      const isMinusButton = button.classList.contains('damage-minus');
      const amount = isMinusButton ? -1 : 1;
      this.changeCommanderDamage(playerId, sourceId, amount);
    });

    // Eliminate buttons
    let eliminatePlayerButtonSelector = '.eliminate-btn';
    Events.hookupEventHandler("click", eliminatePlayerButtonSelector, (event, button) => {
      const playerId = parseInt(button.dataset.playerId);
      this.toggleEliminate(playerId);
    });
  }
  changeLife(playerId, amount, isLifeGainNotLoss = false, updateDamage = false, damageIndex = null) {
    const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`);
    // if (!card || card.classList.contains('eliminated')) return;

    const lifeInput = card.querySelector(`.life-value[data-player-id="${playerId}"]`);
    const lifeDisplay = card.querySelector(`.life-display[data-player-id="${playerId}"]`);
    const currentLife = parseInt(lifeInput.value) || 0;
    const newLife = currentLife + amount * (isLifeGainNotLoss ? 1 : -1);
    DOM.setElementAttributeValueCurrent(lifeDisplay, newLife);
    DOM.isElementDirty(lifeDisplay);
    lifeInput.value = newLife;
    lifeDisplay.textContent = newLife;
    if (updateDamage) {
      let fieldFlag = isLifeGainNotLoss ? flagLifeGain : flagLifeLoss;
      damageRecords[damageIndex][fieldFlag] += amount;
    }
    PageMtgGame.renderCommanderDamageLog();

    // PageMtgGame.debouncedSave();
    this.updateAndToggleShowButtonsSaveCancel();
  }
  changeCommanderDamage(playerId, sourceId, amount) {
    const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`);
    // if (!card || card.classList.contains('eliminated')) return;

    const damageInput = card.querySelector(`.damage-value[data-player-id="${playerId}"][data-source-id="${sourceId}"]`);
    const damageDisplay = card.querySelector(`.damage-display[data-player-id="${playerId}"][data-source-id="${sourceId}"]`);
    const currentDamage = parseInt(damageInput.value) || 0;
    const newDamage = Math.max(0, currentDamage + amount);
    amount = newDamage - currentDamage;
    DOM.setElementAttributeValueCurrent(damageDisplay, newDamage);
    DOM.isElementDirty(damageDisplay);
    damageInput.value = newDamage;
    damageDisplay.textContent = newDamage;

    // Update lethal class
    if (newDamage >= 21) {
      damageDisplay.classList.add('lethal');
    } else {
      damageDisplay.classList.remove('lethal');
    }
    const activeRoundId = PageMtgGame.getActiveRoundId();
    const damageIndex = damageRecords.findIndex(damageRecord => damageRecord[attrRoundId] == activeRoundId && damageRecord[attrPlayerId] == playerId && damageRecord[attrReceivedFromCommanderPlayerId] == sourceId);
    damageRecords[damageIndex][flagLifeLoss] += amount;
    let isLifeGainNotLoss = false;
    this.changeLife(playerId // playerId
    , amount // amount
    , isLifeGainNotLoss // isLifeGainNotLoss
    , false // updateDamage
    , damageIndex // damageIndex
    );
    // PageMtgGame.debouncedSave();
  }
  changeCommanderDeaths(playerId, amount) {
    const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`);
    // if (!card || card.classList.contains('eliminated')) return;

    const deathDisplay = card.querySelector(`.death-display[data-player-id="${playerId}"]`);
    const currentDeaths = parseInt(deathDisplay.textContent) || 0;
    const newDeaths = Math.max(0, currentDeaths + amount);
    deathDisplay.textContent = newDeaths;
    DOM.setElementAttributeValueCurrent(deathDisplay, newDeaths);
    DOM.isElementDirty(deathDisplay);
    const activeRoundId = PageMtgGame.getActiveRoundId();
    const damageIndex = damageRecords.findIndex(damage => damage[attrRoundId] == activeRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null);
    damageRecords[damageIndex][flagCommanderDeaths] = newDeaths;
    PageMtgGame.renderCommanderDamageLog();

    // PageMtgGame.debouncedSave();
    this.updateAndToggleShowButtonsSaveCancel();
  }
  toggleEliminate(playerId) {
    const card = document.querySelector(`.player-card[data-player-id="${playerId}"]`);
    if (!card) return;
    const eliminateBtn = card.querySelector(`.eliminate-btn[data-player-id="${playerId}"]`);
    const wasEliminated = card.classList.contains('eliminated');
    if (wasEliminated) {
      card.classList.remove('eliminated');
      eliminateBtn.textContent = 'Eliminate';
    } else {
      card.classList.add('eliminated');
      eliminateBtn.textContent = 'Revive';
    }
    const isEliminated = card.classList.contains('eliminated');
    const activeRoundId = PageMtgGame.getActiveRoundId();
    const damageIndex = damageRecords.findIndex(damage => damage[attrRoundId] == activeRoundId && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] == null);
    damageRecords[damageIndex][flagIsEliminated] = isEliminated;
    DOM.setElementAttributeValueCurrent(eliminateBtn, isEliminated);
    DOM.isElementDirty(eliminateBtn);
    this.reorderPlayerCards();
    PageMtgGame.renderCommanderDamageLog();

    // PageMtgGame.debouncedSave();
    this.updateAndToggleShowButtonsSaveCancel();
  }
  reorderPlayerCards() {
    let playerGrid = document.getElementById('playersGrid');
    let currentPlayerCards = playerGrid.querySelectorAll('.player-card');
    let newPlayerCards = [];
    let playerCardMetas = [];
    currentPlayerCards.forEach((playerCard, index) => {
      newPlayerCards.push(playerCard.cloneNode(true));
      playerCardMetas.push({
        [flagIsEliminated]: playerCard.classList.contains(flagIsEliminated),
        [attrPlayerId]: playerCard.dataset["playerId"],
        [flagDisplayOrder]: index
      });
    });
    let activeRoundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
    let newPlayerGridInnerHTML = '';
    let playerIdA, playerIdB, isEliminatedAsIntA, isEliminatedAsIntB, playerA, playerB, indexPlayerCard;
    playerCardMetas.sort((a, b) => {
      playerIdA = a[attrPlayerId];
      playerIdB = b[attrPlayerId];
      isEliminatedAsIntA = PageMtgGame.isPlayerEliminated(playerIdA, activeRoundDisplayOrder) ? 1 : 0;
      isEliminatedAsIntB = PageMtgGame.isPlayerEliminated(playerIdB, activeRoundDisplayOrder) ? 1 : 0;
      playerA = players.filter(p => p[attrPlayerId] == playerIdA)[0];
      playerB = players.filter(p => p[attrPlayerId] == playerIdB)[0];
      return players.length * isEliminatedAsIntA + playerA[flagDisplayOrder] - (players.length * isEliminatedAsIntB + playerB[flagDisplayOrder]);
    }).forEach(playerCardMeta => {
      indexPlayerCard = playerCardMeta[flagDisplayOrder];
      newPlayerGridInnerHTML += newPlayerCards[indexPlayerCard].outerHTML;
    });
    playerGrid.innerHTML = newPlayerGridInnerHTML;
    playerGrid.querySelectorAll('.' + flagInitialised).forEach(initialisedElement => {
      initialisedElement.classList.remove(flagInitialised);
    });
    this.hookupPlayerCardEvents();
  }
  static isPlayerEliminated(playerId, roundDisplayOrder = null) {
    if (roundDisplayOrder == null) roundDisplayOrder = PageMtgGame.getActiveRoundDisplayOrder();
    const filteredRoundIds = rounds.filter(round => round[flagDisplayOrder] <= roundDisplayOrder).map(round => round[attrRoundId]);
    let hasDamageWithIsEliminated = damageRecords.filter(damage =>
    //     damage[attrRoundId] <= roundDisplayOrder
    filteredRoundIds.includes(damage[attrRoundId]) && damage[attrPlayerId] == playerId && damage[flagIsEliminated]).length > 0;
    let damageFromOtherPlayers = {};
    let otherPlayerId;
    damageRecords.filter(damage =>
    //     damage[attrRoundId] <= roundId
    filteredRoundIds.includes(damage[attrRoundId]) && damage[attrPlayerId] == playerId && damage[attrReceivedFromCommanderPlayerId] != null).forEach(damage => {
      otherPlayerId = damage[attrReceivedFromCommanderPlayerId];
      damageFromOtherPlayers[otherPlayerId] = damage[flagLifeLoss] + (damageFromOtherPlayers[otherPlayerId] == null ? 0 : damageFromOtherPlayers[otherPlayerId]);
    });
    let maxDamageFromOtherCommander = Object.keys(damageFromOtherPlayers).map(playerId => damageFromOtherPlayers[playerId]).reduce((acc, cur) => Math.max(acc, cur), 0);
    let totalDamageTaken = damageRecords.filter(damage =>
    //     damage[attrRoundId] <= roundId
    filteredRoundIds.includes(damage[attrRoundId]) && damage[attrPlayerId] == playerId).map(damage => damage[flagLifeLoss] - damage[flagLifeGain]).reduce((a, b) => a + b, 0);
    console.log({
      roundDisplayOrder,
      filteredRoundIds,
      hasDamageWithIsEliminated,
      maxDamageFromOtherCommander,
      totalDamageTaken
    });
    return hasDamageWithIsEliminated || maxDamageFromOtherCommander >= 21 || totalDamageTaken >= game[flagStartingLife];
  }
  static updatePlayerSetup() {
    const playerCountInput = document.getElementById('playerCount');
    if (!playerCountInput) return;
    const playerCount = parseInt(playerCountInput.value);
    const grid = document.getElementById('playerSetupGrid');
    if (!grid) return;
    grid.innerHTML = '';
    const wrapperTemplate = document.getElementById(playerSetupWrapperTemplateId);
    let player, wrapper, wrapperHeading, userDdl, deckDdl, nameInput;
    for (let i = 0; i < playerCount; i++) {
      if (i < players.length) {
        player = players[i];
      } else {
        player = PageMtgGame.makeDefaultGamePlayer();
        players.push(player);
      }
      wrapper = wrapperTemplate.cloneNode(true);
      wrapper.removeAttribute("id");
      wrapper.setAttribute(flagDisplayOrder, i + 1);
      wrapper.classList.remove(flagIsCollapsed);
      wrapperHeading = wrapper.querySelector('label');
      wrapperHeading.innerText = 'Player ' + (i + 1);
      userDdl = wrapper.querySelector('.playerUser select');
      DOM.setElementValuesCurrentAndPrevious(userDdl, player[attrUserId]);
      deckDdl = wrapper.querySelector('.playerDeck select');
      DOM.setElementValuesCurrentAndPrevious(deckDdl, player[attrDeckId]);
      nameInput = wrapper.querySelector('.playerName input');
      DOM.setElementValuesCurrentAndPrevious(nameInput, player[flagName]);
      console.log('player: ', player);
      grid.appendChild(wrapper);
    }
  }
  static makeDefaultGamePlayer() {
    return {
      [attrPlayerId]: -players.length,
      [attrGameId]: gameId,
      [attrUserId]: user[attrUserId],
      [attrDeckId]: 0,
      [flagName]: "",
      [flagNotes]: null,
      [flagDisplayOrder]: players.length,
      [flagActive]: true
    };
  }
  async startGame() {
    const playerCountInput = document.getElementById('playerCount');
    if (!playerCountInput) return;
    const playerCount = parseInt(playerCountInput.value);
    const playersToSave = [];
    let playerSetupWrapper, playerId, player, userDdl, userId, deckDdl, deckId, nameInput, name;
    for (let i = 0; i < playerCount; i++) {
      playerSetupWrapper = document.querySelector('.player-name-input-wrapper[' + flagDisplayOrder + '="' + (i + 1) + '"]');
      userDdl = playerSetupWrapper.querySelector('.playerUser select');
      deckDdl = playerSetupWrapper.querySelector('.playerDeck select');
      nameInput = playerSetupWrapper.querySelector('.playerName input');
      userId = DOM.getElementValueCurrent(userDdl);
      deckId = DOM.getElementValueCurrent(deckDdl);
      name = nameInput ? nameInput.value.trim() || null : null; // `Player ${i + 1}` : `Player ${i + 1}`;

      playerId = playerSetupWrapper.getAttribute(attrPlayerId);
      player = players.filter(p => p[attrPlayerId] == playerId)[0];
      playersToSave.push({
        ...player,
        [attrGameId]: gameId,
        [attrUserId]: userId,
        [attrDeckId]: deckId,
        [flagName]: name,
        [flagDisplayOrder]: i + 1,
        [flagActive]: true
      });
    }

    // Save players to server
    const comment = 'Save players';
    const self = this;
    API.saveGamePlayers(playersToSave, null, comment).then(data => {
      if (data[flagStatus] == flagSuccess) {
        self.leave();
        window.location.reload();
      } else {
        console.error('Failed to save players:', data[flagMessage]);
        PageMtgGame.showError('An error occurred while creating the game');
      }
    }).catch(error => {
      console.error('Error creating game:', error);
      PageMtgGame.showError('An error occurred while creating the game');
    }).finally(() => {});
  }
  static resetGame() {
    if (confirm('Are you sure you want to start a new game? Current game will be lost.')) {
      localStorage.removeItem(`mtgGame_${gameId}`);
      window.location.href = hashPageGames;
    }
  }
  async saveGame() {
    /*
    const gameState = {
          [flagPlayer]: players
        , [flagRound]: rounds
        , [flagDamage]: damageRecords
    };
    if (gameState[flagPlayer].length > 0) {
        localStorage.setItem(`mtgGame_${gameId}`, JSON.stringify(gameState));
        PageMtgGame.showSaveIndicator();
    }
    */
    const comment = 'Save player damage';
    const self = this;
    API.saveGameRoundPlayerDamages(rounds, damageRecords, null, comment).then(data => {
      if (data[flagStatus] == flagSuccess) {
        self.leave();
        window.location.reload();
      } else {
        console.error('Failed to save player damages:', data[flagMessage]);
        PageMtgGame.showError('An error occurred while saving player damages');
      }
    }).catch(error => {
      console.error('Error saving player damages:', error);
      PageMtgGame.showError('An error occurred while saving player damages');
    }).finally(() => {});
  }
  /*
  static debouncedSave() {
      clearTimeout(PageMtgGame._saveTimeout);
      PageMtgGame._saveTimeout = setTimeout(() => PageMtgGame.saveGame(), 500);
  }
   static showSaveIndicator() {
      const indicator = document.getElementById('saveIndicator');
      if (indicator) {
          indicator.classList.add('show');
          setTimeout(() => {
              indicator.classList.remove('show');
          }, 2000);
      }
  }
  */
  saveRecordsTableDirty() {
    this.saveGame();
  }
  static showError(message) {
    // Check if there's an overlay error element
    const errorOverlay = document.getElementById('overlayError');
    if (errorOverlay) {
      const errorLabel = errorOverlay.querySelector('.error-message, #labelError');
      if (errorLabel) {
        errorLabel.textContent = message;
      }
      errorOverlay.classList.remove('hidden');
      errorOverlay.style.display = 'flex';
    } else {
      // Fallback to alert
      alert(message);
    }
  }
  updateAndToggleShowButtonsSaveCancel() {
    // let pageBody = document.querySelector(idPageBody);
    let isDirty = DOM.hasDirtyChildrenContainer(pageBody);
    let buttonContainerSelector = '.' + flagContainer + '.' + flagSave + '.' + flagCancel;
    let buttonSave = document.querySelector(buttonContainerSelector + ' ' + idButtonSave);
    let areVisibleSaveCancelButtons = !buttonSave.classList.contains(flagIsCollapsed);
    console.log({
      pageBody,
      isDirty,
      areVisibleSaveCancelButtons
    });
    this.toggleShowButtonsSaveCancel(isDirty || areVisibleSaveCancelButtons);
  }
  leave() {
    super.leave();
  }
}

// Static timeout reference for debouncing
PageMtgGame._saveTimeout = null;
;// ./static/js/pages/tcg/mtg_games.js



class PageMtgGames extends TableBasePage {
  static hash = hashPageMtgGames;
  static attrIdRowObject = attrGameId;
  callSaveTableContent = API.saveGame;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  hookupFilters() {
    /*
    this.sharedHookupFilters();
    this.hookupFilterActive();
    */
  }
  loadRowTable(rowJson) {
    if (rowJson == null) return;
    if (_verbose) {
      utils_Utils.consoleLogIfNotProductionEnvironment("applying data row: ", rowJson);
    }
  }
  getJsonRow(row) {
    return;
  }
  initialiseRowNew(tbody, row) {}
  postInitialiseRowNewCallback(tbody) {}
  hookupTableMain() {
    super.hookupTableMain();
    // this.hookupTableMainRows();
    this.hookupTcgGames();
    // PageMtgGames.hideNewGameForm();
  }
  hookupTcgGames() {
    console.log("hookupTableMain PageMtgGames");
    // Initialize form submission
    const newGameForm = document.getElementById('newGameForm');
    if (newGameForm) {
      newGameForm.addEventListener('submit', this.handleNewGameSubmit.bind(this)); // () => { this.handleNewGameSubmit.bind(this); });
    }

    // Initialize filter form
    const filterForm = document.getElementById('formFilters');
    if (filterForm) {
      filterForm.addEventListener('submit', PageMtgGames.handleFilterSubmit);
    }

    // Close modal on escape key
    document.addEventListener('keydown', function (e) {
      if (e.key === 'Escape') {
        PageMtgGames.hideNewGameForm();
      }
    });

    // Close modal on backdrop click
    const modal = document.getElementById('newGameModal');
    if (modal) {
      modal.addEventListener('click', function (e) {
        if (e.target === modal) {
          PageMtgGames.hideNewGameForm();
        }
      });
    }

    // Button onclicks
    const newGameButton = document.getElementById('btnNewGame');
    if (newGameButton) {
      newGameButton.addEventListener('click', PageMtgGames.showNewGameForm);
    }
    const cancelNewGameButtons = document.querySelectorAll('#newGameForm .form-actions .btn-tcg.btn-tcg-secondary' + ',' + '#newGameModal .modal-content .modal-header .modal-close');
    if (cancelNewGameButtons.length > 0) {
      cancelNewGameButtons.forEach(button => {
        button.addEventListener('click', PageMtgGames.hideNewGameForm);
      });
    }
  }
  static showNewGameForm() {
    const modal = document.getElementById('newGameModal');
    if (modal) {
      modal.classList.remove(flagIsCollapsed);
      document.body.style.overflow = 'hidden';

      // Focus on first input
      const firstInput = modal.querySelector('input, select');
      if (firstInput) {
        firstInput.focus();
      }
    }
  }
  static hideNewGameForm() {
    const modal = document.getElementById('newGameModal');
    if (modal) {
      modal.classList.add(flagIsCollapsed);
      document.body.style.overflow = '';

      // Reset form
      const form = document.getElementById('newGameForm');
      if (form) {
        form.reset();
      }
    }
  }
  async handleNewGameSubmit(e) {
    e.preventDefault();
    const form = e.target;
    const formData = new FormData(form);
    const gameType = formData.get('game_type');
    const gameData = {
      [attrGameId]: -1,
      [flagIsCommander]: gameType === 'commander',
      [flagIsDraft]: gameType === 'draft',
      [flagIsSealed]: gameType === 'sealed',
      [flagLocationName]: formData.get(flagLocationName) || null,
      [flagNotes]: formData.get(flagNotes) || null,
      [flagStartOn]: new Date().toISOString(),
      [flagStartingLife]: formData.get(flagStartingLife) || 40,
      [flagActive]: true
    };
    const submitBtn = form.querySelector('button[type="submit"]');
    const originalText = submitBtn.textContent;
    submitBtn.textContent = 'Creating...';
    submitBtn.disabled = true;
    const games = [gameData];
    const comment = 'Create new game';
    API.saveGame(games, form, comment).then(data => {
      if (data[flagStatus] == flagSuccess) {
        if (_verbose) {
          utils_Utils.consoleLogIfNotProductionEnvironment('Records saved!');
          utils_Utils.consoleLogIfNotProductionEnvironment('Data received:', data);
        }
        const gamePageHash = `${hashPageGame}/${data[attrGameId]}`;
        let filtersJson = {};
        this.leave();
        API.goToHash(gamePageHash, filtersJson);
      } else {
        utils_Utils.consoleLogIfNotProductionEnvironment("error: " + data[flagMessage]);
        // OverlayError.show(data[flagMessage]);
        window.location.reload();
      }
    }).catch(error => {
      console.error('Error creating game:', error);
      PageMtgGames.showError('An error occurred while creating the game');
    }).finally(() => {
      submitBtn.textContent = originalText;
      submitBtn.disabled = false;
    });
  }
  static handleFilterSubmit(e) {
    // Let the form submit normally - it will reload with query params
    // You can add client-side filtering here if needed
  }
  static getCSRFToken() {
    // Try meta tag first
    const metaTag = document.querySelector('meta[name="csrf-token"]');
    if (metaTag) {
      return metaTag.getAttribute('content');
    }

    // Try hidden input
    const hiddenInput = document.querySelector('input[name="csrf_token"]');
    if (hiddenInput) {
      return hiddenInput.value;
    }

    // Try cookie
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
      const [name, value] = cookie.trim().split('=');
      if (name === 'csrf_token') {
        return value;
      }
    }
    return '';
  }
  static showError(message) {
    // Check if there's an overlay error element
    const errorOverlay = document.getElementById('overlayError');
    if (errorOverlay) {
      const errorLabel = errorOverlay.querySelector('.error-message, #labelError');
      if (errorLabel) {
        errorLabel.textContent = message;
      }
      errorOverlay.classList.remove(flagIsCollapsed);
      errorOverlay.style.display = 'flex';
    } else {
      // Fallback to alert
      alert(message);
    }
  }
  static showSuccess(message) {
    // Could implement a toast notification here
    console.log('Success:', message);
  }
  static joinGame(gameId) {
    window.location.href = `${hashPageGame}/${gameId}`;
  }
  static async deleteGame(gameId) {
    if (!confirm('Are you sure you want to delete this game? This action cannot be undone.')) {
      return;
    }
    try {
      const gameData = {
        'game_id': gameId,
        'active': false
      };
      const response = await fetch(hashSaveGame, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRFToken': PageMtgGames.getCSRFToken()
        },
        body: JSON.stringify({
          [flagGame]: [gameData],
          'form-filters': {},
          'comment': 'Game deleted'
        })
      });
      const result = await response.json();
      if (result.status === 'success') {
        // Remove the row from the table
        const row = document.querySelector(`tr[data-game-id="${gameId}"]`);
        if (row) {
          row.style.animation = 'tcg-fadeOut 0.3s ease-out forwards';
          setTimeout(() => row.remove(), 300);
        }
      } else {
        PageMtgGames.showError(result.message || 'Failed to delete game');
      }
    } catch (error) {
      console.error('Error deleting game:', error);
      PageMtgGames.showError('An error occurred while deleting the game');
    }
  }
  toggleShowButtonsSaveCancel() {}
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/tcg/mtg_home.js




class PageMtgHome extends BasePage {
  static hash = hashPageMtgHome;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
    this.hookupTcgHome();
  }
  hookupTcgHome() {}
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/legal/accessibility_report.js

class PageAccessibilityReport extends BasePage {
  static hash = hashPageAccessibilityReport;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/legal/accessibility_statement.js

class PageAccessibilityStatement extends BasePage {
  static hash = hashPageAccessibilityStatement;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/legal/license.js

class PageLicense extends BasePage {
  static hash = hashPageLicense;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/legal/privacy_policy.js

class PagePrivacyPolicy extends BasePage {
  static hash = hashPagePrivacyPolicy;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/legal/retention_schedule.js

class PageRetentionSchedule extends BasePage {
  static hash = hashPageDataRetentionSchedule;
  constructor(router) {
    super(router);
  }
  initialize() {
    this.sharedInitialize();
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/mixin.js
class MixinPage {
  constructor(pageCurrent) {
    this.page = pageCurrent;
  }
  initialize() {
    Utils.consoleLogIfNotProductionEnvironment('hookup start for ', this.page.hash);
    this.hookupFilters();
    this.hookupLocalStorage();
  }
  hookupFilters() {}
  hookupLocalStorage() {}
  leave() {}
}
;// ./static/js/pages/mixin_table.js

class TableMixinPage extends MixinPage {
  constructor(pageCurrent) {
    super(pageCurrent);
  }
  initialize() {
    super.initialize();
    this.hookupFilters();
    this.hookupTable();
  }
  hookupFilters() {
    // Implement filter-specific functionality here
  }
  hookupTable() {
    // Implement table-specific functionality here
  }
}
;// ./static/js/pages/user/user.js




class PageUser extends TableBasePage {
  static hash = hashPageUserAccount;
  static attrIdRowObject = attrUserId;
  callSaveTableContent = API.saveUsers;
  constructor(router) {
    super(router);
    this.mixin = new TableMixinPage(this);
  }
  initialize() {
    this.sharedInitialize();
    this.hookupTableMain();
  }
  hookupFilters() {}
  loadRowTable(rowJson) {
    if (rowJson == null) return;
    if (_verbose) {
      Utils.consoleLogIfNotProductionEnvironment("applying data row: ", rowJson);
    }
  }
  getTableRecords(dirtyOnly = false) {
    dirtyOnly = true;
    let container = document.querySelector('.' + flagCard + '.' + flagUser);
    return [this.getJsonRow(container)];
  }
  getJsonRow(container) {
    console.log("getJsonRow: ", container);
    if (container == null) return;
    let inputFirstname = container.querySelector(' #' + flagFirstname);
    let inputSurname = container.querySelector(' #' + flagSurname);
    let inputEmail = container.querySelector(' #' + flagEmail);
    let idUser = container.getAttribute(attrUserId);
    let jsonRow = {
      [attrUserAuth0Id]: null,
      [flagEmail]: null,
      [flagIsEmailVerified]: null,
      [flagIsSuperUser]: null,
      [flagCanAdminUser]: null
    };
    jsonRow[attrUserId] = idUser;
    jsonRow[flagFirstname] = DOM.getElementAttributeValueCurrent(inputFirstname);
    jsonRow[flagSurname] = DOM.getElementAttributeValueCurrent(inputSurname);
    jsonRow[flagEmail] = DOM.getElementAttributeValueCurrent(inputEmail);
    return jsonRow;
  }
  initialiseRowNew(tbody, row) {}
  postInitialiseRowNewCallback(tbody) {}
  hookupTableMain() {
    super.hookupTableMain();
    this.hookupFieldsFirstname();
    this.hookupFieldsSurname();
    this.hookupFieldsEmail();
  }
  hookupFieldsFirstname() {
    this.hookupChangeHandlerTableCells('.' + flagCard + '.' + flagUser + ' #' + flagFirstname);
  }
  hookupFieldsSurname() {
    this.hookupChangeHandlerTableCells('.' + flagCard + '.' + flagUser + ' #' + flagSurname);
  }
  hookupFieldsEmail() {
    this.hookupChangeHandlerTableCells('.' + flagCard + '.' + flagUser + ' #' + flagEmail);
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/pages/user/users.js





class PageUsers extends TableBasePage {
  static hash = hashPageUserAccounts;
  static attrIdRowObject = attrUserId;
  callSaveTableContent = API.saveUsers;
  constructor(router) {
    super(router);
    this.mixin = new TableMixinPage(this);
  }
  initialize() {
    this.sharedInitialize();
  }
  hookupFilters() {
    this.sharedHookupFilters();
    this.hookupFilterActive();
  }
  loadRowTable(rowJson) {
    if (rowJson == null) return;
    if (_verbose) {
      utils_Utils.consoleLogIfNotProductionEnvironment("applying data row: ", rowJson);
    }
  }
  getJsonRow(row) {
    if (row == null) return;
    let inputFirstname = row.querySelector('td.' + flagFirstname + ' .' + flagFirstname);
    let inputSurname = row.querySelector('td.' + flagSurname + ' .' + flagSurname);
    let inputNotes = row.querySelector('td.' + flagNotes + ' .' + flagNotes);
    let buttonActive = row.querySelector('td.' + flagActive + ' .' + flagActive);
    let jsonRow = {
      [attrUserAuth0Id]: null,
      [flagEmail]: null,
      [flagIsEmailVerified]: null,
      [flagIsSuperUser]: null,
      [flagCanAdminUser]: null
    };
    jsonRow[attrUserId] = row.getAttribute(attrUserId);
    jsonRow[flagFirstname] = DOM.getElementAttributeValueCurrent(inputFirstname);
    jsonRow[flagSurname] = DOM.getElementAttributeValueCurrent(inputSurname);
    jsonRow[flagNotes] = DOM.getElementAttributeValueCurrent(inputNotes);
    jsonRow[flagActive] = buttonActive.classList.contains(flagDelete);
    console.log("jsonRow");
    console.log(jsonRow);
    return jsonRow;
  }
  initialiseRowNew(tbody, row) {}
  postInitialiseRowNewCallback(tbody) {
    let newRows = tbody.querySelectorAll('tr.' + flagRowNew);
    let newestRow = newRows[0];
    let clickableElementsSelector = [].join('');
    newestRow.querySelectorAll(clickableElementsSelector).forEach(clickableElement => {
      clickableElement.click();
    });
  }
  hookupTableMain() {
    super.hookupTableMain();
    this.hookupFieldsFirstname();
    this.hookupFieldsSurname();
    this.hookupFieldsNotesTable();
    this.hookupFieldsActive();
  }
  hookupFieldsFirstname() {
    this.hookupChangeHandlerTableCells(flagFirstname);
  }
  hookupFieldsSurname() {
    this.hookupChangeHandlerTableCells(flagSurname);
  }
  leave() {
    super.leave();
  }
}
;// ./static/js/router.js
// Pages
// Core
// TCG




// Legal





// User
// import PageUserLogin from './pages/user/login.js';
// import PageUserLogout from './pages/user/logout.js';





class Router {
  constructor() {
    // Pages
    this.pages = {};
    // Core
    // TCG
    this.pages[hashPageMtgDecks] = {
      name: 'PageMtgDecks',
      module: PageMtgDecks
    };
    this.pages[hashPageMtgGame] = {
      name: 'PageMtgGame',
      module: PageMtgGame
    };
    this.pages[hashPageMtgGames] = {
      name: 'PageMtgGames',
      module: PageMtgGames
    };
    this.pages[hashPageMtgHome] = {
      name: 'PageMtgGame',
      module: PageMtgHome
    };
    // Legal
    this.pages[hashPageAccessibilityStatement] = {
      name: 'PageAccessibilityStatement',
      module: PageAccessibilityStatement
    };
    this.pages[hashPageDataRetentionSchedule] = {
      name: 'PageDataRetentionSchedule',
      module: PageRetentionSchedule
    };
    this.pages[hashPageLicense] = {
      name: 'PageLicense',
      module: PageLicense
    };
    this.pages[hashPagePrivacyPolicy] = {
      name: 'PagePrivacyPolicy',
      module: PagePrivacyPolicy
    };
    // User
    // this.pages[hashPageUserLogin] = { name: 'PageUserLogin', module: PageUserLogin }; // pathModule: './pages/user/login.js' };
    // this.pages[hashPageUserLogout] = { name: 'PageUserLogout', module: PageUserLogout }; // pathModule: './pages/user/logout.js' };
    this.pages[hashPageUserAccount] = {
      name: 'PageUser',
      module: PageUser
    };
    this.pages[hashPageUserAccounts] = {
      name: 'PageUsers',
      module: PageUsers
    };
    // Routes
    this.routes = {};
    // Core
    // TCG
    this.routes[hashPageMtgDecks] = (isPopState = false) => this.navigateToHash(hashPageMtgDecks, isPopState);
    this.routes[hashPageMtgGame] = (isPopState = false) => this.navigateToHash(hashPageMtgGame, isPopState);
    this.routes[hashPageMtgGames] = (isPopState = false) => this.navigateToHash(hashPageMtgGames, isPopState);
    this.routes[hashPageMtgHome] = (isPopState = false) => this.navigateToHash(hashPageMtgHome, isPopState);
    // Legal
    this.routes[hashPageAccessibilityStatement] = (isPopState = false) => this.navigateToHash(hashPageAccessibilityStatement, isPopState);
    this.routes[hashPageDataRetentionSchedule] = (isPopState = false) => this.navigateToHash(hashPageDataRetentionSchedule, isPopState);
    this.routes[hashPageLicense] = (isPopState = false) => this.navigateToHash(hashPageLicense, isPopState);
    this.routes[hashPagePrivacyPolicy] = (isPopState = false) => this.navigateToHash(hashPagePrivacyPolicy, isPopState);
    // User
    // this.routes[hashPageUserLogin] = (isPopState = false) => this.navigateToHash(hashPageUserLogin, isPopState);
    // this.routes[hashPageUserLogout] = (isPopState = false) => this.navigateToHash(hashPageUserLogout, isPopState);
    this.routes[hashPageUserAccount] = (isPopState = false) => this.navigateToHash(hashPageUserAccount, isPopState);
    this.routes[hashPageUserAccounts] = (isPopState = false) => this.navigateToHash(hashPageUserAccounts, isPopState);
    this.initialize();
  }
  loadPage(hashPage, isPopState = false) {
    const PageClass = this.getClassPageFromHash(hashPage);
    this.currentPage = new PageClass(this);
    this.currentPage.initialize(isPopState);
    window.addEventListener('beforeunload', () => this.currentPage.leave());
  }
  getClassPageFromHash(hashPage) {
    let pageJson = this.pages[hashPage];
    try {
      const module = pageJson.module;
      return module;
    } catch (error) {
      utils_Utils.consoleLogIfNotProductionEnvironment("this.pages: ", this.pages);
      console.error('Page not found:', hashPage);
      throw error;
    }
  }
  initialize() {
    window.addEventListener('popstate', this.handlePopState.bind(this));
  }
  handlePopState(event) {
    this.loadPageCurrent();
  }
  loadPageCurrent() {
    const hashPageCurrent = DOM.getHashPageCurrent();
    this.loadPage(hashPageCurrent);
  }
  navigateToHash(hash, data = null, params = null, isPopState = false) {
    let url = API.getUrlFromHash(hash, params);
    history.pushState({
      data: data,
      params: params
    }, '', hash);
    API.goToUrl(url, data);
  }
  navigateToUrl(url, data = null, appendHistory = true) {
    // this.beforeLeave();
    if (appendHistory) history.pushState(data, '', url);
    url = API.parameteriseUrl(url, data);
    API.goToUrl(url);
  }
  static loadPageBodyFromResponse(response) {
    DOM.loadPageBody(response.data);
  }
}
const router = new Router();
;// ./static/js/app.js




class App {
  constructor() {
    this.dom = new DOM();
    this.router = new Router();
  }
  initialize() {
    this.setupEventListeners();
    this.start();
  }
  setupEventListeners() {
    // document.addEventListener('click', this.handleGlobalClick.bind(this));
  }
  handleGlobalClick(event) {}
  start() {
    this.initPageCurrent();
  }
  initPageCurrent() {
    this.router.loadPageCurrent();
  }
}
const app = new App();
function domReady(fn) {
  if (document.readyState !== 'loading') {
    fn();
  } else {
    document.addEventListener('DOMContentLoaded', fn);
  }
}
domReady(() => {
  app.initialize();
});
window.app = app;
/* harmony default export */ const js_app = ((/* unused pure expression or super */ null && (app)));
})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

// This entry needs to be wrapped in an IIFE because it needs to be isolated against other entry modules.
(() => {
// extracted by mini-css-extract-plugin

})();

/******/ })()
;
//# sourceMappingURL=main.bundle.js.map