Jira Task Priority Colorizer

Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Jira Task Priority Colorizer
// @namespace    http://tampermonkey.net/
// @version      0.2.3
// @description  Change card colors based on Jira task priority and add an emoji if a task has been in a status for more than 3 working days, excluding specific columns
// @author       erolatex
// @include      https://*/secure/RapidBoard.jspa*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Columns to exclude from emoji update
    const excludedColumns = ['ready for development', 'deployed'];

    // CSS styles to change card colors and position the emoji
    const styleContent = `
        .ghx-issue[data-priority*="P0"] {
            background-color: #FFADB0 !important;
        }
        .ghx-issue[data-priority*="P1"] {
            background-color: #FF8488 !important;
        }
        .ghx-issue[data-priority*="P2"] {
            background-color: #FFD3C6 !important;
        }
        .ghx-issue[data-priority*="P3"],
        .ghx-issue[data-priority*="P4"] {
            background-color: #FFF !important;
        }
        .stale-emoji {
            position: absolute;
            bottom: 5px;
            right: 5px;
            font-size: 14px;
            display: flex;
            align-items: center;
            background-color: #d2b48c;
            border-radius: 3px;
            padding: 2px 4px;
            font-weight: bold;
        }
        .stale-emoji span {
            margin-left: 5px;
            font-size: 12px;
            color: #000;
        }
        .ghx-issue {
            position: relative;
        }
        .column-badge.bad-count {
            margin-left: 5px;
            background-color: #d2b48c;
            border-radius: 3px;
            padding: 0 4px;
            font-size: 11px;
            color: #333;
            font-weight: normal;
            text-align: center;
            display: inline-block;
            vertical-align: middle;
            line-height: 20px;
        }
        .ghx-limits {
            display: flex;
            align-items: center;
            gap: 5px;
        }
    `;

    // Inject CSS styles into the page
    const styleElement = document.createElement('style');
    styleElement.type = 'text/css';
    styleElement.appendChild(document.createTextNode(styleContent));
    document.head.appendChild(styleElement);

    /**
     * Calculates the number of working days between two dates.
     * Excludes Saturdays and Sundays.
     * @param {Date} startDate - The start date.
     * @param {Date} endDate - The end date.
     * @returns {number} - The number of working days.
     */
    function calculateWorkingDays(startDate, endDate) {
        let count = 0;
        let currentDate = new Date(startDate);
        // Set time to midnight to avoid timezone issues
        currentDate.setHours(0, 0, 0, 0);
        endDate = new Date(endDate);
        endDate.setHours(0, 0, 0, 0);

        while (currentDate <= endDate) {
            const dayOfWeek = currentDate.getDay();
            if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
                count++;
            }
            currentDate.setDate(currentDate.getDate() + 1);
        }
        return count;
    }

    /**
     * Calculates the number of working days based on the total days in the column.
     * Assumes days are counted backward from the current date.
     * @param {number} daysInColumn - Total number of days in the column.
     * @returns {number} - The number of working days.
     */
    function calculateWorkingDaysFromDaysInColumn(daysInColumn) {
        let workingDays = 0;
        let currentDate = new Date();
        // Set time to midnight to avoid timezone issues
        currentDate.setHours(0, 0, 0, 0);

        for (let i = 0; i < daysInColumn; i++) {
            const dayOfWeek = currentDate.getDay();
            if (dayOfWeek !== 0 && dayOfWeek !== 6) { // Exclude Sunday (0) and Saturday (6)
                workingDays++;
            }
            // Move to the previous day
            currentDate.setDate(currentDate.getDate() - 1);
        }
        return workingDays;
    }

    /**
     * Updates the priorities of the cards and adds/removes the emoji based on working days.
     */
    function updateCardPriorities() {
        // Disconnect the observer to prevent it from reacting to our changes
        observer.disconnect();

        let cards = document.querySelectorAll('.ghx-issue');
        let poopCountPerColumn = {};

        cards.forEach(card => {
            // Update priority attribute
            let priorityElement = card.querySelector('.ghx-priority');
            if (priorityElement) {
                let priority = priorityElement.getAttribute('title') || priorityElement.getAttribute('aria-label') || priorityElement.innerText || priorityElement.textContent;
                if (priority) {
                    card.setAttribute('data-priority', priority);
                }
            }

            // Initialize working days count
            let workingDays = 0;

            // Attempt to get the start date from a data attribute (e.g., data-start-date)
            let startDateAttr = card.getAttribute('data-start-date'); // Example: '2024-04-25'
            if (startDateAttr) {
                let startDate = new Date(startDateAttr);
                let today = new Date();
                workingDays = calculateWorkingDays(startDate, today);
            } else {
                // If start date is not available, use daysInColumn
                let daysElement = card.querySelector('.ghx-days');
                if (daysElement) {
                    let title = daysElement.getAttribute('title');
                    if (title) {
                        let daysMatch = title.match(/(\d+)\s+days?/);
                        if (daysMatch && daysMatch[1]) {
                            let daysInColumn = parseInt(daysMatch[1], 10);
                            workingDays = calculateWorkingDaysFromDaysInColumn(daysInColumn);
                        }
                    }
                }
            }

            // Check and update the emoji 💩
            let columnElement = card.closest('.ghx-column');
            if (workingDays > 3 && columnElement) {
                let columnTitle = columnElement.textContent.trim().toLowerCase();
                if (!excludedColumns.some(col => columnTitle.includes(col))) {
                    let existingEmoji = card.querySelector('.stale-emoji');
                    if (!existingEmoji) {
                        let emojiContainer = document.createElement('div');
                        emojiContainer.className = 'stale-emoji';

                        let emojiElement = document.createElement('span');
                        emojiElement.textContent = '💩';

                        let daysText = document.createElement('span');
                        daysText.textContent = `${workingDays} d`;

                        emojiContainer.appendChild(emojiElement);
                        emojiContainer.appendChild(daysText);

                        card.appendChild(emojiContainer);
                    } else {
                        let daysText = existingEmoji.querySelector('span:last-child');
                        daysText.textContent = `${workingDays} d`;
                    }

                    // Count poop emoji per column
                    let columnId = columnElement.getAttribute('data-column-id') || columnElement.getAttribute('data-id');
                    if (columnId) {
                        if (!poopCountPerColumn[columnId]) {
                            poopCountPerColumn[columnId] = 0;
                        }
                        poopCountPerColumn[columnId]++;
                    }
                }
            } else {
                let existingEmoji = card.querySelector('.stale-emoji');
                if (existingEmoji) {
                    existingEmoji.remove();
                }
            }
        });

        // Update poop count badges for each column
        Object.keys(poopCountPerColumn).forEach(columnId => {
            let columnHeader = document.querySelector(`.ghx-column[data-id="${columnId}"]`);
            if (columnHeader) {
                let limitsContainer = columnHeader.querySelector('.ghx-column-header .ghx-limits');
                let existingBadge = columnHeader.querySelector('.column-badge.bad-count');
                if (!existingBadge) {
                    // Change from 'div' to 'span' and adjust classes
                    existingBadge = document.createElement('span');
                    existingBadge.className = 'ghx-constraint aui-lozenge aui-lozenge-subtle column-badge bad-count';
                }
                existingBadge.textContent = `💩 ${poopCountPerColumn[columnId]}`;
                existingBadge.style.display = 'inline-block';

                if (limitsContainer) {
                    let maxBadge = limitsContainer.querySelector('.ghx-constraint.ghx-busted-max');
                    if (maxBadge) {
                        limitsContainer.insertBefore(existingBadge, maxBadge);
                    } else {
                        limitsContainer.appendChild(existingBadge);
                    }
                } else {
                    // If limitsContainer doesn't exist, create it
                    limitsContainer = document.createElement('div');
                    limitsContainer.className = 'ghx-limits';
                    limitsContainer.appendChild(existingBadge);
                    let headerContent = columnHeader.querySelector('.ghx-column-header-content');
                    if (headerContent) {
                        headerContent.appendChild(limitsContainer);
                    }
                }
            }
        });

        // Reconnect the observer after making changes
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // MutationObserver to watch for changes in the DOM and update priorities accordingly
    const observer = new MutationObserver(() => {
        updateCardPriorities();
    });

    // Start observing the body
    observer.observe(document.body, { childList: true, subtree: true });

    // Update priorities when the page loads
    window.addEventListener('load', function() {
        updateCardPriorities();
    });

    // Periodically update priorities every 5 seconds
    setInterval(updateCardPriorities, 5000);
})();