* 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();
})();