* Personalzuweiser

Weist maximal mögliche Anzahl an Personal einem Fahrzeug zu. Originalskript von BOS-Ernie, angepasst und erweitert, zur Unterstützung der neusten Lehrgänge, durch leeSalami.

// ==UserScript==
// @name        * Personalzuweiser
// @namespace   bos-ernie.leitstellenspiel.de
// @version     2.6.8
// @license     BSD-3-Clause
// @author      BOS-Ernie, leeSalami
// @description Weist maximal mögliche Anzahl an Personal einem Fahrzeug zu. Originalskript von BOS-Ernie, angepasst und erweitert, zur Unterstützung der neusten Lehrgänge, durch leeSalami.
// @match       https://*.leitstellenspiel.de/vehicles/*/zuweisung
// @match       https://*.leitstellenspiel.de/buildings/*
// @match       https://*.meldkamerspel.com/vehicles/*/zuweisung
// @match       https://*.meldkamerspel.com/buildings/*
// @exclude     /new$/
// @exclude     /personals$/
// @exclude     /edit$/
// @exclude     /move$/
// @require     https://update.greasyfork.org/scripts/516844/API-Speicher.user.js
// @icon        https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
// @run-at      document-idle
// @grant       unsafeWindow
// ==/UserScript==

(async function () {
  'use strict';

  // You can customize the hotkeys according to your needs. Helpful tool to find the codes: https://www.toptal.com/developers/keycode
  const ASSIGN_PERSONNEL_HOTKEY = 'KeyK';
  const RESET_PERSONNEL_HOTKEY = 'KeyL';
  const PREVIOUS_HOTKEY = 'ArrowLeft';
  const NEXT_HOTKEY = 'ArrowRight';

  const CUSTOM_VEHICLE_TRAINING = {
    150: { 'no_training': true },
  };

  let running = false;

  if (document.getElementById('personal_table')) {
    document.addEventListener('keydown', (e) => {
      if (e.code === ASSIGN_PERSONNEL_HOTKEY) {
        assign();
      } else if (e.code === RESET_PERSONNEL_HOTKEY) {
        reset();
      }
    });

    if (document.querySelector('.btn-group.pull-right:has(a[href^="/vehicles/"][href$="/zuweisung"])') !== null) {
      document.addEventListener('keydown', (e) => {
        if (e.code === PREVIOUS_HOTKEY) {
          const previousVehicleButton = getVehicleNavigationButton(1);

          if (previousVehicleButton !== null) {
            previousVehicleButton.click();
          }
        } else if (e.code === NEXT_HOTKEY) {
          const nextVehicleButton = getVehicleNavigationButton(2);

          if (nextVehicleButton !== null) {
            nextVehicleButton.click();
          }
        }
      });
    }
  } else if (document.getElementById('building-navigation-container') !== null) {
    document.addEventListener('keydown', (e) => {
      if (e.code === PREVIOUS_HOTKEY) {
        const previousBuildingButton = getBuildingNavigationButton(1);

        if (previousBuildingButton !== null) {
          previousBuildingButton.click();
        }
      } else if (e.code === NEXT_HOTKEY) {
        const nextBuildingButton = getBuildingNavigationButton(3);

        if (nextBuildingButton !== null) {
          nextBuildingButton.click();
        }
      }
    });

    return;
  } else {
    return;
  }

  updatePersonalCount();
  const csrfToken = document.querySelector('meta[name=csrf-token]')?.content;

  if (!csrfToken) {
    return;
  }

  let db;
  const loadingText = I18n.t('common.loading');
  addButtonGroup();

  async function assign() {
    if (running) {
      return;
    }

    running = true;
    const assignedPersonsElement = getAssignedPersonsElement();
    const numberOfAssignedPersonnel = parseInt(assignedPersonsElement.innerText, 10);
    const vehicleCapacity = parseInt(assignedPersonsElement.parentElement.firstElementChild.innerText, 10);

    let numberOfPersonnelToAssign = vehicleCapacity - numberOfAssignedPersonnel;
    const vehicleTypeId = await getVehicleTypeId();

    if (numberOfPersonnelToAssign > 0 && vehicleTypeId !== null) {
      db = await openDb();
      await updateVehicleTypes(db);
      const vehicleTraining = await getIdentifierByVehicleTypeId(vehicleTypeId);

      if (vehicleTraining === null) {
        await personnelAssigned();
        return;
      }

      const trainingCount = Object.keys(vehicleTraining).length;

      if (trainingCount === 0) {
        await personnelAssigned();
        return;
      }

      if (trainingCount !== 1 || !('no_training' in vehicleTraining)) {
        const rowsWithTraining = document.querySelectorAll('tbody tr:not([data-filterable-by="[]"]):has(a.btn-success):not(:has(span[data-education-key]))');
        const sortedRowsWithTraining = sortRows(rowsWithTraining);

        for (let i = 0, n = sortedRowsWithTraining.length; i < n; i++) {
          let hasIdentifier = true;
          let currentIdentifier = null;

          for (const educationIdentifier in vehicleTraining) {
            if (educationIdentifier === 'no_training') {
              continue;
            }

            const rowHasTraining = sortedRowsWithTraining[i].dataset.filterableBy.includes('"' + educationIdentifier + '"');

            if (vehicleTraining[educationIdentifier] === true && !rowHasTraining) {
              hasIdentifier = false;
            } else if (typeof vehicleTraining[educationIdentifier] === 'number') {
              if (!rowHasTraining || vehicleTraining[educationIdentifier] <= 0) {
                hasIdentifier = false;
              } else {
                hasIdentifier = true;
                currentIdentifier = educationIdentifier;
                break;
              }
            }
          }

          if (!hasIdentifier) {
            continue;
          }

          if (await changeAssignment(sortedRowsWithTraining[i].querySelector('a.btn-success'))) {
            numberOfPersonnelToAssign--;

            if (currentIdentifier) {
              vehicleTraining[currentIdentifier]--;
            }

            if (numberOfPersonnelToAssign === 0) {
              break;
            }

            await new Promise(r => setTimeout(r, 5));
          }
        }
      }
      if (numberOfPersonnelToAssign === 0) {
        await personnelAssigned();
        return;
      }

      if ('no_training' in vehicleTraining) {
        if (typeof vehicleTraining['no_training'] === 'number') {
          numberOfPersonnelToAssign = Math.min(numberOfPersonnelToAssign, vehicleTraining['no_training']);
        }

        await assignPersonsWithoutTraining(numberOfPersonnelToAssign);
      }
    }

    await personnelAssigned();
  }

  async function personnelAssigned() {
    const counterElement = document.getElementById('count_personal');
    const vehicleCapacity = parseInt(counterElement.parentElement.firstElementChild.innerText, 10);

    if (parseInt(counterElement.innerText, 10) !== vehicleCapacity) {
      if (unsafeWindow.resetIncompleteVehicles) {
        await reset();
      }
      document.dispatchEvent(new CustomEvent('personnel-assignment-incomplete'));
    } else {
      document.dispatchEvent(new CustomEvent('personnel-assignment-complete'));
    }

    document.dispatchEvent(new CustomEvent('personnel-assigned'));
    running = false;
  }

  function personnelReset() {
    document.dispatchEvent(new CustomEvent('personnel-reset'));
    running = false;
  }

  async function assignPersonsWithoutTraining(amount) {
    const rowsWithoutTraining = document.querySelectorAll('tbody tr[data-filterable-by="[]"]:has(a.btn-success):not(:has(span[data-education-key]))');
    const sortedRowsWithoutTraining = sortRows(rowsWithoutTraining);

    for (let i = 0, n = sortedRowsWithoutTraining.length; i < n; i++) {
      if (await changeAssignment(sortedRowsWithoutTraining[i].querySelector('a.btn-success'))) {
        amount--;

        if (amount === 0) {
          break;
        }

        await new Promise(r => setTimeout(r, 5));
      }
    }
  }

  async function changeAssignment(button) {
    if (button) {
      const personalId = button.getAttribute('personal_id');
      const personalElement = document.getElementById(`personal_${personalId}`);
      personalElement.innerHTML = `<td colspan="4">${loadingText}</td>`;

      try {
        const response = await fetch(button.href, {
          method: 'POST',
          headers: {
            'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'x-csrf-token': csrfToken,
            'x-requested-with': 'XMLHttpRequest',
          },
        });

        if (!response.ok) {
          return false;
        }

        personalElement.innerHTML = await response.text();
        updatePersonalCount();

        return true;
      } catch (e) {
        return false;
      }
    }

    return false
  }

  function updatePersonalCount() {
    const counterElement = document.getElementById('count_personal');

    if (!counterElement) {
      return;
    }

    const counter = document.querySelectorAll('.btn-assigned').length;
    const vehicleCapacity = parseInt(counterElement.parentElement.firstElementChild.innerText, 10);
    counterElement.innerText = String(counter);

    if (counter !== vehicleCapacity) {
      counterElement.classList.remove('label-success');
      counterElement.classList.add('label-warning');
    } else {
      counterElement.classList.remove('label-warning');
      counterElement.classList.add('label-success');
    }
  }

  function sortRows(rows) {
    const vehicleId = getVehicleId();

    return Array.from(rows)
      .sort((a, b) => {
        const aEducationLength = a.dataset.filterableBy.split(',').length - 1;
        const bEducationLength = b.dataset.filterableBy.split(',').length - 1;
        const aInVehicle = a.querySelector('td:nth-child(3) a')?.href?.endsWith('/' + vehicleId);
        const bInVehicle = b.querySelector('td:nth-child(3) a')?.href?.endsWith('/' + vehicleId);

        if (aEducationLength < bEducationLength) {
          return -1;
        } else if (aEducationLength > bEducationLength) {
          return 1;
        } else if ((aInVehicle === true && !bInVehicle) || (aInVehicle === undefined && bInVehicle === false)) {
          return -1;
        } else if (aInVehicle === bInVehicle) {
          return 0;
        } else {
          return 1;
        }
    });
  }

  async function reset() {
    if (running) {
      return;
    }

    running = true;
    const selectButtons = document.getElementsByClassName('btn btn-default btn-assigned');

    // Since the click event removes the button from the DOM, only every second item would be clicked.
    // To prevent this, the loop is executed backwards.
    for (let i = selectButtons.length - 1; i >= 0; i--) {
      await changeAssignment(selectButtons[i]);
      await new Promise(r => setTimeout(r, 5));
    }

    personnelReset();
  }

  function assignClickEvent(event) {
    assign();
    event.preventDefault();
  }

  function resetClickEvent(event) {
    reset();
    event.preventDefault();
  }

  function getAssignedPersonsElement() {
    return document.getElementById("count_personal");
  }

  function addButtonGroup() {
    const okIcon = document.createElement("span");
    okIcon.className = "glyphicon glyphicon-ok";

    const assignButton = document.createElement("button");
    assignButton.type = "button";
    assignButton.className = "btn btn-default";
    assignButton.id = "assign_personnel";
    assignButton.appendChild(okIcon);
    assignButton.addEventListener("click", assignClickEvent);

    const resetIcon = document.createElement("span");
    resetIcon.className = "glyphicon glyphicon-trash";

    const resetButton = document.createElement("button");
    resetButton.type = "button";
    resetButton.className = "btn btn-default";
    resetButton.id = "reset_assigned_personnel";
    resetButton.appendChild(resetIcon);
    resetButton.addEventListener("click", resetClickEvent);

    const buttonGroup = document.createElement("div");
    buttonGroup.id = "vehicle-assigner-button-group";
    buttonGroup.className = "btn-group";
    buttonGroup.style.marginLeft = "5px";
    buttonGroup.appendChild(assignButton);
    buttonGroup.appendChild(resetButton);

    // Append button group to element with class "vehicles-education-filter-box"
    document.getElementsByClassName("vehicles-education-filter-box")[0].appendChild(buttonGroup);
  }

  function getVehicleId() {
    return window.location.pathname.split("/")[2];
  }

  /**
   * @return {number|null}
   */
  async function getVehicleTypeId() {
    const vehicleTypeId = document.getElementById('back_to_vehicle')?.getAttribute('vehicle_type_id');

    if (vehicleTypeId) {
      return parseInt(vehicleTypeId, 10);
    }

    const vehicleId = getVehicleId();
    try {
      const response = await fetch(`/api/v2/vehicles/${vehicleId}`);

      if (response.ok) {
        return (await response.json()).result.vehicle_type;
      }
    } catch (e) {
      return null;
    }

    return null;
  }

  function getVehicleNavigationButton(number) {
    return document.querySelector('.btn-group.pull-right:has(a[href^="/vehicles/"][href$="/zuweisung"]) > a[href^="/vehicles/"][href$="/zuweisung"]:nth-child(' + number + ')')
  }

  function getBuildingNavigationButton(number) {
    return document.querySelector('#building-navigation-container:has(a[href^="/buildings/"]) > a[href^="/buildings/"]:nth-child(' + number + ')')
  }

  /**
   * @return {{}|null}
   */
  async function getIdentifierByVehicleTypeId(vehicleTypeId, allowTrailer = false, maxPersonnel = null) {
    if (vehicleTypeId in CUSTOM_VEHICLE_TRAINING) {
      return CUSTOM_VEHICLE_TRAINING[vehicleTypeId];
    }

    const vehicle = await getData(db, 'vehicleTypes', vehicleTypeId);

    if (!vehicle || (allowTrailer === false && vehicle['isTrailer'] === true)) {
      return null;
    }

    if (maxPersonnel === null) {
      maxPersonnel = vehicle['maxPersonnel'];
    }

    if (!('training' in vehicle['staff'])) {
      const vehicles = await getAllData(db, 'vehicleTypes');
      for (const vehicleId in vehicles) {
        if ('tractiveVehicles' in vehicles[vehicleId] && vehicles[vehicleId]['tractiveVehicles'].includes(vehicleTypeId) && 'training' in vehicles[vehicleId]['staff'] && vehicles[vehicleId]['tractiveVehicles'].length <= 5) {
          return getIdentifierByVehicleTypeId(parseInt(vehicleId, 10), true, maxPersonnel);
        }
      }

      return { 'no_training': true };
    }

    const trainings = vehicle['staff']['training'][Object.keys(vehicle['staff']['training'])[0]];
    const trainingKeys = Object.keys(trainings);
    const trainingsCount = trainingKeys.length;
    let training = {};
    let personnelToTrain = 0;
    let trainingAtScene = 0;
    let isHelicopter = trainingKeys.some(key => key.includes('helicopter'));

    if ('trainingAtScene' in vehicle['staff']) {
      trainingAtScene = vehicle['staff']['trainingAtScene'];
    }

    for (let i = 0; i < trainingsCount; i++) {
      let trainingCount = trainingAtScene;

      if (trainingCount >= maxPersonnel || isHelicopter) {
        trainingCount = true;
      } else if (trainingCount === 0) {
        trainingCount = trainings[trainingKeys[i]][Object.keys(trainings[trainingKeys[i]])[0]];
      }

      if (trainingCount !== true && trainingsCount === 1 && trainingCount < maxPersonnel && trainingKeys[i] !== 'notarzt') {
        trainingCount = true;
      }

      if (trainingCount === true) {
        personnelToTrain = maxPersonnel;
      } else {
        personnelToTrain += trainingCount;
      }

      training[trainingKeys[i]] = trainingCount;
    }

    if (personnelToTrain !== maxPersonnel) {
      training['no_training'] = maxPersonnel - personnelToTrain;
    }

    return training;
  }

  document.dispatchEvent(new CustomEvent('personnel-init'));
  unsafeWindow.personnelInit = true;
})();