Devabit Jira+

Jira enhancements.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Devabit Jira+
// @namespace    http://tampermonkey.net/
// @version      3.2.1
// @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_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 > ._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_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-date-time.ui.issue-field-date-time--container"] > div'
    );
    containers.forEach((el) => {
      if (!deadlineMap.has(el)) {
        //let original = el.getAttribute("data-original");
        //if (!original) {
        const 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 getDeadlineText() {
    const deadlineContainer = document.querySelector(
      'div[data-testid="issue-field-date-time.ui.issue-field-date-time--container"] > div'
    );
    if (!deadlineContainer) return;
    const deadlineText = deadlineContainer.textContent.trim();
    if (!deadlineText) return;
    return deadlineText;
  }

  function getServerPath() {
    const description =
      document.querySelector(
        '[data-testid="issue.views.field.rich-text.description"]'
      )?.innerText || "";
    const match = description.match(/M:\\[^\s\n\r]*/i);
    if (!match) return null;

    // Remove any trailing word-like junk (like "Instructions" stuck to the path)
    const cleaned = match[0].replace(/([A-Za-z0-9_-]+)(?=[A-Z][a-z])/, "$1"); // optional: fine-tune

    return cleaned;
  }

  function feature_insertInfoCopyButton() {
    const projectCode = getProjectCode();

    let deadlineText = getDeadlineText();
    deadlineText = deadlineText?.replace(/\s*\(.*?\)\s*$/, "") ?? "";

    const serverPath = getServerPath();

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

    previousParent.style.paddingLeft = "0"; // ← add this line

    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 8px 6px 8px";
      btn.style.width = "fit-content";

      btn.addEventListener("click", () => {
        navigator.clipboard.writeText(valueText);
      });

      const text = document.createElement("span");
      text.textContent = valueText;
      text.title = valueText;
      text.style.userSelect = "text";
      text.style.fontWeight = "normal";
      text.style.flex = "1";
      text.style.whiteSpace = "nowrap";
      text.style.overflow = "hidden";
      text.style.textOverflow = "ellipsis";

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

      return row;
    }

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

    container.appendChild(overlay);
  }

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

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

  // const debouncedUpdate = debounce(runAllEnhancements, 150);

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

  // window.addEventListener("load", runAllEnhancements);
  setInterval(runAllEnhancements, 150);
  // setInterval(feature_updateJiraTime, 150);
})();