MZ - Ongoing Matches

Fetches results of ongoing matches and updates standings

// ==UserScript==
// @name         MZ - Ongoing Matches
// @namespace    douglaskampl
// @version      3.7
// @description  Fetches results of ongoing matches and updates standings
// @author       Douglas
// @match        https://www.managerzone.com/?p=league*
// @match        https://www.managerzone.com/?p=friendlyseries*
// @match        https://www.managerzone.com/?p=private_cup*
// @match        https://www.managerzone.com/?p=cup*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=managerzone.com
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @require      https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js
// @resource     NPROGRESS_CSS https://unpkg.com/[email protected]/nprogress.css
// @resource     ongoingMatchesStyles https://br18.org/mz/userscript/other/ongoingMatches.css
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    GM_addStyle(GM_getResourceText('NPROGRESS_CSS'));
    GM_addStyle(GM_getResourceText('ongoingMatchesStyles'));

    const UI = {
        BUTTON_STATES: {
            READY: 'Get match results',
            FETCHING: 'Processing…',
            DONE: 'Results updated'
        },
        PROGRESS_MESSAGES: {
            FETCHING_MATCHES: 'Fetching matches…',
            PROCESSING_RESULTS: 'Processing results…',
            UPDATING_STANDINGS: 'Updating standings…',
            ALL_COMPLETE: 'All updates complete'
        },
        MODAL: {
            NO_MATCHES_FOUND: 'Matches have not started yet. Please wait a few minutes.',
            STANDINGS_UPDATED: 'League standings have been updated with the results above.',
            NO_UPDATES_NEEDED: 'No ongoing matches. League standings were not updated.'
        },
        MATCH_STATUS: {
            WIN: 'green',
            DRAW: 'yellow',
            LOSS: 'red'
        }
    };

    const SELECTORS = {
        TRACK_BUTTONS: [
            '[id^="trackButton_series_"]',
            '[id^="trackButton_u18_series_"]',
            '[id^="trackButton_u21_series_"]',
            '[id^="trackButton_u23_series_"]',
            '[id^="trackButton_world_series_"]',
            '[id^="trackButton_u18_world_series_"]',
            '[id^="trackButton_u21_world_series_"]',
            '[id^="trackButton_u23_world_series_"]',
            '[id^="trackButton_friendlyseries_"]',
            '[id^="trackButton_cup_"]',
            '[id^="trackButton_privatecup_"]'
        ],
        MATCHES_TABLE: 'table.hitlist',
        STANDINGS_TABLE: 'table.nice_table',
        LAST_SIX_CELL: 'td.responsive-hide'
    };

    const ENDPOINTS = {
        MATCH_INFO: 'https://www.managerzone.com/xml/match_info.php?sport_id=1&match_id=',
        FRIENDLY_LEAGUE_SCHEDULE: 'https://www.managerzone.com/ajax.php?p=friendlySeries&sub=matches&sport=soccer&fsid='
    };

    class OngoingMatchesTracker {
        constructor() {
            this.matchResults = new Map();
            this.isFriendlyLeague = window.location.href.includes('friendlyseries');
            this.isPrivateCup = window.location.href.includes('private_cup');
            this.isCup = window.location.href.includes('p=cup');
            this.hasRun = false;
            this.observe();
        }

        async init() {
            const matches = await this.getMatches();
            if (!matches || !matches.length) return;
            this.setUpUI(matches);
        }

        async getMatches() {
            if (this.isFriendlyLeague) return await this.getFriendlyLeagueMatches();
            if (this.isPrivateCup || this.isCup) return this.getCupMatches();
            return this.getLeagueMatches();
        }

        getLeagueMatches() {
            const matchesTable = document.querySelector(SELECTORS.MATCHES_TABLE);
            if (!matchesTable) return [];
            return Array.from(matchesTable.querySelectorAll('tr'))
                .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !/^\d+\s*-\s*\d+$/.test(score) && !/^X\s*-\s*X$/.test(score);
                })
                .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
        }

        async getFriendlyLeagueMatches() {
            const fsidMatch = window.location.href.match(/fsid=(\d+)/);
            if (!fsidMatch) return [];
            try {
                const response = await fetch(ENDPOINTS.FRIENDLY_LEAGUE_SCHEDULE + fsidMatch[1]);
                const text = await response.text();
                const doc = new DOMParser().parseFromString(text, 'text/html');
                const now = new Date();
                const ongoingMatches = [];
                const rounds = doc.querySelectorAll('h2.subheader.clearfix');
                rounds.forEach(round => {
                    const headerText = round.textContent;
                    const dateTimeMatch = headerText.match(/(\d{2}\/\d{2}\/\d{4})\s+(\d{1,2}:\d{2}(?:am|pm))/i);
                    if (dateTimeMatch) {
                        const dateStr = dateTimeMatch[1];
                        const timeStr = dateTimeMatch[2];
                        const matchTime = this.parseDateTime(dateStr, timeStr);
                        const matchEndTime = new Date(matchTime.getTime() + 2 * 60 * 60 * 1000);
                        const matchesDiv = round.nextElementSibling;
                        if (matchesDiv && matchesDiv.classList.contains('mainContent')) {
                            const matchRows = matchesDiv.querySelectorAll('tr');
                            matchRows.forEach(m => {
                                const link = m.querySelector('a[href*="mid="]');
                                if (link) {
                                    const score = link.textContent.trim();
                                    if (!/^\d+-\d+$/.test(score)) {
                                        if (score === 'X - X') {
                                            if (now >= matchTime && now <= matchEndTime) {
                                                const homeTeam = m.querySelector('td:first-child').textContent.trim();
                                                const awayTeam = m.querySelector('td:last-child').textContent.trim();
                                                const params = new URLSearchParams(link.href);
                                                ongoingMatches.push({
                                                    mid: params.get('mid'),
                                                    homeTeam,
                                                    awayTeam
                                                });
                                            }
                                        } else {
                                            const homeTeam = m.querySelector('td:first-child').textContent.trim();
                                            const awayTeam = m.querySelector('td:last-child').textContent.trim();
                                            const params = new URLSearchParams(link.href);
                                            ongoingMatches.push({
                                                mid: params.get('mid'),
                                                homeTeam,
                                                awayTeam
                                            });
                                        }
                                    }
                                }
                            });
                        }
                    }
                });
                return ongoingMatches;
            } catch (error) {
                return [];
            }
        }

        getCupMatches() {
            const groupStages = document.querySelector('#group-stages');
            if (!groupStages) return [];
            return Array.from(groupStages.querySelectorAll('table.hitlist tr'))
                .filter(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    if (!link) return false;
                    const score = link.textContent.trim();
                    return !/^\d+\s*-\s*\d+$/.test(score) && !/^X\s*-\s*X$/.test(score);
                })
                .map(row => {
                    const link = row.querySelector('a[href*="mid="]');
                    const homeTeam = row.querySelector('td:first-child').textContent.trim();
                    const awayTeam = row.querySelector('td:last-child').textContent.trim();
                    const params = new URLSearchParams(link.href);
                    return {
                        mid: params.get('mid'),
                        homeTeam,
                        awayTeam
                    };
                });
        }

        setUpUI(matches) {
            const trackButton = this.findTrackButton();
            if (!trackButton) return;
            if (trackButton.parentNode.classList.contains('mz-fetch-added')) return;
            trackButton.parentNode.classList.add('mz-fetch-added');
            const fetchButton = this.createFetchButton();
            trackButton.parentNode.insertBefore(fetchButton, trackButton.nextSibling);
            fetchButton.addEventListener('click', () => {
                if (!this.hasRun) this.handleFetchClick(fetchButton, matches);
            });
        }

        findTrackButton() {
            return SELECTORS.TRACK_BUTTONS.reduce(
                (found, selector) => found || document.querySelector(selector),
                null
            );
        }

        createFetchButton() {
            const button = document.createElement('button');
            button.className = 'mz-fetch-button';
            button.textContent = UI.BUTTON_STATES.READY;
            return button;
        }

        showResultsModal(results) {
            const modal = document.createElement('div');
            modal.className = 'mz-modal';
            const roundNum = this.getCurrentRound();
            const headerTitle = roundNum
                ? `Match Results for Round ${roundNum}`
                : 'Match Results for Current Round';

            const header = document.createElement('div');
            header.className = 'mz-modal-header';
            header.innerHTML = `
            <h3>${headerTitle}</h3>
            <button class="mz-modal-close">Close</button>
          `;

            const content = document.createElement('div');
            content.className = 'mz-modal-content';

            if (results && results.length > 0) {
                results.forEach(r => {
                    const matchDiv = document.createElement('div');
                    matchDiv.className = 'mz-match-result';
                    const matchLink = document.createElement('a');
                    matchLink.href = `https://www.managerzone.com/?p=match&mid=${r.mid}`;
                    matchLink.className = 'mz-match-link';
                    matchLink.textContent = `${r.homeTeam} ${r.score} ${r.awayTeam}`;
                    matchDiv.appendChild(matchLink);
                    content.appendChild(matchDiv);
                });
            } else {
                const noMatchesDiv = document.createElement('div');
                noMatchesDiv.className = 'mz-no-matches';
                noMatchesDiv.textContent = UI.MODAL.NO_MATCHES_FOUND;
                content.appendChild(noMatchesDiv);
            }

            const footer = document.createElement('div');
            footer.className = 'mz-modal-footer';
            footer.innerHTML = `
            <div class="mz-update-info">
              ${results && results.length > 0
                    ? UI.MODAL.STANDINGS_UPDATED
                    : UI.MODAL.NO_UPDATES_NEEDED}
            </div>
          `;

            modal.appendChild(header);
            modal.appendChild(content);
            modal.appendChild(footer);
            document.body.appendChild(modal);
            setTimeout(() => modal.classList.add('show'), 10);

            modal.querySelector('.mz-modal-close').addEventListener('click', () => {
                modal.classList.remove('show');
                setTimeout(() => modal.remove(), 300);
            });
        }

        async clickStandingsTab() {
            if (window.location.href === 'https://www.managerzone.com/?p=league&type=senior') {
                const ul = document.querySelectorAll('.ui-tabs-nav')[0];
                if (ul) {
                    const tabs = ul.querySelectorAll('li[role="tab"]');
                    if (tabs.length >= 1) {
                        const firstTab = tabs[0];
                        if (
                            !firstTab.classList.contains('ui-tabs-active') &&
                            !firstTab.classList.contains('ui-state-active')
                        ) {
                            firstTab.querySelector('a').click();
                            await new Promise(r => setTimeout(r, 1000));
                        }
                    }
                }
            } else if (window.location.href.includes('p=league')) {
                const ul = document.querySelectorAll('.ui-tabs-nav')[0];
                if (ul) {
                    const tabs = ul.querySelectorAll('li[role="tab"]');
                    if (tabs.length >= 2) {
                        const secondTab = tabs[1];
                        if (
                            !secondTab.classList.contains('ui-tabs-active') &&
                            !secondTab.classList.contains('ui-state-active')
                        ) {
                            secondTab.querySelector('a').click();
                            await new Promise(r => setTimeout(r, 1000));
                        }
                    }
                }
            } else if (this.isFriendlyLeague) {
                const allUls = document.querySelectorAll('.ui-tabs-nav');
                if (allUls.length >= 2) {
                    const secondUl = allUls[1];
                    const tabs = secondUl.querySelectorAll('li[role="tab"]');
                    if (tabs.length >= 2) {
                        const secondTab = tabs[1];
                        if (
                            !secondTab.classList.contains('ui-tabs-active') &&
                            !secondTab.classList.contains('ui-state-active')
                        ) {
                            secondTab.querySelector('a').click();
                            await new Promise(r => setTimeout(r, 1000));
                        }
                    }
                }
            }
        }

        async handleFetchClick(button, matches) {
            if (this.hasRun) return;
            this.hasRun = true;

            await this.clickStandingsTab();

            NProgress.configure({ showSpinner: false });
            NProgress.start();
            this.showStatusMessage(UI.PROGRESS_MESSAGES.FETCHING_MATCHES);

            if (!matches.length) {
                if (this.isFriendlyLeague || this.isPrivateCup || this.isCup) {
                    this.showResultsModal([]);
                }
                NProgress.done();
                return;
            }

            button.classList.add('disabled');
            button.textContent = UI.BUTTON_STATES.FETCHING;

            this.showStatusMessage(UI.PROGRESS_MESSAGES.PROCESSING_RESULTS);
            const results = await this.processMatches(matches);

            if (this.isFriendlyLeague || this.isPrivateCup || this.isCup) {
                this.showResultsModal(results);
            }

            this.showStatusMessage(UI.PROGRESS_MESSAGES.UPDATING_STANDINGS);
            this.updateAllTeamStats();

            button.classList.remove('disabled');
            button.classList.add('done');
            button.textContent = UI.BUTTON_STATES.DONE;

            NProgress.done();
            this.showStatusMessage(UI.PROGRESS_MESSAGES.ALL_COMPLETE);
        }

        parseDateTime(dateStr, timeStr) {
            const [day, month, year] = dateStr.split('/');
            const date = `${month}/${day}/${year}`;
            let [time, period] = timeStr.toLowerCase().split(/(?=[ap]m)/);
            let [hours, minutes] = time.split(':');
            hours = parseInt(hours);
            if (period === 'pm' && hours !== 12) hours += 12;
            else if (period === 'am' && hours === 12) hours = 0;
            return new Date(`${date} ${hours}:${minutes}`);
        }

        getCurrentRound() {
            const table = document.querySelector(SELECTORS.STANDINGS_TABLE);
            if (!table) return null;
            const firstDataRow = table.querySelector('tbody tr');
            if (!firstDataRow) return null;
            const matchesCell = firstDataRow.querySelector('td:nth-child(3)');
            if (!matchesCell) return null;
            return parseInt(matchesCell.textContent.trim(), 10) + 1;
        }

        async processMatches(matches) {
            const results = [];
            const total = matches.length;
            for (let i = 0; i < total; i++) {
                const match = matches[i];
                try {
                    const response = await fetch(`${ENDPOINTS.MATCH_INFO}${match.mid}`);
                    const text = await response.text();
                    const matchData = this.parseMatchResponse({ responseText: text });
                    if (matchData) {
                        matchData.homeTeam = match.homeTeam;
                        matchData.awayTeam = match.awayTeam;
                        this.matchResults.set(match.mid, matchData);
                        this.updateMatchDisplay(match.mid, matchData);
                        results.push({
                            ...match,
                            score: `${matchData.homeGoals}-${matchData.awayGoals}`
                        });
                    }
                } catch (error) { }
                NProgress.set((i + 1) / total);
            }
            return results;
        }

        parseMatchResponse(response) {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(response.responseText, 'application/xml');
            const matchNode = xmlDoc.querySelector('Match');
            if (!matchNode) return null;
            const homeTeam = matchNode.querySelector('Team[field="home"]');
            const awayTeam = matchNode.querySelector('Team[field="away"]');
            if (!homeTeam || !awayTeam) return null;
            return {
                homeTid: homeTeam.getAttribute('id'),
                awayTid: awayTeam.getAttribute('id'),
                homeGoals: parseInt(homeTeam.getAttribute('goals'), 10) || 0,
                awayGoals: parseInt(awayTeam.getAttribute('goals'), 10) || 0
            };
        }

        updateMatchDisplay(mid, matchData) {
            const link = Array.from(document.links).find(l => l.href.includes(`mid=${mid}`));
            if (link) {
                link.textContent = `${matchData.homeGoals}-${matchData.awayGoals}`;
            }
        }

        calculateMatchResult(matchData) {
            if (matchData.homeGoals > matchData.awayGoals) {
                return {
                    home: { points: 3, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 0, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else if (matchData.homeGoals < matchData.awayGoals) {
                return {
                    home: { points: 0, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 3, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            } else {
                return {
                    home: { points: 1, goalsFor: matchData.homeGoals, goalsAgainst: matchData.awayGoals },
                    away: { points: 1, goalsFor: matchData.awayGoals, goalsAgainst: matchData.homeGoals }
                };
            }
        }

        sortTableByPoints() {
            const table = document.querySelector(SELECTORS.STANDINGS_TABLE);
            if (!table) return;
            const tbody = table.querySelector('tbody');
            if (!tbody) return;
            const dataRows = Array.from(tbody.querySelectorAll('tr')).filter(r => {
                const cells = r.querySelectorAll('td');
                return cells.length >= 10 && cells[2] && !isNaN(parseInt(cells[2].textContent));
            });
            dataRows.sort((a, b) => {
                const getVal = (rw, i) => parseInt(rw.querySelectorAll('td')[i].textContent.trim(), 10) || 0;
                const pointsA = getVal(a, 9);
                const pointsB = getVal(b, 9);
                if (pointsB !== pointsA) return pointsB - pointsA;
                const goalDiffA = getVal(a, 6) - getVal(a, 7);
                const goalDiffB = getVal(b, 6) - getVal(b, 7);
                if (goalDiffB !== goalDiffA) return goalDiffB - goalDiffA;
                return getVal(b, 6) - getVal(a, 6);
            });
            const nonDataRows = Array.from(tbody.children).filter(r => !dataRows.includes(r));
            const tempContainer = document.createDocumentFragment();
            dataRows.forEach((row, index) => {
                const originalRow = row.cloneNode(true);
                const positionCell = originalRow.querySelector('td:first-child span');
                if (positionCell) {
                    const prevHelpButton = row.querySelector('td:first-child span .help_button');
                    positionCell.innerHTML = `${index + 1}`;
                    if (prevHelpButton) positionCell.appendChild(prevHelpButton);
                }
                originalRow.className = index % 2 === 0 ? '' : 'highlight_row';
                if (index === 0) originalRow.classList.add('mz-table-row-champion');
                else if (index === 1) originalRow.classList.add('mz-table-row-promotion');
                else if (index === 7) originalRow.classList.add('mz-table-row-relegation');
                tempContainer.appendChild(originalRow);
            });
            nonDataRows.forEach(r => {
                tempContainer.appendChild(r.cloneNode(true));
            });
            tbody.innerHTML = '';
            tbody.appendChild(tempContainer);
            table.classList.add('standings-updated');
            setTimeout(() => table.classList.remove('standings-updated'), 2000);
        }

        findTeamRows(tid) {
            const teamLinks = Array.from(document.querySelectorAll(`a[href*="tid=${tid}"]`));
            const rowsSet = new Set();
            teamLinks.forEach(link => {
                const row = link.closest('tr');
                if (row) rowsSet.add(row);
            });
            const highlightedRows = Array.from(document.querySelectorAll('.highlight_row'))
                .filter(r => r.querySelector(`a[href*="tid=${tid}"]`));
            highlightedRows.forEach(r => rowsSet.add(r));
            return Array.from(rowsSet);
        }

        updateAllTeamStats() {
            this.matchResults.forEach(m => {
                const result = this.calculateMatchResult(m);
                this.updateTeamRow(m.homeTid, result.home);
                this.updateTeamRow(m.awayTid, result.away);
            });
            this.sortTableByPoints();
        }

        updateTeamRow(tid, result) {
            const teamRows = this.findTeamRows(tid);
            if (!teamRows.length) return;
            teamRows.forEach(row => {
                const cells = row.querySelectorAll('td');
                if (cells.length < 10) return;
                const parseCell = c => parseInt(c.textContent, 10) || 0;
                const updateCell = (c, val) => {
                    c.classList.add('updated-cell');
                    c.textContent = val;
                    setTimeout(() => c.classList.remove('updated-cell'), 1500);
                };
                updateCell(cells[2], parseCell(cells[2]) + 1);
                if (result.points === 3) updateCell(cells[3], parseCell(cells[3]) + 1);
                else if (result.points === 1) updateCell(cells[4], parseCell(cells[4]) + 1);
                else updateCell(cells[5], parseCell(cells[5]) + 1);
                updateCell(cells[6], parseCell(cells[6]) + result.goalsFor);
                updateCell(cells[7], parseCell(cells[7]) + result.goalsAgainst);
                const goalDiff = parseCell(cells[6]) - parseCell(cells[7]);
                const goalDiffElem = cells[8].querySelector('nobr');
                if (goalDiffElem) updateCell(goalDiffElem, goalDiff);
                updateCell(cells[9], parseCell(cells[9]) + result.points);
                this.updateLastSixDisplay(row, result, tid);
            });
        }

        updateLastSixDisplay(row, result, tid) {
            const lastSixCell = row.querySelector(SELECTORS.LAST_SIX_CELL);
            if (!lastSixCell) return;
            const nobrElement = lastSixCell.querySelector('nobr');
            if (!nobrElement) return;
            const links = nobrElement.querySelectorAll('a');
            const allTeamMatches = Array.from(this.matchResults.entries()).filter(([_, data]) => data.homeTid === tid || data.awayTid === tid);
            if (!allTeamMatches.length) return;
            const [matchId, data] = allTeamMatches[allTeamMatches.length - 1];
            const matchStatus = this.getMatchStatus(result);
            if (links.length >= 6) links[0].remove();
            const newStatusLink = this.createStatusLink(matchStatus, matchId, data, tid);
            nobrElement.appendChild(newStatusLink);
        }

        getMatchStatus(result) {
            if (result.points === 3) return UI.MATCH_STATUS.WIN;
            if (result.points === 1) return UI.MATCH_STATUS.DRAW;
            return UI.MATCH_STATUS.LOSS;
        }

        createStatusLink(status, matchId, matchData, tid) {
            const link = document.createElement('a');
            link.href = `/?p=match&sub=result&mid=${matchId}`;
            const isHome = matchData.homeTid === tid;
            const teamScore = isHome ? matchData.homeGoals : matchData.awayGoals;
            const oppScore = isHome ? matchData.awayGoals : matchData.homeGoals;
            link.title = `${matchData.homeTeam} ${matchData.homeGoals} - ${matchData.awayGoals} ${matchData.awayTeam}`;
            const img = document.createElement('img');
            img.style.border = '0';
            img.src = `img/status_${status}.gif`;
            img.alt = '';
            img.width = '13';
            img.height = '12';
            link.appendChild(img);
            return link;
        }

        showStatusMessage(message) {
            const existingMessage = document.querySelector('.status-message');
            existingMessage?.remove();
            const messageElement = document.createElement('div');
            messageElement.className = 'status-message';
            messageElement.textContent = message;
            document.body.appendChild(messageElement);
            setTimeout(() => {
                messageElement.style.opacity = '0';
                setTimeout(() => messageElement.remove(), 300);
            }, 2000);
        }

        observe() {
            const observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    if (mutation.addedNodes.length) {
                        const trackButton = this.findTrackButton();
                        if (trackButton && !document.querySelector('.mz-fetch-button')) {
                            this.init();
                            break;
                        }
                    }
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }
    }

    const tracker = new OngoingMatchesTracker();
    tracker.init();
})();