* 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.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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