Toffu

Autofills Woffu schedule

  1. // ==UserScript==
  2. // @name Toffu
  3. // @namespace https://greasyfork.org/en/scripts/419870-toffu
  4. // @version 0.9
  5. // @description Autofills Woffu schedule
  6. // @author DonNadie
  7. // @match https://*.woffu.com/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (async function() {
  13. 'use strict';
  14.  
  15. let userToken;
  16. let departmentId;
  17. let scheduleId;
  18. let calendarId;
  19. let userId;
  20.  
  21. const api = async (route, params, method) =>
  22. {
  23. const BASE_URL = "https://" + location.hostname + '/api/';
  24.  
  25. if (typeof params === "undefined" || params == null) {
  26. params = {};
  27. }
  28. if (typeof method === "undefined" || method == null) {
  29. method = "get";
  30. }
  31.  
  32. let request = {
  33. method: method,
  34. };
  35.  
  36. let paramList;
  37.  
  38. // json body or already a FormData
  39. if (params instanceof FormData || params instanceof URLSearchParams) {
  40. paramList = params;
  41. } else if (typeof params === "string") {
  42. request.headers = {
  43. 'Accept': 'application/json',
  44. 'Content-Type': 'application/json'
  45. };
  46. paramList = params;
  47. } else {
  48. if (method === "post") {
  49. paramList = new FormData();
  50. } else {
  51. paramList = new URLSearchParams();
  52. }
  53.  
  54. for (let k in params) {
  55. if (!params.hasOwnProperty(k)) {continue;}
  56. paramList.append(k, params[k]);
  57. }
  58. }
  59.  
  60. if (method === "post" || method === "put") {
  61. request.body = paramList;
  62. } else {
  63. route = route + "?" + paramList;
  64. }
  65.  
  66. if (typeof request.headers === "undefined") {
  67. request.headers = {};
  68. }
  69. if (userToken) {
  70. request.headers['Authorization'] = "Bearer " + userToken;
  71. }
  72.  
  73. return new Promise((resolve, reject) => {
  74. fetch(BASE_URL + route, request)
  75. .then(response => response.json().then(resolve))
  76. .catch(error => reject(error));
  77. });
  78. };
  79.  
  80. const getUserToken = async () => {
  81. const response = await api('users/token');
  82.  
  83. if (response) {
  84. return response.Token;
  85. }
  86. return null;
  87. };
  88.  
  89. const parseUserToken = (token) => {
  90. token = token.split(".")[1];
  91. return JSON.parse(atob(token));
  92. };
  93.  
  94. const getPresence = async (userId, start, end) => {
  95. return (await api('users/' + userId + '/diaries/presence', {
  96. fromDate: start,
  97. toDate: end,
  98. pageIndex: 0,
  99. pageSize: 31,
  100. }, "get")).Diaries;
  101. };
  102.  
  103. const getDateRange = () => {
  104. let response = {};
  105. const inputs = document.querySelectorAll('.react-datepicker__input-container input');
  106. const format = understandDateFormat();
  107. [
  108. {
  109. val: inputs[0].value,
  110. field: "start",
  111. },
  112. {
  113. val: inputs[1].value,
  114. field: "end",
  115. }
  116. ].forEach(date => {
  117. const parts = date.val.split(format.separator);
  118. let parsedDate = {};
  119. parts.forEach((part, i) => {
  120. parsedDate[format.map[i]] = part;
  121. });
  122.  
  123. response[date.field] = parsedDate.year + "-" + parsedDate.month + "-" + parsedDate.day;
  124. });
  125.  
  126. return response;
  127. };
  128.  
  129. const understandDateFormat = () => {
  130. const separator = new Date().toLocaleDateString().replaceAll(/\d/g, '').substr(0, 1);
  131. let map;
  132.  
  133. if (getLang() === 'en') {
  134. map = {
  135. 0: "month",
  136. 1: "day",
  137. 2: "year"
  138. };
  139. } else {
  140. map = {
  141. 0: "day",
  142. 1: "month",
  143. 2: "year"
  144. };
  145. }
  146.  
  147. return {
  148. separator,
  149. map
  150. };
  151. }
  152.  
  153. const getLang = () => {
  154. // can't use document.lang as those retards take ages to properly set it
  155. return getAllH2Contents().includes('Mi Presencia') ? 'es' : 'en';
  156. }
  157.  
  158. const getDayTemplate = (day) => {
  159. const date = day.Date.split('T')[0];
  160.  
  161. const li = document.createElement('li');
  162. li.dataset.removable = true;
  163. li.style.marginLeft = "5px";
  164. li.innerHTML = `
  165. <div class="form-check">
  166. <input class="form-check-input" type="checkbox" value="` + day.DiaryId + `" data-date="` + date + `" id="autodate-` + date + `" checked>
  167. <label class="form-check-label" for="autodate-` + date + `" style="display: inline-block;">
  168. ` + date + `<span class="text-danger"> ` + day.DiffFormatted.Values[0] + `h</span>
  169. </label>
  170. </div>
  171. `;
  172.  
  173. return li;
  174. }
  175.  
  176. const calculateSlotTime = (start, end) => {
  177. const startDate = new Date();
  178. startDate.setHours(parseInt(start));
  179. startDate.setMinutes(parseInt(start.split(':')[1]));
  180.  
  181. const endDate = new Date();
  182. endDate.setHours(parseInt(end));
  183. endDate.setMinutes(parseInt(end.split(':')[1]));
  184.  
  185. return parseInt(Math.abs(endDate - startDate) / 36e5);
  186. }
  187.  
  188. const createSlot = (start, end, order) => {
  189. return {
  190. "Motive":null,
  191. "In":{
  192. "Time": start,
  193. "new":true,
  194. "SignStatus":1,
  195. "SignType":3,
  196. "SignId":0,
  197. "AgreementEventId":null,
  198. "RequestId":null
  199. },
  200. "Out":{
  201. "Time": end,
  202. "new":true,
  203. "SignStatus":1,
  204. "SignType":3,
  205. "SignId":0,
  206. "AgreementEventId":null,
  207. "RequestId":null
  208. },
  209. "totalSlot": calculateSlotTime(start, end),
  210. "order": order
  211. };
  212. }
  213.  
  214. const getTimeWindows = () => {
  215. const totalSlots = 2;
  216. const slots = [];
  217.  
  218. for (let i = 1; i <= totalSlots; i++) {
  219. slots.push(createSlot(document.querySelector('[name="af-window-' + i + '-start"]').value, document.querySelector('[name="af-window-' + i + '-end"]').value, i));
  220. }
  221.  
  222. return slots;
  223. };
  224.  
  225. const submitAutofill = async (selectedDays, submitButton) => {
  226. if (selectedDays.length < 1) {
  227. return;
  228. }
  229.  
  230. submitButton.setAttribute('disabled', 'disabled');
  231.  
  232. const slots = getTimeWindows();
  233.  
  234. for (const selectedDay of selectedDays) {
  235. const dairyId = selectedDay.value;
  236. const date = selectedDay.dataset.date;
  237. const params = {
  238. "DiaryId": dairyId,
  239. "UserId": userId,
  240. "Date": date + "T00:00:00.000",
  241. "DepartmentId": departmentId,
  242. "JobTitleId":null,
  243. "CalendarId": calendarId,
  244. "ScheduleId": scheduleId,
  245. "AgreementId":null,
  246. "TrueStartTime":null,
  247. "TrueEndTime":null,
  248. "TrueBreaksHours":1,
  249. "Accepted":false,
  250. "Comments":null,
  251. "Slots": slots
  252. };
  253.  
  254. // one by one to prevent getting banned
  255. try {
  256. await api("diaries/" + dairyId + "/workday/slots/self", JSON.stringify(params), 'put');
  257. } catch (e) {console.log(e)}
  258. }
  259.  
  260. alert("done, reload to actually check if it worked");
  261. submitButton.removeAttribute('disabled');
  262.  
  263. };
  264.  
  265. const getContainerTemplate = () => {
  266. const div = document.createElement('div');
  267. div.classList.add('dropdown');
  268. div.style.display = 'inline-block';
  269.  
  270. div.innerHTML = `
  271. <style>
  272. .dropdown {
  273. left: 20%;
  274. top: 10px;
  275. position: absolute;
  276. z-index: 99999;
  277. }
  278. .text-danger {
  279. color: red;
  280. }
  281. .dropdown-menu {
  282. background-color: white;
  283. padding: 5px;
  284. border: 2px solid
  285. }
  286. .dropdown-menu:not(.show) {
  287. display: none;
  288. }
  289. </style>
  290. <ul class="dropdown-menu" style="overflow: auto; text-align: left">
  291. <li style="margin-left: 5px">
  292. <strong>Entrada - Salida</strong>
  293. <div>
  294. <input name="af-window-1-start" value="09:00" type="time" class="form-control" style="padding-right:0px">
  295. <input name="af-window-1-end" value="14:00" type="time" class="form-control" style="margin-left: 0px">
  296. </div>
  297. <div>
  298. <input name="af-window-2-start" value="16:00" type="time" class="form-control" style="padding-right:0px">
  299. <input name="af-window-2-end" value="19:00" type="time" class="form-control" style="margin-left: 0px">
  300. </div>
  301. <button class="btn btn-primary" type="button" style="width:100%">Autofill</button>
  302. <hr>
  303. </li>
  304. </ul>
  305. `;
  306. const submitButton = div.querySelector('button');
  307. submitButton.addEventListener('click', () => {
  308. submitAutofill(div.querySelectorAll('input:checked'), submitButton);
  309. });
  310. const ul = div.querySelector('ul');
  311.  
  312. const button = document.createElement('button');
  313. button.classList.add('btn', 'btn-secondary', 'dropdown-toggle')
  314. button.type = "button";
  315. button.innerHTML = 'AutoFill <i class="fa fa-chevron-down"></i>';
  316. button.addEventListener('click', () => {
  317. ul.classList.toggle('show');
  318. });
  319.  
  320. div.appendChild(button);
  321. div.appendChild(ul);
  322.  
  323. return div;
  324. }
  325.  
  326. const setGlobalData = (day) => {
  327. departmentId = day.DepartmentId;
  328. scheduleId = day.ScheduleId;
  329. calendarId = day.CalendarId;
  330. userId = day.UserId;
  331. };
  332.  
  333. const removeOldEntries = () => {
  334. document.querySelectorAll('li[data-removable="true"]').forEach(el => {
  335. el.remove();
  336. });
  337. }
  338.  
  339. const showUnfilledDays = async (ul) => {
  340. removeOldEntries();
  341. const user = parseUserToken(userToken);
  342. const dateRange = getDateRange();
  343. const days = await getPresence(user.UserId, dateRange.start, dateRange.end);
  344. const pendingDays = [];
  345.  
  346. setGlobalData(days[0]);
  347.  
  348. for (const day of days) {
  349. // we only want negative days
  350. if (parseInt(day.DiffFormatted.Values[0]) < 0 && !day.TrueStartTime && day.IsUserEditable) {
  351. pendingDays.push(day);
  352. }
  353. }
  354.  
  355. for (const day of pendingDays) {
  356. const tpl = getDayTemplate(day);
  357.  
  358. ul.appendChild(tpl);
  359. }
  360. };
  361.  
  362. const getAllH2Contents = () => {
  363. let str = '';
  364. for (const el of document.querySelectorAll('h2')) {
  365. str += ' ' + el.innerText;
  366. }
  367. return str;
  368. }
  369.  
  370. const onLoaded = async () => {
  371. if (document.querySelectorAll('.react-datepicker__input-container input').length < 2) {
  372. setTimeout(onLoaded, 1000 * 1)
  373. return;
  374. }
  375.  
  376. userToken = await getUserToken();
  377.  
  378. // not logged in
  379. if (!userToken) {
  380. return;
  381. }
  382.  
  383. const container = getContainerTemplate();
  384.  
  385. document.body.prepend(container);
  386.  
  387. container.querySelector('button').addEventListener('click', () => {
  388. const ul = container.querySelector('ul');
  389. showUnfilledDays(ul);
  390. });
  391.  
  392. };
  393.  
  394. onLoaded();
  395. })();