Greasy Fork

MZ - Player Weekly Matches Tracker

Tracks the amount of friendlies and league matches (Senior/U23/U21/U18) played by each player during the current week

// ==UserScript==
// @name         MZ - Player Weekly Matches Tracker
// @namespace    douglaskampl
// @version      5.4
// @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
// ==/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;
            this.isLoadingComplete = false;
        }

        initializeUI() {
            this.createModal();
            this.createLoadingElements();
            this.createTable();
            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.table.style.display = 'none';
            this.modalContent.appendChild(this.table);
        }

        createLoadingElements() {
            const loadingDiv = document.createElement('div');
            loadingDiv.className = 'friendly-loading';

            const spinner = document.createElement('div');
            spinner.className = 'friendly-spinner';
            spinner.innerHTML = '<div></div><div></div><div></div><div></div>';

            const message = document.createElement('p');
            message.className = 'friendly-message';

            loadingDiv.appendChild(spinner);
            loadingDiv.appendChild(message);
            this.modalContent.appendChild(loadingDiv);

            this.loadingDiv = loadingDiv;
            this.spinner = spinner;
            this.messageElement = 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 check total matches played this week ->';

                    const button = document.createElement('button');
                    button.className = 'friendly-button';
                    button.setAttribute('aria-label', 'Show weekly player matches');
                    button.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM4.87 4.47a.5.5 0 0 0-.434-.27H3.21a.5.5 0 0 0-.457.672l.62 1.855a.5.5 0 0 0 .457.328h1.22a.5.5 0 0 0 .48-.64L4.87 4.47zM6.16 4.2h.81a.5.5 0 0 0 .48-.64L6.778 1.7a.5.5 0 0 0-.46-.327H5.134a.5.5 0 0 0-.457.672l.62 1.855a.5.5 0 0 0 .457.328h.406zm3.464 0h.81a.5.5 0 0 0 .457-.328l.62-1.855a.5.5 0 0 0-.457-.672H9.23a.5.5 0 0 0-.46.327L8.098 3.56a.5.5 0 0 0 .48.64h.416zM11.67 4.47l-.66 1.98a.5.5 0 0 0 .48.64h1.22a.5.5 0 0 0 .457-.328l.62-1.855a.5.5 0 0 0-.457-.672h-1.22a.5.5 0 0 0-.434.27zM4.183 7.342l-1.15 1.15a.5.5 0 0 0-.044.658l.866 1.298a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658L4.84 7.386a.5.5 0 0 0-.658-.044zm1.88 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658L6.72 7.386a.5.5 0 0 0-.658-.044zm2.63 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658L9.35 7.386a.5.5 0 0 0-.658-.044zm2.48 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658l-1.15-1.15a.5.5 0 0 0-.658-.044zm.886 3.03l.866-1.299a.5.5 0 0 0-.044-.658L11.194 7.386a.5.5 0 0 0-.658.044l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044zM4.5 10.05L3.35 8.9a.5.5 0 0 0-.658-.044L1.826 10.155a.5.5 0 0 0-.044.658l.866 1.299a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658zm6.836 0l.866 1.299a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658l-.866-1.299a.5.5 0 0 0-.658-.044L11.336 8.9a.5.5 0 0 0 .044.658zM7.21 10.692l-1.15 1.15a.5.5 0 0 0-.044.658l.866 1.298a.5.5 0 0 0 .658.044l1.15-1.15a.5.5 0 0 0 .044-.658l-.866-1.299a.5.5 0 0 0-.658-.044zm1.58 0l-.866 1.299a.5.5 0 0 0 .044.658l1.15 1.15a.5.5 0 0 0 .658.044l.866-1.299a.5.5 0 0 0-.044-.658l-1.15-1.15a.5.5 0 0 0-.658-.044z"/></svg>';
                    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.isLoadingComplete) {
                            this.loadingDiv.style.display = 'flex';
                            this.spinner.style.display = 'block';
                            this.messageElement.style.display = 'none';
                            this.table.style.display = 'none';
                        } else {
                            this.loadingDiv.style.display = 'none';
                            this.table.style.display = 'table';
                        }
                    });
                } else {
                    this.modal.classList.remove('visible');
                    setTimeout(() => {
                        this.modal.style.display = 'none';
                        if (!this.isLoadingComplete) {
                            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) {
                this.isLoadingComplete = true;
                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() {
            this.isLoadingComplete = false;
            this.appearances.clear();

            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) {
            const extractMatchIdFromLink = (link) => {
                const href = link.getAttribute('href');
                if (!href) return null;
                const match = href.match(/mid=(\d+)/);
                return match ? match[1] : null;
            };

            return MZConfig.MATCH_DAYS_FRIENDLY
                .map((className) => matchesDiv.querySelector(`.${className}`))
                .filter(Boolean)
                .flatMap((div) => Array.from(div.querySelectorAll('a.score-shown:not(.gray)'))
                .map(extractMatchIdFromLink)
                .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 now = new Date();

            const resetHourUTC = 22;
            const resetDayUTC = 1;

            const currentDayUTC = now.getUTCDay();
            const currentHourUTC = now.getUTCHours();

            const startOfCurrentMZWeek = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
            startOfCurrentMZWeek.setUTCHours(resetHourUTC, 0, 0, 0);

            let daysAgo = (currentDayUTC - resetDayUTC + 7) % 7;

            if (currentDayUTC < resetDayUTC || (currentDayUTC === resetDayUTC && currentHourUTC < resetHourUTC)) {
                 daysAgo += 7;
            }

            startOfCurrentMZWeek.setUTCDate(startOfCurrentMZWeek.getUTCDate() - daysAgo);

            const wednesdayDate = new Date(startOfCurrentMZWeek);
            wednesdayDate.setUTCDate(startOfCurrentMZWeek.getUTCDate() + 2);

            const sundayDate = new Date(startOfCurrentMZWeek);
            sundayDate.setUTCDate(startOfCurrentMZWeek.getUTCDate() + 6);

            const relevantDates = new Set();
            relevantDates.add(this.formatDateForMZ(wednesdayDate));
            relevantDates.add(this.formatDateForMZ(sundayDate));

            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: '10'
                });

                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;
            }
        }

        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') {

                    if (playerInfo.league < 2) {
                        playerInfo.league += 1;
                    }
                }
            });
        }

        displayPlayerMatches() {
            this.loadingDiv.style.display = 'none';
            this.spinner.style.display = 'none';
            this.messageElement.style.display = 'none';
            this.table.style.display = 'table';
            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.spinner.style.display = 'none';
            this.messageElement.style.display = 'block';
            this.messageElement.classList.remove('error');
            this.messageElement.textContent = 'No friendly or relevant league matches found for players this week!';
            this.table.style.display = 'none';
            this.table.innerHTML = '';
        }

        showErrorMessage(message) {
            this.isLoadingComplete = true;
            this.loadingDiv.style.display = 'flex';
            this.spinner.style.display = 'none';
            this.messageElement.style.display = 'block';
            this.messageElement.classList.add('error');
            this.messageElement.textContent = message;
            this.table.style.display = 'none';
            this.table.innerHTML = '';
        }
    }

    new MatchTracker();
})();