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

2024-07-12 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

// ==UserScript==
// @name        * Personalzuweiser
// @namespace   bos-ernie.leitstellenspiel.de
// @version     2.4.2
// @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/*
// @icon        https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
// @run-at      document-idle
// @grant       none
// ==/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 },
  };

  if (document.querySelector('.btn-group.pull-right:has(a[href^="/vehicles/"][href$="/zuweisung"])') !== null) {
    document.addEventListener('keydown', (e) => {
      if (e.code === ASSIGN_PERSONNEL_HOTKEY) {
        assign();
      } else if (e.code === RESET_PERSONNEL_HOTKEY) {
        reset();
      } else 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;
  }

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

  async function assign() {
    const assignedPersonsElement = getAssignedPersonsElement();
    const numberOfAssignedPersonnel = parseInt(assignedPersonsElement.innerText);
    const vehicleCapacity = parseInt(assignedPersonsElement.parentElement.firstElementChild.innerText);

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

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

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

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

      if (trainingCount === 0) {
        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) {
        return;
      }

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

        await assignPersonsWithoutTraining(numberOfPersonnelToAssign);
      }
    }
  }

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

  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.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.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}
   */
  function getVehicleTypeId() {
    const vehicleId = getVehicleId();
    const request = new XMLHttpRequest();
    request.open("GET", `/api/v2/vehicles/${vehicleId}`, false);
    request.send();

    if (request.status === 200) {
      const vehicle = JSON.parse(request.responseText);
      return vehicle.result.vehicle_type;
    }

    return null;
  }

  async function fetchVehicles() {
    let aVehicles = null;
    let aVehiclesStored = null;

    if (localStorage.aVehicles) {
      aVehiclesStored = JSON.parse(localStorage.aVehicles);
    }

    if (!aVehiclesStored || aVehiclesStored.lastUpdate < (new Date().getTime() - 60 * 60 * 1000) || aVehiclesStored.userId !== user_id) {
      try {
        const vehicles = await (await fetch(`https://api.lss-manager.de/${I18n.locale}/vehicles`)).json();

        if (vehicles) {
          localStorage.setItem('aVehicles', JSON.stringify({lastUpdate: new Date().getTime(), value: vehicles, userId: user_id}));
          aVehicles = vehicles
        }
      } catch(e) {
        if (aVehiclesStored && aVehiclesStored.userId === user_id) {
          aVehicles = aVehiclesStored.value;
        }
      }
    } else {
      aVehicles = aVehiclesStored.value;
    }

    return aVehicles;
  }

  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 vehicles = await fetchVehicles();
    const vehicleTypeIdString = vehicleTypeId.toString();

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

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

    if (!('training' in vehicles[vehicleTypeIdString]['staff'])) {
      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), true, maxPersonnel);
        }
      }

      return { 'no_training': true };
    }

    const trainings = vehicles[vehicleTypeIdString]['staff']['training'][Object.keys(vehicles[vehicleTypeIdString]['staff']['training'])[0]];
    const trainingsCount = Object.keys(trainings).length;
    let training = {};
    let personnelToTrain = 0;
    let trainingAtScene = 0;

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

    for (const trainingKey in trainings) {
      if (trainings.hasOwnProperty(trainingKey)) {
        let trainingCount = trainingAtScene;

        if (trainingCount === 0) {
          trainingCount = trainings[trainingKey][Object.keys(trainings[trainingKey])[0]];
        } else if (trainingCount >= maxPersonnel) {
          trainingCount = true;
        }

        if (trainingCount !== true && trainingsCount === 1 && trainingCount < maxPersonnel && trainingKey !== 'notarzt') {
          trainingCount = true;
        }

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

        training[trainingKey] = trainingCount;
      }
    }

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

    return training;
  }
})();