// ==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 = `

    ${title}

    ` 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) */