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