MZ - Training Report Checker

Checks (periodically) if the training report is already out for the current day

  1. // ==UserScript==
  2. // @name MZ - Training Report Checker
  3. // @namespace douglaskampl
  4. // @version 4.9
  5. // @description Checks (periodically) if the training report is already out for the current day
  6. // @author Douglas
  7. // @match https://www.managerzone.com/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_addStyle
  12. // @grant GM_getResourceText
  13. // @resource trainingReportCheckerStyles https://br18.org/mz/userscript/other/trainingReport.css
  14. // @run-at document-idle
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. GM_addStyle(GM_getResourceText('trainingReportCheckerStyles'));
  22.  
  23. const CONFIG = {
  24. CHECK_INTERVAL: 200000, /* [in ms] */
  25. TIMEZONE_OFFSET: -3,
  26. FETCH_URL_REPORT: 'https://www.managerzone.com/ajax.php?p=trainingReport&sub=daily&sport=soccer&day=',
  27. READY_ICON_HTML: '<span class="report-icon ready"><i class="fa fa-unlock"></i></span>',
  28. NOT_READY_ICON_HTML: '<span class="report-icon not-ready"><i class="fa fa-lock"></i></span>',
  29. STORAGE_KEY: 'reportCheckedDate',
  30. };
  31.  
  32. const DAY_MAP = {
  33. 0: 1,
  34. 1: 2,
  35. 2: 3,
  36. 3: 4,
  37. 4: 5,
  38. 5: 6,
  39. 6: null
  40. };
  41.  
  42. class TrainingReportChecker {
  43. constructor() {
  44. this.linkId = 'shortcut_link_trainingreport';
  45. this.modalId = 'training_report_modal';
  46. this.balls = null;
  47. this.ballPlayers = [];
  48. this.hovering = false;
  49. this.isSoccer = false;
  50. }
  51.  
  52. init() {
  53. this.verifySportIsSoccer();
  54. if (!this.isSoccer) return;
  55. if (this.isSaturday()) return;
  56.  
  57. this.addTrainingReportLink();
  58. this.updateModalContent(false);
  59. this.checkTrainingReport();
  60. }
  61.  
  62. verifySportIsSoccer() {
  63. const sportLink = document.querySelector('#shortcut_link_thezone');
  64. if (!sportLink) {
  65. const pageSportMeta = document.querySelector('meta[name="mz-sport"]');
  66. if(pageSportMeta && pageSportMeta.content === 'soccer') {
  67. this.isSoccer = true;
  68. }
  69. return;
  70. }
  71. const sport = new URL(sportLink.href).searchParams.get('sport');
  72. if (sport === 'soccer') {
  73. this.isSoccer = true;
  74. }
  75. }
  76.  
  77. isSwedishDSTActive() {
  78. const now = new Date();
  79. const year = now.getUTCFullYear();
  80.  
  81. const getLastSundayOfMonthUTC = (month) => {
  82. const lastDay = new Date(Date.UTC(year, month + 1, 0));
  83. const dayOfWeek = lastDay.getUTCDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
  84. const date = lastDay.getUTCDate() - dayOfWeek;
  85. return new Date(Date.UTC(year, month, date, 1, 0, 0)); // DST change occurs at 1:00 UTC
  86. };
  87.  
  88. const dstStart = getLastSundayOfMonthUTC(2); // March (Month is 0-indexed)
  89. const dstEnd = getLastSundayOfMonthUTC(9); // October
  90.  
  91. const nowUTC = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds());
  92.  
  93. return nowUTC >= dstStart.getTime() && nowUTC < dstEnd.getTime();
  94. }
  95.  
  96. addTrainingReportLink() {
  97. const targetDiv = document.getElementById('pt-wrapper');
  98. if (!targetDiv) return;
  99.  
  100. const link = document.createElement('a');
  101. link.id = this.linkId;
  102. link.href = '/?p=training_report';
  103. link.title = '';
  104. link.innerHTML = CONFIG.NOT_READY_ICON_HTML;
  105. targetDiv.appendChild(link);
  106.  
  107. this.createModal();
  108. link.addEventListener('mouseenter', () => this.showModal());
  109. link.addEventListener('mouseleave', (e) => this.handleMouseLeave(e));
  110. const modal = document.getElementById(this.modalId);
  111. modal.addEventListener('mouseenter', () => {
  112. this.hovering = true;
  113. });
  114. modal.addEventListener('mouseleave', (e) => this.handleMouseLeave(e));
  115. }
  116.  
  117. updateModalContent(isReady) {
  118. const modal = document.getElementById(this.modalId);
  119. if (!modal) return;
  120.  
  121. const todayStr = this.getBrDate();
  122. let content;
  123. const currentDayIsSaturday = this.getBrTime().getDay() === 6 && !this.isExtendedFriday();
  124.  
  125. if (currentDayIsSaturday) {
  126. return;
  127. }
  128.  
  129. if (this.isExtendedFriday()) {
  130. if (isReady) {
  131. content = `Training report is out for ${todayStr} (Friday).`;
  132. if (this.balls !== null && this.balls > 0 && this.ballPlayers.length > 0) {
  133. content += '<br>Training balls earned today: <br>';
  134. content += '<ul style="list-style-type: none; padding: 0; margin: 5px 0;">';
  135. this.ballPlayers.forEach(player => {
  136. content += `<li style="margin: 2px 0;">${player.name} (${player.skill})</li>`;
  137. });
  138. content += '</ul>';
  139. }
  140. modal.className = 'ready';
  141. } else {
  142. content = `Training report is not out yet for ${todayStr} (Friday).`;
  143. modal.className = 'not-ready';
  144. }
  145. } else if (isReady) {
  146. content = `Training report is out for ${todayStr}!`;
  147. if (this.balls !== null && this.balls > 0 && this.ballPlayers.length > 0) {
  148. content += '<br>Training balls earned today: <br>';
  149. content += '<ul style="list-style-type: none; padding: 0; margin: 5px 0;">';
  150. this.ballPlayers.forEach(player => {
  151. content += `<li style="margin: 2px 0;">${player.name} (${player.skill})</li>`;
  152. });
  153. content += '</ul>';
  154. }
  155. modal.className = 'ready';
  156. } else {
  157. content = `Training report is not out yet for ${todayStr}.`;
  158. modal.className = 'not-ready';
  159. }
  160. modal.innerHTML = content;
  161. }
  162.  
  163. async checkTrainingReport() {
  164. if (!this.isWithinCheckWindow()) {
  165. if(!this.hasReportBeenCheckedToday()){
  166. setTimeout(() => this.checkTrainingReport(), CONFIG.CHECK_INTERVAL);
  167. }
  168. return;
  169. }
  170.  
  171. const now = this.getBrTime();
  172. let day = now.getDay();
  173. if (this.isExtendedFriday()) day = 5;
  174. const dayIndex = DAY_MAP[day];
  175.  
  176. if (!dayIndex) {
  177. this.updateModalContent(false);
  178. return;
  179. }
  180.  
  181. if (this.hasReportBeenCheckedToday()) {
  182. const link = document.getElementById(this.linkId);
  183. if (link) {
  184. link.innerHTML = CONFIG.READY_ICON_HTML;
  185. }
  186. await this.fetchEarnedBalls();
  187. this.updateModalContent(true);
  188. return;
  189. }
  190.  
  191. try {
  192. const response = await fetch(CONFIG.FETCH_URL_REPORT + dayIndex + '&sort_order=desc&sort_key=modification&player_sort=all');
  193. const text = await response.text();
  194. const parser = new DOMParser();
  195. const doc = parser.parseFromString(text, 'text/html');
  196. const table = doc.querySelector('body > table:nth-child(3)');
  197.  
  198. if (table && this.isReportReady(table)) {
  199. this.markReportAsChecked();
  200. this.updateModalContent(true);
  201. } else {
  202. const link = document.getElementById(this.linkId);
  203. if (link) {
  204. link.innerHTML = CONFIG.NOT_READY_ICON_HTML;
  205. }
  206. this.updateModalContent(false);
  207. setTimeout(() => this.checkTrainingReport(), CONFIG.CHECK_INTERVAL);
  208. }
  209. } catch (_e) {
  210. setTimeout(() => this.checkTrainingReport(), CONFIG.CHECK_INTERVAL);
  211. }
  212. }
  213.  
  214. getBrDate() {
  215. const date = new Date();
  216. const utc = date.getTime() + (date.getTimezoneOffset() * 60000);
  217. const brTime = new Date(utc + (3600000 * CONFIG.TIMEZONE_OFFSET));
  218. return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format(brTime);
  219. }
  220.  
  221. getBrTime() {
  222. const date = new Date();
  223. const utc = date.getTime() + (date.getTimezoneOffset() * 60000);
  224. return new Date(utc + (3600000 * CONFIG.TIMEZONE_OFFSET));
  225. }
  226.  
  227. isExtendedFriday() {
  228. const now = this.getBrTime();
  229. return now.getDay() === 6 && now.getHours() < 1;
  230. }
  231.  
  232. isSaturday() {
  233. const now = this.getBrTime();
  234. return now.getDay() === 6 && !this.isExtendedFriday();
  235. }
  236.  
  237. isWithinCheckWindow() {
  238. const now = this.getBrTime();
  239. const hour = now.getHours();
  240. const minute = now.getMinutes();
  241. const currentDay = now.getDay();
  242.  
  243. if (this.isSaturday()) return false;
  244.  
  245. if (this.isExtendedFriday()) return true;
  246.  
  247. let startHour, startMinute;
  248. const endHour = 23;
  249. const endMinute = 59;
  250.  
  251. if (this.isSwedishDSTActive()) {
  252. startHour = 19;
  253. startMinute = 0;
  254. } else {
  255. startHour = 20;
  256. startMinute = 0;
  257. }
  258.  
  259. const startTime = startHour * 60 + startMinute;
  260. const checkEndTime = endHour * 60 + endMinute;
  261. const currentTime = hour * 60 + minute;
  262.  
  263. return currentTime >= startTime && currentTime <= checkEndTime;
  264. }
  265.  
  266. createModal() {
  267. const modal = document.createElement('div');
  268. modal.id = this.modalId;
  269. document.body.appendChild(modal);
  270. }
  271.  
  272. positionModal() {
  273. const icon = document.getElementById(this.linkId);
  274. const modal = document.getElementById(this.modalId);
  275. if (!icon || !modal) return;
  276. const rect = icon.getBoundingClientRect();
  277. modal.style.top = (window.scrollY + rect.bottom + 5) + 'px';
  278. modal.style.left = (window.scrollX + rect.left + 50) + 'px';
  279. }
  280.  
  281. showModal() {
  282. const modal = document.getElementById(this.modalId);
  283. if (!modal) return;
  284. this.hovering = true;
  285. this.positionModal();
  286. modal.style.display = 'block';
  287. modal.classList.remove('fade-out');
  288. modal.classList.add('fade-in');
  289. }
  290.  
  291. handleMouseLeave(e) {
  292. const modal = document.getElementById(this.modalId);
  293. const icon = document.getElementById(this.linkId);
  294. if (!icon || !modal) return;
  295. const relatedTarget = e.relatedTarget;
  296. if (relatedTarget !== modal && relatedTarget !== icon) {
  297. this.hovering = false;
  298. this.hideModal();
  299. }
  300. }
  301.  
  302. hideModal() {
  303. if (!this.hovering) {
  304. const modal = document.getElementById(this.modalId);
  305. if (!modal) return;
  306. modal.classList.remove('fade-in');
  307. modal.classList.add('fade-out');
  308. setTimeout(() => {
  309. if (!this.hovering) {
  310. modal.style.display = 'none';
  311. }
  312. }, 200);
  313. }
  314. }
  315.  
  316. isReportReady(table) {
  317. return Array.from(table.querySelectorAll('tr')).some(row =>
  318. row.querySelector('img[src*="training_camp.png"]') ||
  319. row.querySelector('img[src*="gained_skill.png"]')
  320. );
  321. }
  322.  
  323. markReportAsChecked() {
  324. const today = this.getBrDate();
  325. GM_setValue(CONFIG.STORAGE_KEY, today);
  326. const link = document.getElementById(this.linkId);
  327. if (link) {
  328. link.innerHTML = CONFIG.READY_ICON_HTML;
  329. const iconSpan = link.querySelector('.report-icon');
  330. if (iconSpan) {
  331. iconSpan.addEventListener('animationend', () => {
  332. iconSpan.classList.remove('ready-transition');
  333. }, { once: true });
  334. iconSpan.classList.add('ready-transition');
  335. }
  336. }
  337. this.fetchEarnedBalls();
  338. }
  339.  
  340. async fetchEarnedBalls() {
  341. this.balls = 0;
  342. this.ballPlayers = [];
  343. try {
  344. const now = this.getBrTime();
  345. let day = now.getDay();
  346. if (this.isExtendedFriday()) day = 5;
  347. const dayIndex = DAY_MAP[day];
  348. if (!dayIndex) return;
  349.  
  350. const response = await fetch(CONFIG.FETCH_URL_REPORT + dayIndex + '&sort_order=desc&sort_key=modification&player_sort=all');
  351. const text = await response.text();
  352. const parser = new DOMParser();
  353. const doc = parser.parseFromString(text, 'text/html');
  354. const playerDetails = [];
  355. const rows = doc.querySelectorAll('tr');
  356.  
  357. rows.forEach(row => {
  358. const ballImg = row.querySelector('img[src*="gained_skill.png"]');
  359. if (ballImg) {
  360. const nameLink = row.querySelector('.player_link');
  361. const skillCell = row.querySelector('.skillColumn .clippable');
  362. if (nameLink && skillCell) {
  363. const fullName = nameLink.textContent.trim();
  364. const nameParts = fullName.split(' ');
  365. const shortName = nameParts.length > 1
  366. ? `${nameParts[0][0]}. ${nameParts[nameParts.length - 1]}`
  367. : fullName;
  368. const skill = skillCell.textContent.trim();
  369. playerDetails.push({ name: shortName, skill: skill });
  370. }
  371. }
  372. });
  373.  
  374. if (playerDetails.length > 0) {
  375. this.balls = playerDetails.length;
  376. this.ballPlayers = playerDetails;
  377. this.updateModalContent(true);
  378. } else {
  379. this.updateModalContent(this.hasReportBeenCheckedToday());
  380. }
  381. } catch (_e) {
  382. this.updateModalContent(this.hasReportBeenCheckedToday());
  383. }
  384. }
  385.  
  386. hasReportBeenCheckedToday() {
  387. const today = this.getBrDate();
  388. return GM_getValue(CONFIG.STORAGE_KEY) === today;
  389. }
  390. }
  391.  
  392. const checker = new TrainingReportChecker();
  393. if (document.readyState === 'complete' || document.readyState === 'interactive') {
  394. checker.init();
  395. } else {
  396. document.addEventListener('DOMContentLoaded', () => checker.init());
  397. }
  398. })();