jira-extensions

Jira enhancements.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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);
})();