* Personalzuweiser 2.0

Weist benötigtes Personal einem Fahrzeug zu.

  1. // ==UserScript==
  2. // @name * Personalzuweiser 2.0
  3. // @namespace bos-ernie.leitstellenspiel.de
  4. // @version 2.5.1
  5. // @license BSD-3-Clause
  6. // @author BOS-Ernie
  7. // @description Weist benötigtes Personal einem Fahrzeug zu.
  8. // @match https://*.leitstellenspiel.de/vehicles/*/zuweisung
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
  10. // @run-at document-idle
  11. // @grant none
  12. // @resource https://forum.leitstellenspiel.de/index.php?thread/27234-script-personalzuweiser-2-0/
  13. // ==/UserScript==
  14.  
  15. /* global $, I18n */
  16.  
  17. (async function () {
  18. const assignButtonHotkey = "s";
  19. const resetButtonHotkey = "x";
  20. const buildingButtonHotkey = "w";
  21. const previousVehicleButtonHotkey = "a";
  22. const nextVehicleButtonHotkey = "d";
  23.  
  24. const assignMostSeniorPersonnelFirst = true;
  25.  
  26. /*
  27. * Um die Personalzuweisung für einen Fahrzeugtyp zu überschreiben, entferne die Kommentare am entsprechenden Block
  28. * oder füge einen neuen hinzu. Wenn du Fragen zur Konfiguration hast, melde dich im Forum.
  29. *
  30. * Erklärung der Felder:
  31. * {
  32. * id: 53, // ID des Fahrzeugtyps
  33. * caption: "Dekon-P", // Name des Fahrzeugtyps
  34. * maxStaff: 6, // Maximale Fahrzeugbesatzung
  35. * training: [ // Benötigte Lehrgänge
  36. * {
  37. * key: "dekon_p", // Schlüssel des benötigten Lehrgangs
  38. * number: 6, // Anzahl des benötigten Lehrgangs
  39. * },
  40. * ],
  41. * }
  42. */
  43. const vehiclesConfigurationOverride = [
  44. // {
  45. // id: 53,
  46. // caption: "Dekon-P",
  47. // maxStaff: 6,
  48. // training: [
  49. // {
  50. // key: "dekon_p",
  51. // number: 6,
  52. // },
  53. // ],
  54. // },
  55. // {
  56. // id: 134,
  57. // caption: "Pferdetransporter klein",
  58. // maxStaff: 4,
  59. // training: [
  60. // {
  61. // key: "police_horse",
  62. // number: 4,
  63. // },
  64. // ],
  65. // },
  66. // {
  67. // id: 135,
  68. // caption: "Pferdetransporter groß",
  69. // maxStaff: 2,
  70. // training: [
  71. // {
  72. // key: "police_horse",
  73. // number: 2,
  74. // },
  75. // ],
  76. // },
  77. // {
  78. // id: 137,
  79. // caption: "Zugfahrzeug Pferdetransport",
  80. // maxStaff: 6,
  81. // training: [
  82. // {
  83. // key: "police_horse",
  84. // number: 6,
  85. // },
  86. // ],
  87. // },
  88. ];
  89.  
  90. let vehiclesConfiguration = [];
  91.  
  92. const storageKey = "bos-ernie.personnel-allocator.vehicle-type-configurations";
  93. const storageTtl = 24 * 60 * 60 * 1000;
  94.  
  95. function transformVehiclesData(data) {
  96. return Object.entries(data)
  97. .filter(([id, vehicle]) => !vehicle.isTrailer)
  98. .map(([id, vehicle]) => {
  99. const trainingMap = {};
  100.  
  101. if (vehicle.staff && vehicle.staff.training) {
  102. for (const trainings of Object.values(vehicle.staff.training)) {
  103. for (const [trainingKey, trainingInfo] of Object.entries(trainings)) {
  104. if (trainingInfo.min !== 0) {
  105. trainingMap[trainingKey] = trainingInfo.min ? trainingInfo.min : vehicle.maxPersonnel;
  106. }
  107. }
  108. }
  109. }
  110.  
  111. return {
  112. id: Number(id),
  113. caption: vehicle.caption,
  114. maxStaff: vehicle.maxPersonnel,
  115. training: Object.entries(trainingMap).map(([key, number]) => ({
  116. key,
  117. number,
  118. })),
  119. };
  120. });
  121. }
  122.  
  123. async function initVehiclesConfiguration() {
  124. const storedVehiclesConfiguration = localStorage.getItem(storageKey);
  125.  
  126. if (storedVehiclesConfiguration) {
  127. const cachedData = JSON.parse(storedVehiclesConfiguration);
  128.  
  129. if (cachedData.lastUpdate > new Date().getTime() - storageTtl) {
  130. vehiclesConfiguration = applyVehicleConfigurationOverride(cachedData.data);
  131. return;
  132. }
  133. }
  134.  
  135. try {
  136. const response = await fetch("https://api.lss-manager.de/de_DE/vehicles");
  137. if (!response.ok) {
  138. throw new Error("Network response was not ok");
  139. }
  140. const data = await response.json();
  141. vehiclesConfiguration = applyVehicleConfigurationOverride(transformVehiclesData(data));
  142.  
  143. localStorage.setItem(
  144. storageKey,
  145. JSON.stringify({
  146. lastUpdate: new Date().getTime(),
  147. data: vehiclesConfiguration,
  148. }),
  149. );
  150. } catch (error) {
  151. console.error("Error fetching and transforming vehicles data:", error);
  152. }
  153. }
  154.  
  155. function applyVehicleConfigurationOverride(vehiclesConfiguration) {
  156. return vehiclesConfiguration.map(vehicle => {
  157. const override = vehiclesConfigurationOverride.find(override => override.id === vehicle.id);
  158. return override ? override : vehicle;
  159. });
  160. }
  161.  
  162. function observeNumberOfAssignedPersonnelMutations() {
  163. const targetNode = document.getElementById("count_personal");
  164. const config = { attributes: true, childList: true, subtree: true };
  165.  
  166. const callback = function (mutationsList, observer) {
  167. for (const mutation of mutationsList) {
  168. if (mutation.type === "childList") {
  169. updateNumberOfAssignedPersonnelDecoration();
  170. }
  171. }
  172. };
  173.  
  174. const observer = new MutationObserver(callback);
  175. observer.observe(targetNode, config);
  176. }
  177.  
  178. function updateNumberOfAssignedPersonnelDecoration() {
  179. const assignedPersonsElement = getAssignedPersonsElement();
  180. const vehicleCapacity = parseInt(assignedPersonsElement.parentElement.firstElementChild.innerText);
  181.  
  182. let numberOfAssignedPersonnel = parseInt(assignedPersonsElement.innerText);
  183. let numberOfPersonnelToAssign = vehicleCapacity - numberOfAssignedPersonnel;
  184.  
  185. if (numberOfPersonnelToAssign <= 0) {
  186. assignedPersonsElement.classList.remove("label-warning");
  187. assignedPersonsElement.classList.add("label-success");
  188. } else {
  189. assignedPersonsElement.classList.remove("label-success");
  190. assignedPersonsElement.classList.add("label-warning");
  191. }
  192. }
  193.  
  194. async function assign() {
  195. const vehicleTypeId = getVehicleTypeId();
  196. if (vehicleTypeId === null) {
  197. return;
  198. }
  199.  
  200. const vehicleConfiguration = vehiclesConfiguration.find(vehicle => vehicle.id === vehicleTypeId);
  201. const vehicleCapacity = vehicleConfiguration.maxStaff;
  202.  
  203. const assignedPersonsElement = getAssignedPersonsElement();
  204. let numberOfAssignedPersonnel = parseInt(assignedPersonsElement.innerText);
  205. let numberOfPersonnelToAssign = vehicleCapacity - numberOfAssignedPersonnel;
  206.  
  207. if (numberOfPersonnelToAssign <= 0) {
  208. return;
  209. }
  210.  
  211. for (const training of vehicleConfiguration.training) {
  212. if (numberOfPersonnelToAssign === 0) {
  213. break;
  214. }
  215.  
  216. numberOfPersonnelToAssign -= await assignPersonnel(training.key, training.number);
  217. }
  218.  
  219. if (numberOfPersonnelToAssign > 0) {
  220. await assignPersonnel(null, numberOfPersonnelToAssign);
  221. }
  222. }
  223.  
  224. function getAvailableWithTraining(identifier) {
  225. const rows = document.querySelectorAll("tr[data-filterable-by]");
  226.  
  227. return Array.from(rows).filter(row => {
  228. const filterData = row
  229. .getAttribute("data-filterable-by")
  230. .replace(/"/g, "")
  231. .replace(/[\[\]]/g, "")
  232. .split(",")
  233. .map(item => item.trim());
  234.  
  235. const isInTraining = row.children[2].innerText.startsWith("Im Unterricht");
  236.  
  237. if (identifier === null) {
  238. return filterData.length === 1 && filterData[0] === "" && !isInTraining;
  239. }
  240.  
  241. return filterData.includes(identifier) && !isInTraining;
  242. });
  243. }
  244.  
  245. async function assignPersonnel(identifier, number) {
  246. let numberOfPersonnelAssigned = 0;
  247. if (number === 0) {
  248. return numberOfPersonnelAssigned;
  249. }
  250.  
  251. const rowsNotInTraining = getAvailableWithTraining(identifier);
  252.  
  253. if (assignMostSeniorPersonnelFirst) {
  254. rowsNotInTraining.reverse();
  255. }
  256.  
  257. for (const row of rowsNotInTraining) {
  258. if (numberOfPersonnelAssigned === number) {
  259. break;
  260. }
  261.  
  262. const button = row.querySelector("a.btn-success");
  263.  
  264. if (!button) {
  265. continue;
  266. }
  267.  
  268. const personalId = button.getAttribute("personal_id");
  269. const personalElement = document.getElementById(`personal_${personalId}`);
  270. personalElement.innerHTML = `<td colspan="4">${I18n.t("common.loading")}</td>`;
  271.  
  272. const response = await fetch(button.href, {
  273. method: "POST",
  274. headers: {
  275. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  276. "x-csrf-token": document.querySelector("meta[name=csrf-token]").content,
  277. "x-requested-with": "XMLHttpRequest",
  278. },
  279. });
  280.  
  281. if (!response.ok) {
  282. throw new Error("HTTP Fehler! Statuscode: " + response.status);
  283. }
  284.  
  285. personalElement.innerHTML = await response.text();
  286.  
  287. numberOfPersonnelAssigned++;
  288.  
  289. const assignedPersonsElement = getAssignedPersonsElement();
  290. getAssignedPersonsElement().innerText = parseInt(assignedPersonsElement.innerText) + 1;
  291.  
  292. await new Promise(r => setTimeout(r, 50));
  293. }
  294.  
  295. return numberOfPersonnelAssigned;
  296. }
  297.  
  298. async function reset() {
  299. const selectButtons = document.getElementsByClassName("btn btn-default btn-assigned");
  300.  
  301. // Since the click event removes the button from the DOM, only every second item would be clicked.
  302. // To prevent this, the loop is executed backwards.
  303. for (let i = selectButtons.length - 1; i >= 0; i--) {
  304. selectButtons[i].click();
  305. // Wait 250ms to prevent possible race conditions
  306. await new Promise(r => setTimeout(r, 250));
  307. }
  308. }
  309.  
  310. function assignClickEvent(event) {
  311. assign();
  312. event.preventDefault();
  313. }
  314.  
  315. function resetClickEvent(event) {
  316. reset();
  317. event.preventDefault();
  318. }
  319.  
  320. function getAssignedPersonsElement() {
  321. return document.getElementById("count_personal");
  322. }
  323.  
  324. function addButtonGroup() {
  325. let okIcon = document.createElement("span");
  326. okIcon.className = "glyphicon glyphicon-ok";
  327.  
  328. let assignButton = document.createElement("button");
  329. assignButton.type = "button";
  330. assignButton.className = "btn btn-success";
  331. assignButton.appendChild(okIcon);
  332. assignButton.addEventListener("click", assignClickEvent);
  333.  
  334. let resetIcon = document.createElement("span");
  335. resetIcon.className = "glyphicon glyphicon-trash";
  336.  
  337. let resetButton = document.createElement("button");
  338. resetButton.type = "button";
  339. resetButton.className = "btn btn-danger";
  340. resetButton.appendChild(resetIcon);
  341. resetButton.addEventListener("click", resetClickEvent);
  342.  
  343. let buttonGroup = document.createElement("div");
  344. buttonGroup.id = "vehicle-assigner-button-group";
  345. buttonGroup.className = "btn-group";
  346. buttonGroup.style = "margin-left: 5px";
  347. buttonGroup.appendChild(assignButton);
  348. buttonGroup.appendChild(resetButton);
  349.  
  350. // Append button group to element with class "vehicles-education-filter-box"
  351. document.getElementsByClassName("vehicles-education-filter-box")[0].appendChild(buttonGroup);
  352. }
  353.  
  354. function getVehicleId() {
  355. return window.location.pathname.split("/")[2];
  356. }
  357.  
  358. function getVehicleTypeId() {
  359. const vehicleId = getVehicleId();
  360. const request = new XMLHttpRequest();
  361. request.open("GET", `/api/v2/vehicles/${vehicleId}`, false);
  362. request.send(null);
  363.  
  364. if (request.status === 200) {
  365. const vehicle = JSON.parse(request.responseText);
  366. return vehicle.result.vehicle_type;
  367. }
  368.  
  369. return null;
  370. }
  371.  
  372. function removeEventListenersOfAssignButtons() {
  373. const personalTable = document.getElementById("personal_table");
  374.  
  375. const buttons = personalTable.querySelectorAll("a.btn");
  376. for (let button of buttons) {
  377. button = button.cloneNode(true);
  378. button.replaceWith(button);
  379. }
  380. }
  381.  
  382. async function main() {
  383. await initVehiclesConfiguration();
  384.  
  385. observeNumberOfAssignedPersonnelMutations();
  386. removeEventListenersOfAssignButtons();
  387.  
  388. addButtonGroup();
  389.  
  390. document.addEventListener("keydown", function (event) {
  391. const activeElement = document.activeElement;
  392. if (activeElement.tagName.toLowerCase() !== "body") {
  393. return;
  394. }
  395.  
  396. const key = event.key.toLocaleLowerCase();
  397. const buildingElement = document.querySelector("#iframe-inside-container ol.breadcrumb a");
  398. switch (key) {
  399. case assignButtonHotkey:
  400. assign();
  401. break;
  402. case resetButtonHotkey:
  403. reset();
  404. break;
  405. case previousVehicleButtonHotkey:
  406. document.querySelectorAll(".btn-group.pull-right a")[0].click();
  407. break;
  408. case nextVehicleButtonHotkey:
  409. document.querySelectorAll(".btn-group.pull-right a")[1].click();
  410. break;
  411.  
  412. case buildingButtonHotkey:
  413. if (buildingElement) {
  414. buildingElement.click();
  415. }
  416. break;
  417. }
  418. });
  419. }
  420.  
  421. main();
  422. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址