jira-extensions

Jira enhancements.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         jira-extensions
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Jira enhancements.
// @match        https://devabit.atlassian.net/browse/*
// @match        https://devabit.atlassian.net/jira/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const deadlineMap = 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 feature_hideElements() {
    let selectors = [
      'span[data-testid="atlassian-navigation.ui.conversation-assistant.app-navigation-ai-mate"]',
      "div._1e0c1txw._vchhusvi._gy1pu2gc._1p57u2gc._4cvr1h6o._2lx2vrvc._kqswh2mm",
      'div[data-testid="issue.views.issue-base.foundation.status.actions-wrapper"]',
      'div[data-testid="issue.views.issue-base.foundation.status.improve-issue"]',
      "div._19itglyw._vchhusvi._r06hglyw._2rko1qi0._1dqonqa1._189ee4h9._1h6d1l7x._1e0c1txw._2lx21bp4._bfhkvuon",
      "div._19itglyw._vchhusvi._r06hglyw._2rko1qi0._189ee4h9._1dqonqa1._1h6d1l7x._1rjcutpp._18zrutpp._1ul95x59",
      'div[data-testid="issue-smart-request-summary.ui.ai-container"]',
      'div[data-testid="issue-view-common-views.placeholder-template-header"]',
      'div[data-testid="issue-view-common-views.placeholder-template-main-container"]',
    ];

    if (!Array.isArray(selectors)) return;

    selectors.forEach((selector) => {
      const elements = document.querySelectorAll(selector);
      elements.forEach((el) => {
        el.style.display = "none";
      });
    });
  }

  function parseDateTimeString(str) {
    if (!str) return null;

    // Example: "19 Feb 2026, 16:00"
    const regex = /^(\d{1,2}) ([A-Za-z]{3}) (\d{4}), (\d{1,2}):(\d{2})$/;
    const m = str.match(regex);
    if (!m) return null;

    const [, day, monStr, year, hh, mm] = m;

    const month = [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec",
    ].indexOf(monStr);

    if (month === -1) return null;

    return new Date(
      parseInt(year, 10),
      month,
      parseInt(day, 10),
      parseInt(hh, 10),
      parseInt(mm, 10),
      0,
    );
  }

  function formatTimeLeft(ms, showCountdown) {
    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(" ");

    // --- Format exact target date ---
    const target = new Date(Date.now() + ms);

    const uaMonths = [
      "January",
      "February",
      "March",
      "April",
      "May",
      "June",
      "July",
      "August",
      "September",
      "October",
      "November",
      "December",
    ];

    const dd = target.getDate();
    const mm = uaMonths[target.getMonth()];
    const yyyy = target.getFullYear();
    const HH = target.getHours().toString().padStart(2, "0");
    const MM = target.getMinutes().toString().padStart(2, "0");

    // 12-hour format with AM/PM
    let hours12 = target.getHours() % 12;
    if (hours12 === 0) hours12 = 12;
    const ampm = target.getHours() >= 12 ? "PM" : "AM";
    const HH12 = hours12.toString().padStart(2, "0");

    const formattedDate24 = `${HH}:${MM}`;
    const formattedDate12 = `${HH12}:${MM} ${ampm}`;

    // Combine both formats
    const formattedDateCombined = `${dd} ${mm} ${yyyy} р., ${formattedDate24} (${formattedDate12})`;

    if (ms <= 0) {
      return showCountdown
        ? `${formattedDateCombined}\n(over deadline by ${label})`
        : `${formattedDateCombined}`;
    } else {
      return showCountdown
        ? `${formattedDateCombined}\n(left ${label})`
        : `${formattedDateCombined}`;
    }
  }

  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_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",
      "._19pkidpf._2hwxidpf._otyridpf._18u0idpf._1i4qfg65._11c8wadc._y3gn1h6o",
      "._19pkidpf._2hwxidpf._otyridpf._18u0idpf._1i4qfg65._11c81o8v._y3gn1h6o",
      "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`;
    });
  }

  function feature_setupLiveDeadlineCountdown() {
    const containers = document.querySelectorAll(
      'div[data-testid="issue-field-date-time.ui.issue-field-date-time--container"] > div',
    );
    containers.forEach((el) => {
      if (!deadlineMap.has(el)) {
        const original = el.textContent.trim();

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

      if (isDueDeadlineApply()) {
        el.innerText = `${formatTimeLeft(msLeft, true)}`;
        el.style.whiteSpace = "pre";
        el.style.flexDirection = "column";
        el.style.gap = "0rem";
        el.style.alignItems = "flex-start";
      } else {
        el.textContent = `${formatTimeLeft(msLeft, false)}`;
        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 = "#000000";
      } else {
        el.style.backgroundColor = jiraColors.green; // green
        el.style.color = "#ffffff";
      }
    });
  }

  function runAllEnhancements() {
    feature_hideElements();
    feature_highlightIfPOisReceived();
    feature_setupLiveDeadlineCountdown();
    feature_updateLiveDeadlineCountdown();
    feature_highlightIfMonthBilledIsIncorrect();
    feature_updateJiraTime();
  }

  setInterval(runAllEnhancements, 150);
})();