// ==UserScript==
// @name Jiracula
// @namespace https://www.nicsys.eu/
// @description Bite Jira in the neck and inject your timesheets !
// @author Nike
// @match https://citnet.tech.ec.europa.eu/CITnet/jira/secure/Tempo.jspa*
// @grant none
// @version 1.0
// @grant GM_notification
// ==/UserScript==
// ___
// | |
// | |__ __
// | |__|___________ ____ __ __| | _____
// | | \_ __ \__ \ _/ ___\| | \ | \__ \
// /\__| | || | \// __ \\ \___| | / |__/ __ \_
// \_____ |__||__| (____ /\_____>____/|____(______/
// \/ \/
// By Nike
(function() {
'use strict'
function addBtn(){
const ul = document.querySelector('div.aui-header-primary ul.aui-nav')
const style=`
background: linear-gradient(to bottom, #b30000, #4d0000);
color: white !important;
text-shadow: 0 0 2px #ff0000, 0 0 5px #800000;
border: 1px solid #330000;
`
ul.insertAdjacentHTML('beforeend',`
Jiracula`)
const btn = ul.lastElementChild.querySelector('a')
btn.addEventListener('click', biteJira)
}
function alert(type, msg){
window.postMessage({
from: 'jiracula',
type: type,
message: msg
}, '*')
}
function biteJira(){
fileSelector(async content => {
let tasksData = null
try{
tasksData = JSON.parse(content)
if(!Array.isArray(taskData)) throw new Error('Not an array!')
} catch(err){
alert('Error', 'Could not parse the file !\n(Should be JSON array)')
}
if(tasksData){
const euid = document.getElementById('header-details-user-fullname').dataset.username
const userKey = await getUserKey(euid)
const modal = showProgressModal("Encoding worklogs…")
const percInc = 100/tasksData.length
let perct = 0
for(const row of tasksData){
console.log('===Injecting==>', row)
const taskId = await getTaskId(row.TaskCode)
console.log('=taskID==>', taskId)
await postWorklog({
"attributes": {},
"billableSeconds": "",
"worker": userKey,
"comment": row.Description,
"started": toIsoDate(row.Date),
"timeSpentSeconds": 3600*row.Duration,
"originTaskId": taskId,
"remainingEstimate": null,
"endDate": null,
"includeNonWorkingDays": false
})
perct += percInc
modal.update(perct)
}
modal.close()
//forceReload()
}
})
}
function forceReload() {
const url = new URL(location.href)
url.searchParams.set('_cb', Math.random().toString(36).slice(2))
location.replace(url.toString())
}
function fileSelector(contentReady){
const input = document.createElement('input')
input.type = 'file'
input.accept = '.jrcl.json' // optional filter
input.style.display = 'none'
document.body.appendChild(input)
input.addEventListener('change', () => {
const file = input.files[0]
const reader = new FileReader()
reader.onload = e => { contentReady(e.target.result) }
reader.readAsText(file)
})
input.click()
}
async function getUserKey(EULogin){
const url = `https://citnet.tech.ec.europa.eu/CITnet/jira/rest/api/2/user?username=${EULogin}`
const res = await fetch(url, {
method: "GET",
credentials: "same-origin", // 🔑 ensures JSESSIONID + XSRF cookies are sent
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Referer": "https://citnet.tech.ec.europa.eu/CITnet/jira/secure/Tempo.jspa"
}
})
const resObject = await res.json()
return(resObject.key)
}
async function getTaskId(code){
const url = `https://citnet.tech.ec.europa.eu/CITnet/jira/rest/api/latest/issue/${code}`
const res = await fetch(url, {
method: "GET",
credentials: "same-origin", // 🔑 ensures JSESSIONID + XSRF cookies are sent
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Referer": "https://citnet.tech.ec.europa.eu/CITnet/jira/secure/Tempo.jspa"
}
})
const resObject = await res.json()
return(resObject.id)
}
async function postWorklog(data) {
const url = "https://citnet.tech.ec.europa.eu/CITnet/jira/rest/tempo-timesheets/4/worklogs/"
const res = await fetch(url, {
method: "POST",
credentials: "same-origin", // 🔑 ensures JSESSIONID + XSRF cookies are sent
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Referer": "https://citnet.tech.ec.europa.eu/CITnet/jira/secure/Tempo.jspa"
},
body: JSON.stringify(data)
})
if (!res.ok) {
throw new Error(`Jiracula failed: ${res.status} ${res.statusText}`)
}
return await res.json()
}
function toIsoDate(dmy) {
const [d, m, y] = dmy.split('/')
const fullYear = 2000 + Number(y) // adjust century rule if needed
return `${fullYear}-${m.padStart(2, '0')}-${d.padStart(2, '0')}T00:00:00.000`
}
function showProgressModal(title = "Working…") {
const style = document.createElement('style')
style.textContent = `
#jiracula-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6);
display: flex; align-items: center; justify-content: center;
z-index: 999999;
}
#jiracula-box {
background: #fff; padding: 20px; border-radius: 8px;
box-shadow: 0 0 20px rgba(0,0,0,0.5);
min-width: 300px; text-align: center;
font-family: sans-serif;
}
#jiracula-box h2 { margin: 0 0 10px; font-size: 18px; }
#jiracula-box progress { width: 100%; }
`
document.head.appendChild(style)
const modal = document.createElement('div')
modal.id = 'jiracula-modal'
modal.innerHTML = `
`
document.body.appendChild(modal)
return {
update(val) {
document.getElementById('jiracula-progress').value = val
},
close() {
modal.remove()
style.remove()
}
}
}
addBtn()
window.addEventListener('message', e => { // Listen for page→sandbox messages
if (e.source !== window) return // only our own page
if (!e.data || e.data.from !== 'jiracula') return
if (e.data.type === 'error') {
console.error('Jiracula error:', e.data.message)
if (typeof GM_notification === 'function') {
GM_notification({
title: 'Jiracula Error',
text: e.data.message,
timeout: 5000
})
} else {
alert('Jiracula Error: ' + e.data.message)
}
}
})
})()
/**
const modal = showProgressModal("Uploading worklogs…")
let progress = 0
const interval = setInterval(() => {
progress += 10
modal.update(progress)
if (progress >= 100) {
clearInterval(interval)
modal.close()
}
}, 500)
*/