* Personaler

Überprüft das Personalsoll und die Werbephasen in allen Gebäuden

  1. // ==UserScript==
  2. // @name * Personaler
  3. // @namespace bos-ernie.leitstellenspiel.de
  4. // @version 1.1.0
  5. // @license BSD-3-Clause
  6. // @author BOS-Ernie
  7. // @description Überprüft das Personalsoll und die Werbephasen in allen Gebäuden
  8. // @match https://www.leitstellenspiel.de/
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=leitstellenspiel.de
  10. // @run-at document-idle
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. /* global $ */
  15.  
  16. (function () {
  17. "use strict";
  18.  
  19. const defaultSettings = {
  20. "building-type-0": 250,
  21. "building-type-2": 80,
  22. "building-type-6": 300,
  23. "building-type-9": 140,
  24. "building-type-11": 215,
  25. "building-type-12": 34,
  26. "building-type-13": 400,
  27. "building-type-18": 250,
  28. "building-type-19": 300,
  29. "building-type-20": 80,
  30. };
  31.  
  32. const buildingsWithPersonal = [0, 2, 6, 9, 11, 12, 13, 18, 19, 20];
  33.  
  34. function addStyle() {
  35. const style =
  36. ".loader{width:100px;height:100px;border-radius:100%;position:relative;margin:0 auto;top:40px;left:-2.5px}.loader span{display:inline-block;width:5px;height:20px;background-color:#c9302c}.loader span:first-child{animation:1s ease-in-out infinite grow}.loader span:nth-child(2){animation:1s ease-in-out .15s infinite grow}.loader span:nth-child(3){animation:1s ease-in-out .3s infinite grow}.loader span:nth-child(4){animation:1s ease-in-out .45s infinite grow}@keyframes grow{0%,100%{-webkit-transform:scaleY(1);-ms-transform:scaleY(1);-o-transform:scaleY(1);transform:scaleY(1)}50%{-webkit-transform:scaleY(1.8);-ms-transform:scaleY(1.8);-o-transform:scaleY(1.8);transform:scaleY(1.8)}}";
  37.  
  38. const styleElement = document.createElement("style");
  39. styleElement.innerHTML = style;
  40. document.head.appendChild(styleElement);
  41. }
  42.  
  43. function addMenuEntry() {
  44. const profileMenu = document.getElementById("logout_button").parentElement.parentElement;
  45. const divider = document.createElement("li");
  46. divider.setAttribute("class", "divider");
  47. divider.setAttribute("role", "presentation");
  48.  
  49. profileMenu.append(divider);
  50.  
  51. const bedIcon = document.createElement("span");
  52. bedIcon.setAttribute("class", "glyphicon glyphicon-user");
  53.  
  54. const button = document.createElement("a");
  55. button.setAttribute("href", "javascript: void(0)");
  56. button.setAttribute("id", "personaler-button");
  57. button.append(bedIcon);
  58. button.append(" Personaler");
  59. button.addEventListener("click", menuEntryClick);
  60.  
  61. const li = document.createElement("li");
  62. li.appendChild(button);
  63.  
  64. profileMenu.append(li);
  65. }
  66.  
  67. function addModal() {
  68. const modal = document.createElement("div");
  69. modal.className = "modal fade";
  70. modal.id = "personaler-modal";
  71. modal.setAttribute("tabindex", "-1");
  72. modal.setAttribute("role", "dialog");
  73. modal.setAttribute("aria-labelledby", "personaler-modal-label");
  74. modal.setAttribute("aria-hidden", "true");
  75. modal.style.zIndex = "5000";
  76. modal.innerHTML = `<div class="modal-dialog modal-lg" role="document" style="width: 1280px;">
  77. <div class="modal-content">
  78. <div class="modal-header">
  79. <h1 class="modal-title" id="personaler-modal-label">
  80. <span class="glyphicon glyphicon-user" aria-hidden="true"></span> Personaler
  81. </h1>
  82. <button type="button" class="close" data-dismiss="modal" aria-label="Close">
  83. <span aria-hidden="true">&times;</span>
  84. </button>
  85. </div>
  86. <div class="modal-body" style="max-height: calc(100vh - 212px);overflow-y: auto;">
  87. <ul class="nav nav-tabs" role="tablist" style="margin-bottom: 10px">
  88. <li role="presentation" class="active">
  89. <a
  90. href="#tab-buildings-with-incorrect-personal-count-target"
  91. aria-controls="tab-buildings-with-incorrect-personal-count-target"
  92. role="tab"
  93. data-toggle="tab"
  94. >
  95. Gebäude mit falschem Personal (Soll)
  96. </a>
  97. </li>
  98. <li role="presentation">
  99. <a
  100. href="#tab-buildings-without-automatic-hiring"
  101. aria-controls="tab-buildings-without-automatic-hiring"
  102. role="tab"
  103. data-toggle="tab"
  104. >
  105. Gebäude ohne automatische Werbung
  106. </a>
  107. </li>
  108. <li role="presentation">
  109. <a href="#tab-settings" aria-controls="tab-settings" role="tab" data-toggle="tab">
  110. Einstellungen
  111. </a>
  112. </li>
  113. </ul>
  114. <div>
  115. <div class="tab-content">
  116. <div role="tabpanel" class="tab-pane active" id="tab-buildings-with-incorrect-personal-count-target">
  117. <div id="buildings-with-incorrect-personal-count-target">
  118. <div class="row">
  119. <div class="col-md-12 bg">
  120. <div class="loader">
  121. <span></span>
  122. <span></span>
  123. <span></span>
  124. <span></span>
  125. </div>
  126. </div>
  127. </div>
  128. </div>
  129. </div>
  130. <div role="tabpanel" class="tab-pane" id="tab-buildings-without-automatic-hiring">
  131. <div id="buildings-without-automatic-hiring">
  132. <div class="row">
  133. <div class="col-md-12 bg">
  134. <div class="loader">
  135. <span></span>
  136. <span></span>
  137. <span></span>
  138. <span></span>
  139. </div>
  140. </div>
  141. </div>
  142. </div>
  143. </div>
  144. <div role="tabpanel" class="tab-pane" id="tab-settings">
  145. <div id="settings">
  146. <h2>Personal (Soll)</h2>
  147. <form id="settings" class="form-horizontal">
  148. <div class="form-group">
  149. <label for="building-type-0" class="col-sm-4 control-label">
  150. Feuerwache
  151. </label>
  152. <div class="col-sm-1">
  153. <input type="number" class="form-control" id="building-type-0" min="0" max="400" />
  154. </div>
  155. </div>
  156. <div class="form-group">
  157. <label for="building-type-18" class="col-sm-4 control-label">
  158. Feuerwache (Kleinwache)
  159. </label>
  160. <div class="col-sm-1">
  161. <input type="number" class="form-control" id="building-type-18" min="0" max="400" />
  162. </div>
  163. </div>
  164. <div class="form-group">
  165. <label for="building-type-2" class="col-sm-4 control-label">
  166. Rettungswache
  167. </label>
  168. <div class="col-sm-1">
  169. <input type="number" class="form-control" id="building-type-2" min="0" max="400" />
  170. </div>
  171. </div>
  172. <div class="form-group">
  173. <label for="building-type-20" class="col-sm-4 control-label">
  174. Rettungswache (Kleinwache)
  175. </label>
  176. <div class="col-sm-1">
  177. <input type="number" class="form-control" id="building-type-20" min="0" max="400" />
  178. </div>
  179. </div>
  180. <div class="form-group">
  181. <label for="building-type-6" class="col-sm-4 control-label">
  182. Polizeiwache
  183. </label>
  184. <div class="col-sm-1">
  185. <input type="number" class="form-control" id="building-type-6" min="0" max="400" />
  186. </div>
  187. </div>
  188. <div class="form-group">
  189. <label for="building-type-19" class="col-sm-4 control-label">
  190. Polizeiwache (Kleinwache)
  191. </label>
  192. <div class="col-sm-1">
  193. <input type="number" class="form-control" id="building-type-19" min="0" max="400" />
  194. </div>
  195. </div>
  196. <div class="form-group">
  197. <label for="building-type-11" class="col-sm-4 control-label">
  198. Bereitschaftspolizei
  199. </label>
  200. <div class="col-sm-1">
  201. <input type="number" class="form-control" id="building-type-11" min="0" max="400" />
  202. </div>
  203. </div>
  204. <div class="form-group">
  205. <label for="building-type-13" class="col-sm-4 control-label">
  206. Polizeihubschrauberstation
  207. </label>
  208. <div class="col-sm-1">
  209. <input type="number" class="form-control" id="building-type-13" min="0" max="400" />
  210. </div>
  211. </div>
  212. <div class="form-group">
  213. <label for="building-type-12" class="col-sm-4 control-label">
  214. Schnelleinsatzgruppe (SEG)
  215. </label>
  216. <div class="col-sm-1">
  217. <input type="number" class="form-control" id="building-type-12" min="0" max="400" />
  218. </div>
  219. </div>
  220. <div class="form-group">
  221. <label for="building-type-9" class="col-sm-4 control-label">
  222. THW
  223. </label>
  224. <div class="col-sm-1">
  225. <input type="number" class="form-control" id="building-type-9" min="0" max="400" />
  226. </div>
  227. </div>
  228. <div class="form-group">
  229. <div class="col-sm-offset-2 col-sm-2">
  230. <button id="save-settings" type="submit" class="btn btn-success">
  231. Speichern
  232. </button>
  233. </div>
  234. </div>
  235. </form>
  236. </div>
  237. </div>
  238. </div>
  239. </div>
  240. </div>
  241. </div>
  242. </div>
  243. `;
  244. document.body.appendChild(modal);
  245. }
  246.  
  247. function menuEntryClick(event) {
  248. event.preventDefault();
  249. render();
  250.  
  251. $("#personaler-modal").modal("show");
  252. }
  253.  
  254. function getSettings() {
  255. return JSON.parse(localStorage.getItem("personalerSettings"));
  256. }
  257.  
  258. function fillForm() {
  259. if (localStorage.getItem("personalerSettings") === null) {
  260. localStorage.setItem("personalerSettings", JSON.stringify(defaultSettings));
  261. }
  262.  
  263. let settings = getSettings();
  264.  
  265. for (let key in settings) {
  266. document.getElementById(key).value = settings[key];
  267. }
  268. }
  269.  
  270. function saveSettings(event) {
  271. event.preventDefault();
  272.  
  273. const saveButton = document.getElementById("save-settings");
  274.  
  275. saveButton.disabled = true;
  276.  
  277. saveButton.innerHTML = `
  278. <span class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
  279. Speichern...`;
  280.  
  281. const settings = {};
  282. const inputs = document.querySelectorAll("#settings input");
  283. for (let i = 0; i < inputs.length; i++) {
  284. settings[inputs[i].id] = inputs[i].value;
  285. }
  286. localStorage.setItem("personalerSettings", JSON.stringify(settings));
  287.  
  288. setTimeout(() => {
  289. saveButton.disabled = false;
  290. saveButton.innerHTML = '<span class="glyphicon glyphicon-ok"></span> Speichern';
  291. }, 250);
  292. }
  293.  
  294. function getPersonalCountTarget(building_type) {
  295. const settings = getSettings();
  296. return settings["building-type-" + building_type];
  297. }
  298.  
  299. async function getBuildings() {
  300. if (
  301. !sessionStorage.aBuildings ||
  302. JSON.parse(sessionStorage.aBuildings).lastUpdate < new Date().getTime() - 5 * 1000 * 60
  303. ) {
  304. const buildings = await fetch("/api/buildings.json").then(response => response.json());
  305.  
  306. try {
  307. sessionStorage.setItem("aBuildings", JSON.stringify({ lastUpdate: new Date().getTime(), value: buildings }));
  308. } catch (e) {
  309. return buildings;
  310. }
  311. }
  312. return JSON.parse(sessionStorage.aBuildings).value;
  313. }
  314.  
  315. async function getBuildingsWithIncorrectPersonalCountTarget() {
  316. const buildings = await getBuildings();
  317.  
  318. return buildings.filter(
  319. building =>
  320. buildingsWithPersonal.includes(building.building_type) &&
  321. building.personal_count_target !== getPersonalCountTarget(building.building_type),
  322. );
  323. }
  324.  
  325. async function getBuildingsWithoutAutomaticHiring() {
  326. const buildings = await getBuildings();
  327.  
  328. return buildings.filter(
  329. building => buildingsWithPersonal.includes(building.building_type) && building.hiring_automatic !== true,
  330. );
  331. }
  332.  
  333. async function render() {
  334. const saveButton = document.getElementById("save-settings");
  335. saveButton.addEventListener("click", saveSettings);
  336.  
  337. fillForm();
  338.  
  339. const buildingsWithIncorrectPersonalCountTarget = await getBuildingsWithIncorrectPersonalCountTarget();
  340. if (buildingsWithIncorrectPersonalCountTarget.length > 0) {
  341. const tabPersonalCountTarget = document.getElementById("buildings-with-incorrect-personal-count-target");
  342. tabPersonalCountTarget.innerHTML = `
  343. <table class="table table-striped table-hover">
  344. <thead>
  345. <tr>
  346. <th>Gebäude</th>
  347. <th>Ist: Personal (Soll)</th>
  348. <th>Soll: Personal (Soll)</th>
  349. </tr>
  350. </thead>
  351. <tbody>
  352. ${buildingsWithIncorrectPersonalCountTarget
  353. .sort((a, b) => {
  354. if (a.caption < b.caption) {
  355. return -1;
  356. }
  357. if (a.caption > b.caption) {
  358. return 1;
  359. }
  360. return 0;
  361. })
  362. .map(
  363. building => `
  364. <tr>
  365. <td><a href="/buildings/${building.id}">${building.caption}</a></td>
  366. <td>${building.personal_count_target}</td>
  367. <td>${getPersonalCountTarget(building.building_type)}</td>
  368. </tr>
  369. `,
  370. )
  371. .join("")}
  372. </tbody>
  373. </table>
  374. `;
  375. } else {
  376. const tabPersonalCountTarget = document.getElementById("buildings-with-incorrect-personal-count-target");
  377. tabPersonalCountTarget.innerHTML = `
  378. <div class="row">
  379. <div class="col-md-12 bg">
  380. <div class="alert alert-success" role="alert">
  381. <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> Alle Gebäude haben das richtige Soll-Personal
  382. </div>
  383. </div>
  384. </div>
  385. `;
  386. }
  387.  
  388. const tabHiring = document.getElementById("buildings-without-automatic-hiring");
  389. const buildingsWithoutAutomaticHiring = await getBuildingsWithoutAutomaticHiring();
  390. if (buildingsWithoutAutomaticHiring.length > 0) {
  391. tabHiring.innerHTML = `
  392. <table class="table table-striped table-hover">
  393. <thead>
  394. <tr>
  395. <th>Gebäude</th>
  396. </tr>
  397. </thead>
  398. <tbody>
  399. ${buildingsWithoutAutomaticHiring
  400. .sort((a, b) => {
  401. if (a.caption < b.caption) {
  402. return -1;
  403. }
  404. if (a.caption > b.caption) {
  405. return 1;
  406. }
  407. return 0;
  408. })
  409. .map(
  410. building => `
  411. <tr>
  412. <td><a href="/buildings/${building.id}">${building.caption}</a></td>
  413. </tr>
  414. `,
  415. )
  416. .join("")}
  417. </tbody>
  418. </table>
  419. `;
  420. } else {
  421. tabHiring.innerHTML = `
  422. <div class="row">
  423. <div class="col-md-12 bg">
  424. <div class="alert alert-success" role="alert">
  425. <span class="glyphicon glyphicon-ok" aria-hidden="true"></span> Alle Gebäude haben automatische Werbung aktiviert
  426. </div>
  427. </div>
  428. </div>
  429. `;
  430. }
  431. }
  432.  
  433. async function main() {
  434. addStyle();
  435. addMenuEntry();
  436. addModal();
  437. }
  438.  
  439. main();
  440. })();