// ==UserScript== // @name MZ - Player Weekly Matches Tracker // @namespace douglaskampl // @version 5.0 // @description Tracks the amount of friendlies and league matches (Senior/U23/U21/U18) played by each player during the current week // @author Douglas // @match https://www.managerzone.com/?p=challenges* // @icon https://www.google.com/s2/favicons?sz=64&domain=managerzone.com // @grant GM_addStyle // @grant GM_getResourceText // @resource weeklyMatchesTrackerStyles https://br18.org/mz/userscript/other/weeklyMatches.css // @run-at document-idle // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; GM_addStyle(GM_getResourceText('weeklyMatchesTrackerStyles')); class MZConfig { static get ENDPOINTS() { return { CHALLENGE_TEMPLATE: 'https://www.managerzone.com/ajax.php', MATCHES_LIST: 'https://www.managerzone.com/ajax.php', MANAGER_DATA: 'https://www.managerzone.com/xml/manager_data.php', MATCH_INFO: 'https://www.managerzone.com/xml/match_info.php' }; } static get MATCH_DAYS_FRIENDLY() { return ['d1', 'd3', 'd4', 'd5', 'd7']; } static get SPORT_IDS() { return { SOCCER: '1', HOCKEY: '2' }; } static get LEAGUE_TYPES() { return ['series', 'u18_series', 'u21_series', 'u23_series']; } } class MatchTracker { constructor() { this.initializeState(); this.initializeUI(); this.fetchPageData(); } initializeState() { const sportElement = document.querySelector('#shortcut_link_thezone'); const sportParam = new URL(sportElement.href).searchParams.get('sport'); this.sport = sportParam; this.sportId = sportParam === 'soccer' ? MZConfig.SPORT_IDS.SOCCER : MZConfig.SPORT_IDS.HOCKEY; this.teamId = null; this.appearances = new Map(); this.matchesFetched = { friendly: false, league: false }; this.pendingLeagueFetches = MZConfig.LEAGUE_TYPES.length; } initializeUI() { this.createModal(); this.createTable(); this.createLoadingElements(); this.addMainButton(); } createModal() { const modal = document.createElement('div'); modal.className = 'friendly-modal'; const content = document.createElement('div'); content.className = 'friendly-modal-content'; const close = document.createElement('span'); close.className = 'friendly-close'; close.innerHTML = '×'; close.onclick = () => this.toggleModal(false); content.appendChild(close); modal.appendChild(content); document.body.appendChild(modal); this.modal = modal; this.modalContent = content; modal.onclick = (e) => { if (e.target === modal) { this.toggleModal(false); } }; } createTable() { this.table = document.createElement('table'); this.table.className = 'friendly-table'; this.modalContent.appendChild(this.table); } createLoadingElements() { const loadingDiv = document.createElement('div'); loadingDiv.className = 'friendly-loading'; const message = document.createElement('p'); message.className = 'friendly-message'; message.textContent = 'Loading…'; loadingDiv.appendChild(message); this.modalContent.appendChild(loadingDiv); this.loadingDiv = loadingDiv; this.loadingMessage = message; } addMainButton() { const checkExist = setInterval(() => { const target = document.getElementById('fss-title-heading'); if (target) { clearInterval(checkExist); const container = document.createElement('div'); container.className = 'friendly-trigger-container'; const text = document.createElement('span'); text.className = 'friendly-text'; text.textContent = 'Click to see total matches played this week ->'; const button = document.createElement('button'); button.className = 'friendly-button'; button.setAttribute('aria-label', 'Show weekly player matches'); button.innerHTML = ''; button.onclick = () => this.toggleModal(true); container.appendChild(text); container.appendChild(button); target.parentNode.insertBefore(container, target); } }, 100); } toggleModal(show) { requestAnimationFrame(() => { if (show) { this.modal.style.display = 'flex'; requestAnimationFrame(() => { this.modal.classList.add('visible'); if (!this.matchesFetched.friendly || !this.matchesFetched.league) { this.loadingDiv.style.display = 'flex'; this.loadingMessage.classList.add('loading'); this.loadingMessage.classList.remove('error'); } }); } else { this.modal.classList.remove('visible'); setTimeout(() => { this.modal.style.display = 'none'; if (!this.matchesFetched.friendly || !this.matchesFetched.league) { this.loadingDiv.style.display = 'none'; } }, 250); } }); } async fetchPageData() { try { const response = await fetch(window.location.href); const data = await response.text(); const doc = new DOMParser().parseFromString(data, 'text/html'); const username = doc.getElementById('header-username').textContent; await this.fetchManagerData(username); } catch (error) { console.warn('Error fetching page data:', error); this.showErrorMessage('Could not fetch page data.'); } } async fetchManagerData(username) { try { const url = new URL(MZConfig.ENDPOINTS.MANAGER_DATA); url.searchParams.set('sport_id', this.sportId); url.searchParams.set('username', username); const response = await fetch(url); if (!response.ok) throw new Error(`Manager data fetch failed: ${response.status}`); const xmlDoc = new DOMParser().parseFromString(await response.text(), 'text/xml'); const teamElement = Array.from(xmlDoc.getElementsByTagName('Team')) .find(team => team.getAttribute('sport') === this.sport); if (!teamElement) throw new Error('Could not find team element in manager data.'); this.teamId = teamElement.getAttribute('teamId'); if (!this.teamId) throw new Error('Could not extract team ID from manager data.'); await this.fetchAllMatches(); } catch (error) { console.warn('Error fetching manager data:', error); this.showErrorMessage(`Could not fetch manager data: ${error.message}`); } } checkCompletion() { if (this.matchesFetched.friendly && this.matchesFetched.league) { if (this.appearances.size === 0) { this.showNoMatchesMessage(); } else { this.displayPlayerMatches(); } } } handleLeagueFetchCompletion() { this.pendingLeagueFetches--; if (this.pendingLeagueFetches <= 0) { this.matchesFetched.league = true; this.checkCompletion(); } } async fetchAllMatches() { const friendlyPromise = this.fetchFriendlyMatches().catch(error => { console.warn('Error in fetchFriendlyMatches:', error); }).finally(() => { this.matchesFetched.friendly = true; this.checkCompletion(); }); const leaguePromises = MZConfig.LEAGUE_TYPES.map(leagueType => this.fetchLeagueMatches(leagueType).catch(error => { console.warn(`Error in fetchLeagueMatches for ${leagueType}:`, error); }).finally(() => { this.handleLeagueFetchCompletion(); }) ); await Promise.all([friendlyPromise, ...leaguePromises]).catch(error => { console.warn('Error fetching one or more match types:', error); this.showErrorMessage('Error fetching some match data.'); }); } async fetchFriendlyMatches() { try { const url = new URL(MZConfig.ENDPOINTS.CHALLENGE_TEMPLATE); url.searchParams.set('p', 'challenge'); url.searchParams.set('sub', 'personal-challenge-template'); url.searchParams.set('sport', this.sport); const response = await fetch(url); if (!response.ok) throw new Error(`Challenge template fetch failed: ${response.status}`); const doc = new DOMParser().parseFromString(await response.text(), 'text/html'); const matchesDiv = this.getCurrentWeekFriendlyMatchesDiv(doc); let friendlyMatchIds = []; if (matchesDiv) { friendlyMatchIds = this.extractFriendlyMatchIds(matchesDiv); } if (friendlyMatchIds.length > 0) { await this.fetchAndProcessMatches(friendlyMatchIds, 'friendly'); } } catch (error) { console.warn('Error fetching friendly matches:', error); } } getCurrentWeekFriendlyMatchesDiv(doc) { const scheduleDiv = doc.getElementById('friendly_series_schedule'); if (!scheduleDiv) return null; const calendarDiv = scheduleDiv.querySelector('.calendar'); if (!calendarDiv) return null; const calendarForm = calendarDiv.querySelector('#saveMatchTactics'); return calendarForm?.querySelector('div.flex-nowrap.fss-row.fss-gw-wrapper.fss-has-matches'); } extractFriendlyMatchIds(matchesDiv) { return MZConfig.MATCH_DAYS_FRIENDLY .map(className => matchesDiv.querySelector(`.${className}`)) .filter(Boolean) .flatMap(div => Array.from(div.querySelectorAll('a.score-shown:not(.gray)')) .map(link => { const href = link.getAttribute('href'); if (!href) return null; const match = href.match(/mid=(\d+)/); return match ? match[1] : null; }) .filter(Boolean) ); } formatDateForMZ(date) { const d = String(date.getDate()).padStart(2, '0'); const m = String(date.getMonth() + 1).padStart(2, '0'); const y = date.getFullYear(); return `${d}-${m}-${y}`; } getRelevantLeagueDates() { const today = new Date(); const dayOfWeek = today.getDay(); const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; const mondayDate = new Date(today); mondayDate.setDate(today.getDate() + diffToMonday); mondayDate.setHours(0, 0, 0, 0); const relevantDates = new Set(); const wednesdayDate = new Date(mondayDate); wednesdayDate.setDate(mondayDate.getDate() + 2); const currentWednesday = this.formatDateForMZ(wednesdayDate); const sundayDate = new Date(mondayDate); sundayDate.setDate(mondayDate.getDate() + 6); const currentSunday = this.formatDateForMZ(sundayDate); if (today >= wednesdayDate) { relevantDates.add(currentWednesday); } if (today >= sundayDate || dayOfWeek === 0) { relevantDates.add(currentSunday); } return relevantDates; } async fetchLeagueMatches(selectType) { const relevantDates = this.getRelevantLeagueDates(); if (relevantDates.size === 0) { return; } try { const url = new URL(MZConfig.ENDPOINTS.MATCHES_LIST); url.searchParams.set('p', 'matches'); url.searchParams.set('sub', 'list'); url.searchParams.set('sport', this.sport); const body = new URLSearchParams({ type: 'played', hidescore: 'false', tid1: this.teamId, offset: '', selectType: selectType, limit: '1' // Fetch only 1 page, assuming league matches are recent enough }); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' }, body: body.toString() }); if (!response.ok) throw new Error(`League (${selectType}) fetch failed: ${response.status}`); const data = await response.json(); if (!data || !data.list) { console.warn(`Invalid or empty league match list response for ${selectType}`); return; } const doc = new DOMParser().parseFromString(data.list, 'text/html'); const leagueMatchIds = this.extractLeagueMatchIds(doc, relevantDates); if (leagueMatchIds.length > 0) { await this.fetchAndProcessMatches(leagueMatchIds, 'league'); } } catch (error) { console.warn(`Error fetching league matches for type ${selectType}:`, error); throw error; // Re-throw to be caught by Promise.all catch } } extractLeagueMatchIds(doc, relevantDates) { const matchIds = []; const groups = doc.querySelectorAll('#fixtures-results-list > dd.group'); groups.forEach(group => { const dateText = group.textContent.trim(); if (relevantDates.has(dateText)) { let nextElement = group.nextElementSibling; while (nextElement && !nextElement.classList.contains('group')) { if (nextElement.tagName === 'DD' && (nextElement.classList.contains('odd') || nextElement.classList.contains('even'))) { const link = nextElement.querySelector('a.score-shown:not(.gray)'); if (link) { const href = link.getAttribute('href'); if (href) { const match = href.match(/mid=(\d+)/); if (match && match[1]) { matchIds.push(match[1]); } } } } nextElement = nextElement.nextElementSibling; } } }); return matchIds; } async fetchAndProcessMatches(matchIds, type) { if (!matchIds || matchIds.length === 0) { return; } const processSingleMatch = async (matchId) => { try { const url = new URL(MZConfig.ENDPOINTS.MATCH_INFO); url.searchParams.set('sport_id', this.sportId); url.searchParams.set('match_id', matchId); const response = await fetch(url); if (!response.ok) { console.warn(`Failed to fetch match info for ID ${matchId}, status: ${response.status}`); return; } const xmlText = await response.text(); if (!xmlText) { console.warn(`Empty response for match info ID ${matchId}`); return; } const xmlDoc = new DOMParser().parseFromString(xmlText, 'text/xml'); if (xmlDoc.getElementsByTagName("parsererror").length > 0) { console.warn(`Error parsing XML for match info ID ${matchId}`); return; } const teamElements = Array.from(xmlDoc.getElementsByTagName('Team')); const ourTeamElement = teamElements.find(team => team.getAttribute('id') === this.teamId); if (ourTeamElement) { this.updatePlayerAppearances(ourTeamElement, type); } else { console.warn(`Could not find team ${this.teamId} in match ${matchId}`); } } catch (error) { console.warn(`Error processing match ID ${matchId}:`, error); } }; await Promise.all(matchIds.map(matchId => processSingleMatch(matchId))); } updatePlayerAppearances(ourTeamElement, type) { Array.from(ourTeamElement.getElementsByTagName('Player')).forEach(player => { const playerId = player.getAttribute('id'); if (!playerId) return; const playerName = player.getAttribute('name'); let playerInfo = this.appearances.get(playerId); if (!playerInfo) { playerInfo = { name: playerName, friendly: 0, league: 0 }; this.appearances.set(playerId, playerInfo); } if (type === 'friendly') { playerInfo.friendly += 1; } else if (type === 'league') { playerInfo.league += 1; } }); } displayPlayerMatches() { this.loadingDiv.style.display = 'none'; this.loadingMessage.classList.remove('loading', 'error'); this.table.innerHTML = ''; this.table.appendChild(this.createHeaderRow()); const sortedPlayers = Array.from(this.appearances.entries()) .filter(([, playerInfo]) => playerInfo.friendly > 0 || playerInfo.league > 0) .sort(([, a], [, b]) => { const totalA = a.friendly + a.league; const totalB = b.friendly + b.league; if (totalB !== totalA) { return totalB - totalA; } if (b.league !== a.league) { return b.league - a.league; } return b.friendly - a.friendly; }); if (sortedPlayers.length === 0) { this.showNoMatchesMessage(); return; } sortedPlayers.forEach(([playerId, playerInfo]) => { this.table.appendChild(this.createPlayerRow(playerId, playerInfo)); }); } createHeaderRow() { const row = document.createElement('tr'); ['Player', 'Matches'].forEach(text => { const th = document.createElement('th'); th.className = 'friendly-header'; th.textContent = text; row.appendChild(th); }); return row; } createPlayerRow(playerId, playerInfo) { const row = document.createElement('tr'); const nameCell = document.createElement('td'); nameCell.className = 'friendly-cell'; const link = document.createElement('a'); link.className = 'friendly-link'; link.href = `https://www.managerzone.com/?p=players&pid=${playerId}`; link.target = '_blank'; link.textContent = playerInfo.name; nameCell.appendChild(link); const appearancesCell = document.createElement('td'); appearancesCell.className = 'friendly-cell'; const totalMatches = playerInfo.friendly + playerInfo.league; const detailsParts = []; if (playerInfo.friendly > 0) { detailsParts.push(`${playerInfo.friendly} Friendl${playerInfo.friendly === 1 ? 'y' : 'ies'}`); } if (playerInfo.league > 0) { detailsParts.push(`${playerInfo.league} League Match${playerInfo.league === 1 ? '' : 'es'}`); } const detailsString = detailsParts.length > 0 ? `(${detailsParts.join(', ')})` : ''; appearancesCell.innerHTML = ''; const totalSpan = document.createElement('span'); totalSpan.className = 'total-matches'; totalSpan.textContent = totalMatches; appearancesCell.appendChild(totalSpan); if (detailsString) { const detailsSpan = document.createElement('span'); detailsSpan.className = 'match-details'; detailsSpan.textContent = ' ' + detailsString; appearancesCell.appendChild(detailsSpan); } else if (totalMatches === 0) { totalSpan.textContent = '0'; } row.appendChild(nameCell); row.appendChild(appearancesCell); return row; } showNoMatchesMessage() { this.loadingDiv.style.display = 'flex'; this.table.innerHTML = ''; this.loadingMessage.style.color = 'lightgray'; // Keep this specific style this.loadingMessage.classList.remove('loading', 'error'); this.loadingMessage.textContent = 'No friendly or relevant league matches found for players this week!'; } showErrorMessage(message) { this.loadingDiv.style.display = 'flex'; this.table.innerHTML = ''; this.loadingMessage.style.color = 'red'; // Keep this specific style this.loadingMessage.classList.add('error'); this.loadingMessage.classList.remove('loading'); this.loadingMessage.textContent = message; } } new MatchTracker(); })();