Devabit Jira+

Jira enhancements.

2025-07-23 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Devabit Jira+
// @namespace    http://tampermonkey.net/
// @version      2.7.5
// @description  Jira enhancements.
// @match        https://devabit.atlassian.net/browse/*
// @match        https://devabit.atlassian.net/jira/*
// @grant        none
// @license      3-clause BSD
// ==/UserScript==

(function() {
    "use strict";

    const deadlineMap = new Map();
    const estimatedHoursMap = new Map();

    const uaMonths = {
        січ: 0,
        лют: 1,
        бер: 2,
        квіт: 3,
        трав: 4,
        черв: 5,
        лип: 6,
        серп: 7,
        вер: 8,
        жовт: 9,
        лист: 10,
        груд: 11,
    };

    const jiraColors = {
        red: "#a10a0a",
        green: "#315e1d",
        yellow: "#ffd414",
        white: "#ffffff",
        black: "#000000",
    };

    function parseDateString(str) {
        // Parses date like "16 лип. 2025 р." or "16 лип. 2025 р., 17:00"
        // Ignores time after comma
        const regex = /(\d{1,2})\s([а-яіїєґ]{3})\.?\s(\d{4})/i;
        const m = regex.exec(str);
        if (!m) return null;
        return {
            day: +m[1],
            month: m[2].toLowerCase(),
            year: +m[3],
        };
    }

    function parseDateTimeString(str) {
        const regex =
            /(\d{1,2})\s([а-яіїєґ]{3,5})\.?\s(\d{4})\sр\.,?\s(\d{1,2}):(\d{2})/i;
        const m = regex.exec(str);
        if (!m) return null;
        const day = parseInt(m[1], 10);
        const month = uaMonths[m[2].toLowerCase()];
        const year = parseInt(m[3], 10);
        const hour = parseInt(m[4], 10);
        const minute = parseInt(m[5], 10);
        if (month === undefined) return null;
        return new Date(year, month, day, hour, minute);
    }

    function formatTimeLeft(ms) {
        const absMs = Math.abs(ms);
        const totalSeconds = Math.floor(absMs / 1000);
        const totalMinutes = Math.floor(totalSeconds / 60);
        const totalHours = Math.floor(totalMinutes / 60);
        const totalDays = Math.floor(totalHours / 24);
        const totalWeeks = Math.floor(totalDays / 7);
        const totalMonths = Math.floor(totalDays / 30);

        let parts = [];

        if (totalMonths >= 1) {
            parts.push(`${totalMonths}mo`);
            const remainingDays = totalDays % 30;
            if (remainingDays) parts.push(`${remainingDays}d`);
        } else if (totalWeeks >= 1) {
            parts.push(`${totalWeeks}w`);
            const remainingDays = totalDays % 7;
            if (remainingDays) parts.push(`${remainingDays}d`);
        } else {
            const hours = Math.floor((totalSeconds % 86400) / 3600);
            const minutes = Math.floor((totalSeconds % 3600) / 60);
            const seconds = totalSeconds % 60;

            if (totalDays) parts.push(`${totalDays}d`);
            if (hours) parts.push(`${hours}h`);
            if (minutes) parts.push(`${minutes}m`);
            if (seconds || parts.length === 0) {
                parts.push(`${seconds.toString().padStart(2, "0")}s`);
            }
        }

        const label = parts.join(" ");

        if (ms <= 0) {
            return `over deadline by ${label}`;
        } else {
            return `${label} left`;
        }
    }

    function datesMatch(date1, date2) {
        return (
            date1 &&
            date2 &&
            date1.day === date2.day &&
            date1.month === date2.month &&
            date1.year === date2.year
        );
    }

    function jiraTimeToHours(input) {
        if (!/\d/.test(input)) return null; // ignore if no digits

        const timeUnits = {
            w: 40,
            d: 8,
            h: 1,
            m: 1 / 60,
        };
        const regex = /(\d+)\s*(w|d|h|m)/gi;
        let totalHours = 0,
            match;

        while ((match = regex.exec(input)) !== null) {
            totalHours += parseInt(match[1], 10) * timeUnits[match[2].toLowerCase()];
        }

        return +totalHours.toFixed(2);
    }

    function isDueDeadlineApply() {
        const el = document.querySelector(
            'button[id="issue.fields.status-view.status-button"] > span.css-178ag6o'
        );
        return !(
            el &&
            (el.innerText === "Delivered" ||
                el.innerText === "Within client" ||
                el.innerText === "Archive" ||
                el.innerText === "Scheduled")
        );
    }

    function feature_highlighIfDeadlineAndDueDateMismatch() {
        if (!isDueDeadlineApply()) return;

        const dueDateContainer = document.querySelector(
            'div[data-testid="coloured-due-date.ui.colored-due-date-container"]'
        );
        const dueDateSpan = document.querySelector(
            'div[data-testid="coloured-due-date.ui.colored-due-date-container"] > span'
        );

        if (!dueDateSpan) return;

        const officialDate = parseDateString(dueDateSpan.textContent.trim());
        if (!officialDate) return;

        deadlineMap.forEach((info, el) => {
            const originalDate = parseDateString(info.original);

            if (!datesMatch(originalDate, officialDate)) {
                dueDateContainer.style.backgroundColor = jiraColors.red; // red highlight
            } else {
                dueDateContainer.style.backgroundColor = ""; // clear highlight if matches
            }
        });
    }

    function feature_highlightIfMonthBilledIsIncorrect() {
        if (!isDueDeadlineApply()) return;

        const tags = document.querySelectorAll(
            'span[data-testid="issue.views.common.tag.tag-item"] > span'
        );
        const now = new Date();
        const currentMonth = now.toLocaleString("en-US", {
            month: "long",
        });
        const currentYear = now.getFullYear();

        tags.forEach((tag) => {
            // Check for Delivered in sibling span.css-178ag6o
            const parent = tag.closest(
                'span[data-testid="issue.views.common.tag.tag-item"]'
            );
            if (!parent) return;

            const deliveredSpan = parent.querySelector("span.css-178ag6o");
            if (deliveredSpan && deliveredSpan.textContent.includes("Delivered")) {
                // Skip highlighting & timer for this task
                parent.style.backgroundColor = "";
                parent.style.color = "";
                parent.style.border = "";
                return;
            }

            const text = tag.textContent.trim();
            const regex = /^([A-Za-z]+)\s+(\d{4})$/;
            const match = text.match(regex);
            if (!match) return;

            const [_, tagMonth, tagYearStr] = match;
            const tagYear = parseInt(tagYearStr, 10);

            parent.style.border = "none"; // remove border

            if (
                tagMonth.toLowerCase() === currentMonth.toLowerCase() &&
                tagYear === currentYear
            ) {
                parent.style.backgroundColor = jiraColors.green; // green
                parent.style.color = "white";
            } else {
                parent.style.backgroundColor = jiraColors.red; // red
                parent.style.color = "white";
            }
        });
    }

    function feature_highlightIfPOisReceived() {
        const spans = document.querySelectorAll(
            'div[data-testid="issue.views.field.checkbox-inline-edit.customfield_10357--container"] > form > div._1pfhu2gc > div._vwz4kb7n > div > div > div > div > div > div >  span > span._11c82smr._1reo15vq._18m915vq._p12fmgvx._1bto1l2s._o5721q9c'
        );
        if (!spans.length) return;

        spans.forEach(span => {
            const value = span.textContent.trim().toLowerCase();
            const parent = span.parentElement?.closest('span');
            if (!parent) return;

            parent.style.border = 'none';
            parent.style.borderRadius = '4px';

            if (value === 'yes') {
                parent.style.backgroundColor = jiraColors.green;
                parent.style.color = '#ffffff';
            } else if (value === 'no') {
                parent.style.backgroundColor = jiraColors.red;
                parent.style.color = '#ffffff';
            } else {
                parent.style.backgroundColor = '';
                parent.style.color = '';
            }
        });
    }

    function feature_updateJiraTime() {
        const selectors = [
            ".css-v44io0",
            "span > span > span._k48p1wq8",
            'span[data-testid="issue.issue-view.common.logged-time.value"]',
            'span[data-testid="issue.component.logged-time.remaining-time"] > span',
        ];

        document.querySelectorAll(selectors.join(",")).forEach((el) => {
            let original = el.getAttribute("data-original");
            if (!original) {
                original = el.textContent.trim();
                el.setAttribute("data-original", original);
            }

            // Skip if no digits in string
            if (!/\d/.test(original)) return;

            // Skip non-time strings in .css-v44io0
            if (el.classList.contains("css-v44io0") && !/[wdhm]/i.test(original)) {
                return;
            }

            const hours = jiraTimeToHours(original);
            if (hours == null) return;

            el.textContent = `${hours}h`;

            // Save estimate if it’s the main estimate field
            if (el.classList.contains("css-v44io0")) {
                estimatedHoursMap.set("estimate", hours);
            }
        });
    }

    function feature_bailando() {
        const aEl = document.querySelector(
            'a[aria-label="Go to your Jira homepage"]'
        );
        if (aEl) {
            aEl.removeAttribute("style");
            aEl.style.textDecoration = "none";
        }

        const logoWrapper = document.querySelector(
            'span[data-testid="atlassian-navigation--product-home--logo--wrapper"]'
        );
        if (!logoWrapper) return;

        // Remove existing SVG
        const svg = logoWrapper.querySelector("svg");
        if (svg) svg.remove();

        // Use flex container
        logoWrapper.style.display = "flex";
        logoWrapper.style.alignItems = "center";
        logoWrapper.style.gap = "8px";

        // Add GIF only once
        if (!logoWrapper.querySelector("img.devabit-gif")) {
            const img = document.createElement("img");

            img.src = "https://media.tenor.com/vX-qFMkapQQAAAAj/cat-dancing.gif";

            img.className = "devabit-gif";
            img.style.height = "32px";
            img.style.width = "auto";
            img.style.verticalAlign = "middle";
            logoWrapper.appendChild(img);
        }

        // Add/update Жира span
        let textSpan = logoWrapper.querySelector("span.devabit-text");
        if (!textSpan) {
            textSpan = document.createElement("span");
            textSpan.className = "devabit-text";
            textSpan.textContent = "жира :)";
            textSpan.style.fontWeight = "700";
            textSpan.style.fontFamily = '"Atlassian Sans", Arial, sans-serif';
            textSpan.style.fontSize = "14px";
            textSpan.style.cursor = "pointer";
            textSpan.style.userSelect = "none";
            textSpan.style.paddingRight = "6px";
            textSpan.style.setProperty("text-decoration", "none", "important");
            logoWrapper.appendChild(textSpan);
        }

        textSpan.style.color = "#ffffff";
        textSpan.style.mixBlendMode = "difference";
    }

    function feature_setupLiveDeadlineCountdown() {
        const containers = document.querySelectorAll(
            'div[data-testid="issue-field-inline-edit-read-view-container.ui.container"]'
        );
        containers.forEach((el) => {
            if (!deadlineMap.has(el)) {
                let original = el.getAttribute("data-original");
                if (!original) {
                    original = el.textContent.trim();
                    el.setAttribute("data-original", original);
                }

                const deadline = parseDateTimeString(original);
                if (!deadline) return;

                deadlineMap.set(el, {
                    deadline,
                    original,
                });
            }
        });
    }

    function feature_updateLiveDeadlineCountdown() {
        const now = new Date();

        deadlineMap.forEach((info, el) => {
            const msLeft = info.deadline - now;
            const hoursLeft = msLeft / (1000 * 60 * 60);

            let label = formatTimeLeft(msLeft);

            if (isDueDeadlineApply()) {
                el.innerText = `${info.original}\n(${label})`;
                el.style.whiteSpace = "pre-line";
                el.style.flexDirection = "column";
                el.style.gap = "0rem";
                el.style.alignItems = "flex-start";
            } else {
                el.textContent = `${info.original}`;
                el.style.whiteSpace = "normal";
                el.style.backgroundColor = "";
                return;
            }

            if (msLeft <= 0) {
                el.style.backgroundColor = jiraColors.red;
                el.style.color = "#ffffff";
            } else if (hoursLeft < 0.5) {
                el.style.backgroundColor = jiraColors.yellow; // yellow
                el.style.color = "#ffffff";
            } else {
                el.style.backgroundColor = jiraColors.green; // green
                el.style.color = "#ffffff";
            }
        });
    }

    function getProjectCode() {
        const heading = document.querySelector(
            'h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'
        );
        if (!heading) return null;

        const title = heading.textContent.trim();

        const casePatterns = [
            /[A-Z]{2}-[A-Z]{3}\d{7}-\d{3}/g, // AI-DFL2507006-016
            /[A-Z]{2}-[A-Z]{3}\d{7}/g, // AI-DFL2507006
            /\d{4}\/\d{5}(?:\/#\d+)?/g, // 2025/62049/#2 or 2025/62049
            /\b\d{4}\/\d{4}\b/g // 3411/2025 (including "Quote 3411/2025")
        ];

        for (const pattern of casePatterns) {
            const matches = title.match(pattern);
            if (matches?.length) {
                return matches[matches.length - 1];
            }
        }

        return null;
    }

    function getDealineText() {
        const deadlineEl = [...deadlineMap.keys()][0];
        const deadlineText = deadlineEl?.getAttribute("data-original") || "";

        if (!deadlineText) return;
        return deadlineText;
    }

    function getEstimateText() {
        const estimateEl = document.querySelector(
            'div[data-testid="issue-field-inline-edit-read-view-container.ui.container"] > span > span'
        );
        const estimateText = estimateEl?.textContent.trim() || "";

        if (!estimateText) return;
        return estimateText;
    }

    function feature_insertInfoCopyButton() {
        const projectCode = getProjectCode();
        const deadlineText = getDealineText();
        const estimateText = getEstimateText();

        const previousParent = document.querySelector(
            'div[data-testid="issue.views.issue-base.context.status-and-approvals-wrapper.status-and-approval"] > div'
        );
        if (!previousParent) return;

        const container = previousParent.parentElement;
        if (!container) return;


        const existing = container.querySelector("#case-id-overlay");
        if (existing) existing.remove();

        const overlay = document.createElement("div");
        overlay.id = "case-id-overlay";
        overlay.style.display = "flex";
        overlay.style.flexDirection = "column";
        overlay.style.gap = "6px";
        overlay.style.marginBottom = "12px";

        function createRow(buttonText, valueText) {
            const row = document.createElement("div");
            row.style.display = "flex";
            row.style.alignItems = "center";
            row.style.gap = "8px";

            const btn = document.createElement("button");
            btn.textContent = buttonText;
            btn.className = "_mizu194a _1ah31bk5 _ra3xnqa1 _128m1bk5 _1cvmnqa1 _4davt94y _19itglyw _vchhusvi _r06hglyw _80omtlke _2rkosqtm _11c82smr _v5649dqc _189eidpf _1rjc12x7 _1e0c116y _1bsbviql _p12f1osq _kqswh2mm _4cvr1q9y _1bah1h6o _gy1p1b66 _1o9zidpf _4t3iviql _k48p1wq8 _y4tize3t _bozgze3t _y3gn1h6o _s7n4nkob _14mj1kw7 _9v7aze3t _1tv3nqa1 _39yqe4h9 _11fnglyw _18postnw _bfhk1w7a _syaz1gjq _8l3mmuej _aetrb3bt _10531gjq _f8pj1gjq _30l31gjq _9h8h1gjq _irr3166n _1di61dty _4bfu18uv _1hmsglyw _ajmmnqa1 _1a3b18uv _4fprglyw _5goinqa1 _9oik18uv _1bnxglyw _jf4cnqa1 _1nrm18uv _c2waglyw _1iohnqa1";
            btn.style.padding = "6px";
            btn.style.width = "fit-content";

            btn.addEventListener("click", () => {
                navigator.clipboard.writeText(valueText);
                btn.textContent = "Done";
                setTimeout(() => {
                    btn.textContent = buttonText;
                }, 1000);
            });

            const text = document.createElement("span");
            text.textContent = valueText;
            text.style.userSelect = "text";
            text.style.fontWeight = "normal";
            text.style.flex = "1";

            row.appendChild(btn);
            row.appendChild(text);

            return row;
        }

        if (projectCode) overlay.appendChild(createRow("Copy", projectCode));
        if (deadlineText) overlay.appendChild(createRow("Copy", deadlineText));
        if (estimateText) overlay.appendChild(createRow("Copy", estimateText));

        container.appendChild(overlay);
    }

    function debounce(func, delay) {
        let timer;
        return function(...args) {
            clearTimeout(timer);
            timer = setTimeout(() => func.apply(this, args), delay);
        };
    }

    function isBrowsePage() {
        return /\/browse\/[A-Z]+-\d+/i.test(location.pathname);
    }

    function removeOverlayIfNotBrowse() {
        if (!isBrowsePage()) {
            // overlays to hide
            // document.getElementById("case-id-overlay")?.remove();
            // document.getElementById('jira-top-overlay')?.remove();
        }
    }

    window.addEventListener("popstate", removeOverlayIfNotBrowse);
    window.addEventListener("hashchange", removeOverlayIfNotBrowse);

    function runAllEnhancements() {
        // if (!isBrowsePage()) {
        //   removeOverlayIfNotBrowse();
        // }
        feature_insertInfoCopyButton();
        feature_highlightIfPOisReceived();
        feature_updateJiraTime();
        feature_bailando();
        feature_setupLiveDeadlineCountdown();
        feature_updateLiveDeadlineCountdown();
        feature_highlightIfMonthBilledIsIncorrect();
        feature_highlighIfDeadlineAndDueDateMismatch();
    }

    const debouncedUpdate = debounce(runAllEnhancements, 300);

    const observer = new MutationObserver(debouncedUpdate);
    observer.observe(document.body, {
        childList: true,
        subtree: true,
    });

    window.addEventListener("load", runAllEnhancements);
    setInterval(feature_updateLiveDeadlineCountdown, 1000);
})();