const API_BASE = ''; let allTickets = []; let filteredTickets = []; let customFieldsMap = []; let lastUsername = ''; let lastApiToken = ''; let currentGanttView = 'month'; let isGanttExpanded = false; let currentSortCol = null; let isAsc = true; // --- JIRA STATUS CONFIG & MAPPING --- const STATUS_DEFINITIONS = { 'Opportunity': { label: 'Opportunity', cssClass: 's-opportunity', dark: { color: '#c084fc', bg: 'rgba(192, 132, 252, 0.16)', border: '#a855f7', badgeBg: '#9333ea', badgeText: '#ffffff' }, light: { color: '#6d28d9', bg: '#f5f3ff', border: '#7c3aed', badgeBg: '#7c3aed', badgeText: '#ffffff' }, drawioColor: '#9333ea' }, 'Delayed': { label: 'Delayed', cssClass: 's-delayed', dark: { color: '#94a3b8', bg: 'rgba(148, 163, 184, 0.16)', border: '#64748b', badgeBg: '#475569', badgeText: '#ffffff' }, light: { color: '#334155', bg: '#f1f5f9', border: '#64748b', badgeBg: '#475569', badgeText: '#ffffff' }, drawioColor: '#64748b' }, 'To be Analysed': { label: 'To be Analysed', cssClass: 's-analyse', dark: { color: '#fbbf24', bg: 'rgba(251, 191, 36, 0.16)', border: '#f59e0b', badgeBg: '#d97706', badgeText: '#ffffff' }, light: { color: '#92400e', bg: '#fffbeb', border: '#d97706', badgeBg: '#b45309', badgeText: '#ffffff' }, drawioColor: '#d97706' }, 'Analysed': { label: 'Analysed', cssClass: 's-analysed', dark: { color: '#22d3ee', bg: 'rgba(34, 211, 238, 0.16)', border: '#06b6d4', badgeBg: '#0891b2', badgeText: '#ffffff' }, light: { color: '#0e7490', bg: '#ecfeff', border: '#0891b2', badgeBg: '#0e7490', badgeText: '#ffffff' }, drawioColor: '#0891b2' }, 'Project Ongoing': { label: 'Project Ongoing', cssClass: 's-ongoing', dark: { color: '#60a5fa', bg: 'rgba(96, 165, 250, 0.20)', border: '#3b82f6', badgeBg: '#2563eb', badgeText: '#ffffff' }, light: { color: '#1e40af', bg: '#eff6ff', border: '#2563eb', badgeBg: '#1d4ed8', badgeText: '#ffffff' }, drawioColor: '#2563eb' }, 'Rejected': { label: 'Rejected', cssClass: 's-rejected', dark: { color: '#f87171', bg: 'rgba(248, 113, 113, 0.18)', border: '#ef4444', badgeBg: '#dc2626', badgeText: '#ffffff' }, light: { color: '#991b1b', bg: '#fef2f2', border: '#dc2626', badgeBg: '#dc2626', badgeText: '#ffffff' }, drawioColor: '#dc2626' }, 'Done': { label: 'Done', cssClass: 's-done', dark: { color: '#34d399', bg: 'rgba(52, 211, 153, 0.18)', border: '#10b981', badgeBg: '#059669', badgeText: '#ffffff' }, light: { color: '#065f46', bg: '#ecfdf5', border: '#059669', badgeBg: '#047857', badgeText: '#ffffff' }, drawioColor: '#059669' } }; function mapJiraStatus(rawStatus) { if (!rawStatus) return 'Opportunity'; const s = String(rawStatus).trim().toLowerCase(); // "Resolved" and "Closed" => DONE if (s.includes('done') || s.includes('resolv') || s.includes('clos') || s.includes('ferm') || s.includes('termin') || s.includes('rsolu')) { return 'Done'; } // "Rejected" => Rejected if (s.includes('reject') || s.includes('rejet') || s.includes('refus') || s.includes('abandon')) { return 'Rejected'; } // "In Progress" => Project Ongoing if (s.includes('progress') || s.includes('en cours') || s.includes('ongoing') || s.includes('review') || s.includes('dev')) { return 'Project Ongoing'; } // "On Hold" => Analysed if (s.includes('hold') || s.includes('analysé') || s.includes('analysed') || s.includes('attente')) { return 'Analysed'; } // "Open" and "SPQI" => To be Analysed if (s === 'open' || s.includes('spqi') || s.includes('ouvert') || s.includes('to analyse') || s.includes('to be analysed')) { return 'To be Analysed'; } // "Delayed" => Delayed if (s.includes('delay') || s.includes('retard') || s.includes('reporté')) { return 'Delayed'; } // "New" and "Reopened" => Opportunity if (s.includes('new') || s.includes('reopen') || s.includes('nouveau') || s.includes('opportunit') || s.includes('backlog')) { return 'Opportunity'; } return 'Opportunity'; } function getStatusThemeConfig(mappedStatus) { const isLight = document.documentElement.getAttribute('data-theme') === 'light'; const def = STATUS_DEFINITIONS[mappedStatus] || STATUS_DEFINITIONS['Opportunity']; return isLight ? def.light : def.dark; } function getStatusClass(s) { const mapped = mapJiraStatus(s); const def = STATUS_DEFINITIONS[mapped]; return def ? def.cssClass : 's-opportunity'; } // --- THEME MANAGEMENT (LIGHT / DARK) --- function initTheme() { const savedTheme = localStorage.getItem('jira_extractor_theme') || 'dark'; setTheme(savedTheme, false); } function toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; const newTheme = currentTheme === 'light' ? 'dark' : 'light'; setTheme(newTheme, true); } function setTheme(theme, withToast = false) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('jira_extractor_theme', theme); const icon = document.getElementById('themeIcon'); const label = document.getElementById('themeLabel'); if (icon && label) { if (theme === 'light') { icon.className = 'fa-solid fa-moon'; label.textContent = 'Dark mode'; } else { icon.className = 'fa-solid fa-sun'; label.textContent = 'Light mode'; } } if (document.getElementById('panel-roadmap') && document.getElementById('panel-roadmap').classList.contains('active')) { renderRoadmap(); } if (withToast) { showToast(theme === 'light' ? 'Light mode enabled' : 'Dark mode enabled'); } } // Save settings to local storage function saveSettings() { const settings = { projectKey: document.getElementById('projectKey').value, customFields: document.getElementById('customFields').value, jqlQuery: document.getElementById('jqlQuery').value }; localStorage.setItem('jira_roadmap_settings', JSON.stringify(settings)); } // Load settings on startup function loadSettings() { const saved = localStorage.getItem('jira_roadmap_settings'); if (saved) { const settings = JSON.parse(saved); document.getElementById('projectKey').value = settings.projectKey || ''; document.getElementById('customFields').value = settings.customFields || ''; document.getElementById('jqlQuery').value = settings.jqlQuery || ''; } } // --- UTILITIES --- function showToast(msg, type='success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); t.className = `toast toast-${type}`; t.innerHTML = `${msg}`; c.appendChild(t); setTimeout(() => t.remove(), 3500); } function autoJql() { const key = document.getElementById('projectKey').value.trim().toUpperCase(); const jql = document.getElementById('jqlQuery'); if (!jql.value.trim() || jql.value.trim().startsWith('project =')) { jql.value = `project = "${key}" ORDER BY created DESC`; } } async function pingApi() { saveSettings(); const btn = event.target.closest('.btn'); const orig = btn.innerHTML; btn.innerHTML = ' Testing...'; btn.disabled = true; try { const r = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({username:'_', apiToken:'_', projectKey:'_'}) }); if (r.status === 400) showToast('Local API connected!', 'success'); else showToast('API responded: ' + r.status, 'success'); } catch(e) { showToast('API unreachable. Please run "npm start".', 'error'); } finally { btn.innerHTML = orig; btn.disabled = false; } } async function startExtraction() { saveSettings(); const btn = document.getElementById('extractBtn'); const user = document.getElementById('username').value.trim(); const token = document.getElementById('apiToken').value.trim(); const project = document.getElementById('projectKey').value.trim().toUpperCase(); const jql = document.getElementById('jqlQuery').value.trim(); const custom = document.getElementById('customFields').value.trim(); if (!user || !token || !project) { showToast('Required fields missing.', 'error'); return; } btn.innerHTML = ' Extracting...'; btn.disabled = true; try { const resp = await fetch(`${API_BASE}/api/extract`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: user, apiToken: token, projectKey: project, jql: jql, customFields: custom }) }); const data = await resp.json(); if (!resp.ok) throw new Error(data.error || `Error ${resp.status}`); allTickets = data.tickets; filteredTickets = [...allTickets]; customFieldsMap = parseCustomFields(custom); lastUsername = user; lastApiToken = token; renderResults(); showToast(`${allTickets.length} tickets extracted.`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { btn.innerHTML = ' Start Extraction'; btn.disabled = false; } } function parseCustomFields(customFields) { const map = []; if (customFields) { customFields.split(',').forEach(cf => { const [id, label] = cf.split(':').map(s => s.trim()); if (id) map.push({ id, label: label || id }); }); } return map; } function sortTable(colKey) { if (currentSortCol === colKey) { isAsc = !isAsc; } else { currentSortCol = colKey; isAsc = true; } filteredTickets.sort((a, b) => { let valA = a[colKey] || ''; let valB = b[colKey] || ''; if (colKey.toLowerCase().includes('date') || colKey === 'created' || colKey === 'dueDate') { valA = new Date(valA); valB = new Date(valB); } if (valA < valB) return isAsc ? -1 : 1; if (valA > valB) return isAsc ? 1 : -1; return 0; }); renderTable(); } // --- TICKET UPDATE --- async function updateTicket(ticketKey, dueDate, goLiveDate) { if (!lastUsername || !lastApiToken) { showToast('Please extract tickets first.', 'error'); return; } const btn = document.querySelector(`[data-ticket="${ticketKey}"]`); const orig = btn.innerHTML; btn.innerHTML = ''; btn.disabled = true; const goLiveField = customFieldsMap.find(cf => cf.label.toLowerCase().includes('go live')); const updates = {}; if (dueDate !== undefined) updates.duedate = dueDate; if (goLiveDate !== undefined && goLiveField) updates[goLiveField.id] = goLiveDate; try { const resp = await fetch(`${API_BASE}/api/update`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: lastUsername, apiToken: lastApiToken, ticketKey, updates }) }); if (!resp.ok) { const data = await resp.json(); throw new Error(data.error || `HTTP Error ${resp.status}`); } const ticket = allTickets.find(t => t.key === ticketKey); if (ticket) { if (updates.duedate !== undefined) ticket.dueDate = updates.duedate; if (goLiveField && updates[goLiveField.id] !== undefined) ticket[goLiveField.label] = updates[goLiveField.id]; } renderTable(); renderRoadmap(); showToast(`Ticket ${ticketKey} updated!`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { btn.innerHTML = orig; btn.disabled = false; } } function enableUpdateBtn(ticketKey) { const btn = document.getElementById(`update-${ticketKey}`); if (btn) btn.disabled = false; } function handleUpdateClick(ticketKey, goLiveColName) { const dueInput = document.getElementById(`due-${ticketKey}`); const dueDate = dueInput && dueInput.value ? dueInput.value : null; let goLiveDate = null; if (goLiveColName) { const goLiveInput = document.getElementById(`gl-${ticketKey}`); goLiveDate = goLiveInput && goLiveInput.value ? goLiveInput.value : null; } updateTicket(ticketKey, dueDate, goLiveDate); } // --- RENDERING --- function renderResults() { document.getElementById('results-section').classList.add('visible'); renderStats(); renderTable(); renderJSON(); renderRoadmap(); } function renderStats() { const unassigned = allTickets.filter(t => !t.assignee).length; const now = new Date(); const overdue = allTickets.filter(t => t.dueDate && mapJiraStatus(t.status) !== 'Done' && new Date(t.dueDate) < now).length; document.getElementById('statsRow').innerHTML = `
${allTickets.length}
Tickets
${new Set(allTickets.map(t=>mapJiraStatus(t.status))).size}
Statuses
${unassigned}
Unassigned
${overdue}
Overdue
`; } function escHtml(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=String(s); return d.innerHTML; } function fmtDate(d) { if(!d) return '-'; try { return new Date(d).toLocaleDateString('en-US',{day:'2-digit',month:'short',year:'numeric'}); } catch { return escHtml(d); } } function renderTable() { let goLiveColName = null; if (allTickets.length > 0) { goLiveColName = Object.keys(allTickets[0]).find(key => !['key','url','summary','description','status','statusCategory','assignee','reporter','priority','issueType','created','updated','dueDate','resolution','resolutionDate','labels','components','fixVersions','projectName'].includes(key) ); } const colMapping = { 'Ticket': 'key', 'Summary': 'summary', 'Status': 'status', 'Assignee': 'assignee', 'Priority': 'priority', 'Type': 'issueType', 'Due Date': 'dueDate' }; const cols = ['Ticket','Summary','Status','Assignee','Priority','Type','Due Date']; if (goLiveColName) { cols.push(goLiveColName); colMapping[goLiveColName] = goLiveColName; } cols.push('Actions'); document.getElementById('tableHead').innerHTML = '' + cols.map(c => { const key = colMapping[c]; if (!key) return `${c}`; const icon = currentSortCol === key ? (isAsc ? ' ▴' : ' ▾') : ''; return ` ${c}${icon} `; }).join('') + ''; let body = ''; filteredTickets.forEach(t => { const dueDateValue = t.dueDate ? t.dueDate.substring(0, 10) : ''; const mappedStatus = mapJiraStatus(t.status); body += ` ${escHtml(t.key)} ${escHtml(t.summary)} ${escHtml(mappedStatus)} ${escHtml(t.assignee) || '-'} ${escHtml(t.priority)} ${escHtml(t.issueType)} `; if (goLiveColName) { const glVal = t[goLiveColName] ? t[goLiveColName].substring(0, 10) : ''; body += ``; } body += ` `; }); document.getElementById('tableBody').innerHTML = body || 'No results found'; } // --- ROADMAP (GANTT) --- function setGanttView(view) { currentGanttView = view; renderRoadmap(); } function toggleGanttExpand() { isGanttExpanded = !isGanttExpanded; updateGanttExpandState(); } function updateGanttExpandState() { const container = document.querySelector('.gantt-container'); const icon = document.getElementById('ganttExpandIcon'); const label = document.getElementById('ganttExpandLabel'); const btn = document.getElementById('ganttExpandBtn'); if (container) { if (isGanttExpanded) { container.classList.add('expanded'); } else { container.classList.remove('expanded'); } } if (icon && label) { if (isGanttExpanded) { icon.className = 'fa-solid fa-down-left-and-up-right-to-center'; label.textContent = 'Collapse'; if (btn) btn.classList.add('active'); } else { icon.className = 'fa-solid fa-up-right-and-down-left-from-center'; label.textContent = 'Expand'; if (btn) btn.classList.remove('active'); } } } // --- GANTT UTILITIES --- /** * Extracts short title and project type according to the pattern: "Title - Type - Amount - ID" */ function parseTicketSummary(summary) { if (!summary) return { shortTitle: 'Untitled', type: 'OTHER' }; const parts = summary.split(' - '); const shortTitle = parts[0].trim(); const typePart = parts[1] ? parts[1].trim().toUpperCase() : 'OTHER'; const type = ['PR', 'CR', 'TR'].includes(typePart) ? typePart : 'OTHER'; return { shortTitle, type }; } /** * Defines activity type accent color */ function getTypeColor(type) { const isLight = document.documentElement.getAttribute('data-theme') === 'light'; switch(type) { case 'PR': return isLight ? '#0284c7' : '#38bdf8'; // Blue case 'CR': return isLight ? '#b45309' : '#fbbf24'; // Amber case 'TR': return isLight ? '#6d28d9' : '#c084fc'; // Purple default: return isLight ? '#475569' : '#94a3b8'; // Gray } } /** * MAIN ROADMAP RENDERING FUNCTION */ function renderRoadmap() { const preview = document.getElementById('roadmapPreview'); if (!filteredTickets || filteredTickets.length === 0) { preview.innerHTML = `
No tickets to display.
`; return; } const goLiveColName = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "Go Live Date"; const leftWidth = 280; let colWidth = 140; if (currentGanttView === 'quarter') colWidth = 200; if (currentGanttView === 'year') colWidth = 250; // Filter tickets with at least one date const ticketsWithDates = filteredTickets.filter(t => { const d1 = t.dueDate ? new Date(t.dueDate) : null; const d2 = t[goLiveColName] ? new Date(t[goLiveColName]) : null; return (d1 && !isNaN(d1.getTime())) || (d2 && !isNaN(d2.getTime())); }); if (ticketsWithDates.length === 0) { preview.innerHTML = `
No Due Date or Go Live date found for filtered tickets.
`; return; } const allDates = ticketsWithDates.flatMap(t => [ t.dueDate ? new Date(t.dueDate) : null, t[goLiveColName] ? new Date(t[goLiveColName]) : null ].filter(d => d !== null)); let minDate = new Date(Math.min(...allDates)); let maxDate = new Date(Math.max(...allDates)); minDate.setDate(1); if (currentGanttView === 'quarter') minDate.setMonth(Math.floor(minDate.getMonth() / 3) * 3); if (currentGanttView === 'year') minDate.setMonth(0); maxDate.setMonth(maxDate.getMonth() + 2); maxDate.setDate(0); const timeScale = []; let tempDate = new Date(minDate); while (tempDate <= maxDate) { let label = "", key = ""; if (currentGanttView === 'month') { label = tempDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); key = `${tempDate.getFullYear()}-${tempDate.getMonth()}`; tempDate.setMonth(tempDate.getMonth() + 1); } else if (currentGanttView === 'quarter') { const q = Math.floor(tempDate.getMonth() / 3) + 1; label = `Q${q} ${tempDate.getFullYear()}`; key = `${tempDate.getFullYear()}-Q${q}`; tempDate.setMonth(tempDate.getMonth() + 3); } else if (currentGanttView === 'year') { label = `${tempDate.getFullYear()}`; key = `${tempDate.getFullYear()}`; tempDate.setFullYear(tempDate.getFullYear() + 1); } timeScale.push({ key, label }); } const getXPos = (date) => { if (!date) return 0; const d = new Date(date); let index = -1; if (currentGanttView === 'month') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}-${d.getMonth()}`); else if (currentGanttView === 'quarter') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}-Q${Math.floor(d.getMonth() / 3) + 1}`); else if (currentGanttView === 'year') index = timeScale.findIndex(m => m.key === `${d.getFullYear()}`); if (index === -1) return 0; let ratio = 0; if (currentGanttView === 'month') ratio = (d.getDate() - 1) / 30; else if (currentGanttView === 'quarter') ratio = ((d.getMonth() % 3) * 30 + d.getDate()) / 90; else if (currentGanttView === 'year') ratio = (d.getMonth() * 30 + d.getDate()) / 365; return leftWidth + (index * colWidth) + (ratio * colWidth); }; const grouped = ticketsWithDates.reduce((acc, t) => { const name = t.assignee || "Unassigned"; if (!acc[name]) acc[name] = []; acc[name].push(t); return acc; }, {}); let html = `
`; const todayX = getXPos(new Date()); if (todayX > leftWidth) { html += `
`; html += `
Today
`; } html += `
Assignee / Ticket (Title)
`; timeScale.forEach(ts => { html += `
${ts.label}
`; }); html += `
`; Object.keys(grouped).sort().forEach(assignee => { html += `
${assignee}
`; const sortedTickets = grouped[assignee].sort((a, b) => (new Date(a.dueDate || 0)) - (new Date(b.dueDate || 0))); sortedTickets.forEach(t => { const dDue = t.dueDate ? new Date(t.dueDate) : null; const dLive = t[goLiveColName] ? new Date(t[goLiveColName]) : null; const start = dDue || dLive; const end = dLive || dDue; const x1 = getXPos(start); const x2 = getXPos(end); const barWidth = Math.max(170, (x2 - x1) + 10); const { shortTitle, type } = parseTicketSummary(t.summary); const mappedStatus = mapJiraStatus(t.status); const statusCfg = getStatusThemeConfig(mappedStatus); const typeColor = getTypeColor(type); html += `
${escHtml(shortTitle)}
${escHtml(mappedStatus)}
${type} ${escHtml(shortTitle)}
${timeScale.map(() => `
`).join('')}
`; }); }); html += `
`; preview.innerHTML = html; setTimeout(() => { const m = document.getElementById('todayMarker'); if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); }, 150); } // --- TAB & FILTER --- function switchTab(id, btn) { document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active')); btn.classList.add('active'); document.getElementById(`panel-${id}`).classList.add('active'); if(id === 'roadmap') renderRoadmap(); } function filterTable() { const q = document.getElementById('searchInput').value.toLowerCase(); filteredTickets = q ? allTickets.filter(t => Object.values(t).some(v => v && String(v).toLowerCase().includes(q))) : [...allTickets]; renderTable(); if(document.getElementById('panel-roadmap').classList.contains('active')) renderRoadmap(); } function exportDrawIOXML(mode) { try { if (!filteredTickets || filteredTickets.length === 0) { showToast("No tickets to export", "error"); return; } const goLiveCol = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "dueDate"; const ticketsWithDates = filteredTickets.filter(t => t.dueDate || t[goLiveCol]); if (ticketsWithDates.length === 0) { showToast("No end dates found", "error"); return; } const leftWidth = 250; const rowHeight = 50; let pixelPerDay = 12; if (currentGanttView === 'quarter') pixelPerDay = 4; if (currentGanttView === 'year') pixelPerDay = 1.2; const allDates = ticketsWithDates.map(t => new Date(t.dueDate || t[goLiveCol])); let minDate = new Date(Math.min(...allDates)); minDate.setDate(1); let maxDate = new Date(Math.max(...allDates)); maxDate.setMonth(maxDate.getMonth() + 4); const getX = (dateStr) => { const d = new Date(dateStr); if (isNaN(d.getTime())) return leftWidth; return Math.floor(leftWidth + ((d - minDate) / 86400000 * pixelPerDay)); }; let xmlNodes = ''; let curr = new Date(minDate); while (curr < maxDate) { const xPos = getX(curr); let label = ""; if (currentGanttView === 'month') { label = curr.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }).toUpperCase(); curr.setMonth(curr.getMonth() + 1); } else if (currentGanttView === 'quarter') { label = "Q" + (Math.floor(curr.getMonth() / 3) + 1) + " " + curr.getFullYear(); curr.setMonth(curr.getMonth() + 3); } else { label = curr.getFullYear(); curr.setFullYear(curr.getFullYear() + 1); } xmlNodes += ` `; xmlNodes += ` `; } const xToday = getX(new Date()); xmlNodes += ` `; let currentY = 80; const groups = ticketsWithDates.reduce((acc, t) => { const a = t.assignee || 'Unassigned'; if (!acc[a]) acc[a] = []; acc[a].push(t); return acc; }, {}); Object.keys(groups).sort().forEach(assignee => { xmlNodes += ` `; currentY += 40; groups[assignee].forEach(t => { const { shortTitle, type } = parseTicketSummary(t.summary); const mappedStatus = mapJiraStatus(t.status); const statusDef = STATUS_DEFINITIONS[mappedStatus] || STATUS_DEFINITIONS['Opportunity']; const barColor = statusDef.drawioColor; const startDate = t.created ? new Date(t.created) : minDate; const endDate = new Date(t.dueDate || t[goLiveCol]); const xStart = getX(startDate); const xEnd = getX(endDate); let barWidth = xEnd - xStart; if (barWidth < 50) barWidth = 140; xmlNodes += ` `; const label = `[${mappedStatus}] [${type}] ${escHtml(shortTitle)}`; xmlNodes += ` `; currentY += rowHeight; }); currentY += 20; }); const finalXml = ` ${xmlNodes} `; if (mode === 'download') { const blob = new Blob([finalXml], { type: 'text/xml' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = "jira_export.drawio"; a.click(); } else { navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copied!")); } } catch (error) { console.error(error); showToast("Error during XML export", "error"); } } function renderJSON() { document.getElementById('jsonViewer').innerHTML = syntaxHL(JSON.stringify(allTickets, null, 2)); } function syntaxHL(json) { json = json.replace(/&/g,'&').replace(//g,'>'); return json.replace(/("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, m => { let c = 'json-number'; if(/^"/.test(m)) c = /:$/.test(m)?'json-key':'json-string'; else if(/true|false/.test(m)) c='json-bool'; else if(/null/.test(m)) c='json-null'; return `${m}`; }); } function downloadJSON() { const blob = new Blob([JSON.stringify(allTickets, null, 2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `export-${Date.now()}.json`; a.click(); } function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(() => showToast('Copied!')); } function clearResults() { allTickets=[]; filteredTickets=[]; document.getElementById('results-section').classList.remove('visible'); } document.addEventListener('keydown', e => { if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();startExtraction()} }); document.addEventListener('DOMContentLoaded', () => { initTheme(); loadSettings(); console.log("Settings loaded from local storage."); });