* BePo Personalbeschaffer

Wirbt benötigtes Personal für eine BePo-Wache an.

// ==UserScript==
// @name        * BePo Personalbeschaffer
// @namespace   bos-ernie.leitstellenspiel.de
// @version     1.1.0
// @license     BSD-3-Clause
// @author      BOS-Ernie
// @description Wirbt benötigtes Personal für eine BePo-Wache an.
// @match       https://www.leitstellenspiel.de/buildings/*/hire
// @match       https://polizei.leitstellenspiel.de/buildings/*/hire
// @icon        https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
// @run-at      document-idle
// @grant       none
// @resource    https://forum.leitstellenspiel.de/index.php?thread/24767-script-vorschau-bepo-personalbeschaffer-by-bos-ernie/
// ==/UserScript==

/* global $, loadedBuildings */

(function () {
  const minimalRemainingPersonnelInPoliceStation = 34;

  // https://api.lss-manager.de/de_DE/schoolings
  const personnelSettingsInternal = [
    {
      caption: "Ohne Ausbildung",
      key: null,
      numberOfRequiredPersonnel: 83,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "Hundeführer (Schutzhund)",
      key: "k9",
      numberOfRequiredPersonnel: 6,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "Hundertschaftsführer (FüKw)",
      key: "police_fukw",
      numberOfRequiredPersonnel: 15,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "MEK",
      key: "police_mek",
      numberOfRequiredPersonnel: 42,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "SEK",
      key: "police_sek",
      numberOfRequiredPersonnel: 42,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "Reiterstaffel",
      key: "police_horse",
      numberOfRequiredPersonnel: 24,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "Wasserwerfer",
      key: "police_wasserwerfer",
      numberOfRequiredPersonnel: 15,
      numberOfSelectedPersonnel: 0,
    },
    {
      caption: "Zugführer (leBefKw)",
      key: "police_einsatzleiter",
      numberOfRequiredPersonnel: 12,
      numberOfSelectedPersonnel: 0,
    },
  ];

  const personnelSettingsProxy = personnelSettingsInternal.map(setting => {
    return new Proxy(setting, {
      set: function (target, key, value) {
        target[key] = value;
        updateFooter(target.key, target.numberOfSelectedPersonnel);
        return true;
      },
    });
  });

  function initPanelBodies() {
    const elements = document.getElementsByClassName("panel-body");
    for (let i = 0; i < elements.length; i++) {
      elements[i].classList.add("hidden");
    }
  }
  function removePanelHeadingClickEvent() {
    const elements = document.getElementsByClassName("personal-select-heading");
    for (let i = 0; i < elements.length; i++) {
      elements[i].replaceWith(elements[i].cloneNode(true));
      elements[i].addEventListener("click", panelHeadingClickEvent);
    }
  }

  function addFooter() {
    const wrapper = document.createElement("div");
    wrapper.style = "display: flex; flex-wrap: wrap; flex-direction: row; column-gap: 15px";

    const list = document.createElement("ul");
    list.classList.add("list-inline");
    list.style = "color: #fff;padding-top: 8px;";

    for (let i = 0; i < personnelSettingsProxy.length; i++) {
      const setting = personnelSettingsProxy[i];

      list.appendChild(createTotalSummaryElement(setting));
    }

    wrapper.appendChild(list);

    const nav = document.querySelector(".navbar.navbar-default.navbar-fixed-bottom");

    nav.children[0].children[0].insertAdjacentElement("afterend", wrapper);
  }

  function updateFooter(key, selectedPersonnel) {
    document.getElementById("number-of-selected-personnel-" + key).innerHTML = selectedPersonnel;

    const requiredPersonnel = personnelSettingsProxy.find(setting => setting.key === key).numberOfRequiredPersonnel;

    const labelClass = selectedPersonnel === requiredPersonnel ? "label-success" : "label-warning";

    const spanPersonnel = document.getElementById("personnel-" + key);

    spanPersonnel.classList.remove("label-success", "label-warning");
    spanPersonnel.classList.add(labelClass);
  }

  function addClickEventHandlerToCheckboxes() {
    const inputElements = document.getElementsByClassName("schooling_checkbox");

    for (let i = 0; i < inputElements.length; i++) {
      inputElements[i].addEventListener("change", updateNumberOfSelectedPersonnel);
    }
  }

  function updateNumberOfSelectedPersonnel(event) {
    const attributes = Object.keys(event.target.attributes);

    let attributeIndex = attributes.find(key => event.target.attributes[key].value === "true");
    let key = null;
    if (attributeIndex !== undefined) {
      key = event.target.attributes[attributeIndex].name;
    }

    const setting = personnelSettingsProxy.find(setting => setting.key === key);

    if (event.target.checked) {
      setting.numberOfSelectedPersonnel = setting.numberOfSelectedPersonnel + 1;
    } else {
      setting.numberOfSelectedPersonnel = setting.numberOfSelectedPersonnel - 1;
    }

    const buildingId = event.target.getAttribute("building_id");

    const panelHeading = document.querySelector(".personal-select-heading[building_id='" + buildingId + "']");
  }

  function addPersonnelSelector() {
    let elements = document.getElementsByClassName("panel-heading personal-select-heading");
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i];
      const buildingId = element.getAttribute("building_id");
      element.children[1].prepend(createPersonnelSelector(buildingId));
    }
  }

  function createPersonnelSelector(buildingId) {
    const trashIcon = document.createElement("span");
    trashIcon.classList.add("glyphicon", "glyphicon-trash");

    const resetButton = document.createElement("button");
    resetButton.classList.add("btn", "btn-xs", "btn-default", "personnel-reset-button");
    resetButton.setAttribute("type", "button");
    resetButton.setAttribute("data-building-id", buildingId);
    resetButton.addEventListener("click", resetPersonnelClick);
    resetButton.appendChild(trashIcon);

    const buttonGroup = document.createElement("div");
    buttonGroup.classList.add("btn-group", "btn-group-xs");
    buttonGroup.setAttribute("role", "group");
    buttonGroup.appendChild(resetButton);
    buttonGroup.appendChild(createSelectButton(buildingId));

    return buttonGroup;
  }

  function createSelectButton(buildingId) {
    const userIcon = document.createElement("span");
    userIcon.classList.add("glyphicon", "glyphicon-user");

    const button = document.createElement("button");
    button.classList.add("btn", "btn-xs", "btn-default", "personnel-select-button");
    button.setAttribute("id", "personnel-select-button-" + buildingId);
    button.setAttribute("type", "button");
    button.setAttribute("data-building-id", buildingId);
    button.addEventListener("click", selectPersonnelClick);
    button.innerText = " Personal auswählen";
    button.prepend(userIcon);

    return button;
  }

  function createTotalSummaryElement(setting) {
    const listItem = document.createElement("li");

    const spanCaption = document.createElement("span");
    spanCaption.innerHTML = setting.caption + ": ";

    const spanSelected = document.createElement("span");
    spanSelected.setAttribute("id", "number-of-selected-personnel-" + setting.key);
    spanSelected.innerHTML = "0";

    const spanRequired = document.createElement("span");
    spanRequired.setAttribute("id", "number-of-required-personnel-" + setting.key);
    spanRequired.innerHTML = setting.numberOfRequiredPersonnel;

    const spanPersonnel = document.createElement("span");
    spanPersonnel.setAttribute("id", "personnel-" + setting.key);
    spanPersonnel.classList.add("label", "label-warning");
    spanPersonnel.appendChild(spanSelected);
    spanPersonnel.appendChild(document.createTextNode("/"));
    spanPersonnel.appendChild(spanRequired);

    listItem.appendChild(spanCaption);
    listItem.appendChild(spanPersonnel);

    return listItem;
  }

  async function selectPersonnelClick(event) {
    event.preventDefault();

    const button = event.target.closest("button");

    button.disabled = true;
    button.classList.remove("btn-default");
    button.classList.add("btn-success");

    const okIcon = document.createElement("span");
    okIcon.classList.add("glyphicon", "glyphicon-ok");
    button.replaceChild(okIcon, button.children[0]);

    const buildingId = button.dataset.buildingId;
    await selectPersonnel(buildingId);

    const panelBody = getPanelBody(buildingId);
    const numberOfSelectedPersonnel = panelBody.querySelectorAll("input:checked").length;

    button.innerHTML = button.innerHTML + " (" + numberOfSelectedPersonnel + ")";
  }

  async function resetPersonnelClick(event) {
    event.preventDefault();

    const resetButton = event.target.closest("button");
    const buildingId = resetButton.dataset.buildingId;

    const selectButton = createSelectButton(buildingId);

    document.getElementById("personnel-select-button-" + buildingId).replaceWith(selectButton);

    await resetPersonnel(buildingId);
  }

  async function selectPersonnel(buildingId) {
    await panelHeadingClick(buildingId);

    for (let i = personnelSettingsProxy.length - 1; i >= 0; i--) {
      const setting = personnelSettingsProxy[i];

      const panelBody = getPanelBody(buildingId);
      let inputElements = [];
      if (setting.key === null) {
        const schoolingCells = panelBody.querySelectorAll("td[id^='school_personal_education_']");
        for (let j = 0; j < schoolingCells.length; j++) {
          const schoolingCell = schoolingCells[j];
          // Personnel with anything but whitespace in schooling column, have a schooling and should not be selected
          if (schoolingCell.innerHTML.replace(/\s/g, "").length > 0) {
            continue;
          }

          inputElements.push(schoolingCell.parentElement.children[0].children[0]);
        }
      } else {
        inputElements = panelBody.querySelectorAll("input[" + setting.key + "='true']");
      }

      inputElements = Array.from(inputElements).filter(function (element) {
        if (typeof element === "undefined") {
          return false;
        }

        return element.parentElement.parentElement.children[3].innerHTML.replace(/\s/g, "").length === 0;
      });

      let j = inputElements.length - 1;
      while (setting.numberOfSelectedPersonnel < setting.numberOfRequiredPersonnel && j >= 0) {
        inputElements[j].click();
        --j;

        if (
          setting.key === null &&
          inputElements.length - setting.numberOfSelectedPersonnel <= minimalRemainingPersonnelInPoliceStation
        ) {
          break;
        }
      }
    }
  }

  function resetPersonnel(buildingId) {
    const panelBody = getPanelBody(buildingId);
    const inputElements = panelBody.querySelectorAll("input:checked");

    for (let i = 0; i < inputElements.length; i++) {
      inputElements[i].click();
    }
  }

  async function panelHeadingClickEvent(event) {
    // Skip redundant panelHeadingClick call which is handled by button click event
    if (
      event.target.classList.contains("personnel-select-button") ||
      event.target.classList.contains("glyphicon-trash")
    ) {
      return;
    }

    let buildingIdElement = event.target.outerHTML.match(/building_id="(\d+)"/);
    if (buildingIdElement === null) {
      buildingIdElement =
        event.target.parentElement.parentElement.parentElement.parentElement.outerHTML.match(/building_id="(\d+)"/);
    }

    await panelHeadingClick(buildingIdElement[1], true);
  }

  async function panelHeadingClick(buildingId, toggle = false) {
    const panelHeading = getPanelHeading(buildingId);
    const panelBody = getPanelBody(buildingId);
    const href = panelHeading.outerHTML.match(/href="([^"]+)"/)[1];

    if (loadedBuildings.indexOf(href) > -1) {
      if (toggle) {
        togglePanelBody(panelBody);
      }

      return;
    }

    loadedBuildings.push(href);
    await $.get(href, function (data) {
      panelBody.innerHTML = data;
    });

    const schoolingSelectAvailableButtons = panelBody.getElementsByClassName("schooling_select_available");
    for (let i = 0; i < schoolingSelectAvailableButtons.length; i++) {
      schoolingSelectAvailableButtons[i].parentElement.remove();
    }

    addClickEventHandlerToCheckboxes();

    if (toggle) {
      showPanelBody(panelBody);
    }
  }

  function togglePanelBody(panelBody) {
    if (panelBody.classList.contains("hidden")) {
      panelBody.classList.remove("hidden");
    } else {
      panelBody.classList.add("hidden");
    }
  }

  function showPanelBody(panelBody) {
    if (panelBody.classList.contains("hidden")) {
      panelBody.classList.remove("hidden");
    }
  }

  function getPanelHeading(buildingId) {
    return document.querySelector(".personal-select-heading[building_id='" + buildingId + "']");
  }

  function getPanelBody(buildingId) {
    return document.querySelector(".panel-body[building_id='" + buildingId + "']");
  }

  function main() {
    if (!window.location.href.match(/\/buildings\/\d+\/hire/)) {
      return;
    }

    const h1 = document.querySelector("h1[building_type]");
    if (!h1 || h1.getAttribute("building_type") !== "11") {
      return;
    }

    initPanelBodies();
    removePanelHeadingClickEvent();
    addPersonnelSelector();
    addFooter();
  }

  main();
})();