diff --git a/index.html b/index.html index b686293..e94a459 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + @@ -24,53 +24,53 @@

Jira Ticket Extractor

-

Extraction via API locale — Proxy transparent

+

Extraction via local API — Transparent proxy

API : localhost:3000
-
-
Paramètres d'extraction
+
Extraction Settings
-
Le proxy d'entreprise et l'URL Jira sont gérés par le backend (config.ini). Ici, fournissez uniquement vos identifiants Jira et les filtres du projet.
+
Corporate proxy and Jira URL are handled by the backend (config.ini). Provide only your Jira credentials and project filters below.
- - + +
- - - Jira Cloud : Personal Access Token. Jira Server : mot de passe. + + + Jira Cloud : Personal Access Token. Jira Server : password.
- +
- + - Format : customfield_ID:NomAffiché, séparés par virgules + Format : customfield_ID:DisplayName, comma separated
- +
@@ -81,21 +81,21 @@
- + - +
- - - + + +
@@ -111,15 +111,15 @@
- Vue : + View :
- - - + + +
-
@@ -129,10 +129,10 @@
diff --git a/public/css/style.css b/public/css/style.css index 00d3333..693d189 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -23,6 +23,45 @@ --gantt-sticky-bg: #1a1a1a; --gantt-group-bg: rgba(255, 255, 255, 0.03); --gantt-grid-border: rgba(255, 255, 255, 0.05); + + /* Status Colors - Dark Mode */ + --status-opportunity: #c084fc; + --status-opportunity-bg: rgba(192, 132, 252, 0.15); + --status-opportunity-border: #a855f7; + --status-opportunity-badge: #9333ea; + + --status-delayed: #94a3b8; + --status-delayed-bg: rgba(148, 163, 184, 0.15); + --status-delayed-border: #64748b; + --status-delayed-badge: #475569; + + --status-analyse: #fbbf24; + --status-analyse-bg: rgba(251, 191, 36, 0.15); + --status-analyse-border: #f59e0b; + --status-analyse-badge: #d97706; + + --status-analysed: #22d3ee; + --status-analysed-bg: rgba(34, 211, 238, 0.15); + --status-analysed-border: #06b6d4; + --status-analysed-badge: #0891b2; + + --status-ongoing: #60a5fa; + --status-ongoing-bg: rgba(96, 165, 250, 0.18); + --status-ongoing-border: #3b82f6; + --status-ongoing-badge: #2563eb; + + --status-rejected: #f87171; + --status-rejected-bg: rgba(248, 113, 113, 0.16); + --status-rejected-border: #ef4444; + --status-rejected-badge: #dc2626; + + --status-done: #34d399; + --status-done-bg: rgba(52, 211, 153, 0.16); + --status-done-border: #10b981; + --status-done-badge: #059669; + + --gantt-bar-bg: #141f19; + --gantt-bar-border: rgba(255, 255, 255, 0.12); } [data-theme="light"] { @@ -48,6 +87,45 @@ --gantt-sticky-bg: #e6ede8; --gantt-group-bg: rgba(0, 0, 0, 0.03); --gantt-grid-border: rgba(0, 0, 0, 0.06); + + /* Status Colors - Light Mode (High Contrast) */ + --status-opportunity: #6d28d9; + --status-opportunity-bg: #f5f3ff; + --status-opportunity-border: #7c3aed; + --status-opportunity-badge: #7c3aed; + + --status-delayed: #334155; + --status-delayed-bg: #f1f5f9; + --status-delayed-border: #64748b; + --status-delayed-badge: #475569; + + --status-analyse: #92400e; + --status-analyse-bg: #fffbeb; + --status-analyse-border: #d97706; + --status-analyse-badge: #b45309; + + --status-analysed: #0e7490; + --status-analysed-bg: #ecfeff; + --status-analysed-border: #0891b2; + --status-analysed-badge: #0e7490; + + --status-ongoing: #1e40af; + --status-ongoing-bg: #eff6ff; + --status-ongoing-border: #2563eb; + --status-ongoing-badge: #1d4ed8; + + --status-rejected: #991b1b; + --status-rejected-bg: #fef2f2; + --status-rejected-border: #dc2626; + --status-rejected-badge: #dc2626; + + --status-done: #065f46; + --status-done-bg: #ecfdf5; + --status-done-border: #059669; + --status-done-badge: #047857; + + --gantt-bar-bg: #ffffff; + --gantt-bar-border: #cbd5e1; } * { @@ -543,58 +621,69 @@ tr:hover td { padding: 3px 10px; border-radius: 20px; font-size: 11px; - font-weight: 600; + font-weight: 700; white-space: nowrap; + border: 1px solid transparent; +} + +.s-opportunity { + background: var(--status-opportunity-bg); + color: var(--status-opportunity); + border-color: var(--status-opportunity-border); +} + +.s-delayed { + background: var(--status-delayed-bg); + color: var(--status-delayed); + border-color: var(--status-delayed-border); +} + +.s-analyse, .s-to-be-analysed { + background: var(--status-analyse-bg); + color: var(--status-analyse); + border-color: var(--status-analyse-border); +} + +.s-analysed { + background: var(--status-analysed-bg); + color: var(--status-analysed); + border-color: var(--status-analysed-border); +} + +.s-ongoing, .s-project-ongoing, .s-progress { + background: var(--status-ongoing-bg); + color: var(--status-ongoing); + border-color: var(--status-ongoing-border); +} + +.s-rejected { + background: var(--status-rejected-bg); + color: var(--status-rejected); + border-color: var(--status-rejected-border); } .s-done { - background: rgba(0, 230, 138, 0.15); - color: #00e68a; -} - -.s-progress { - background: rgba(77, 200, 255, 0.15); - color: #4dc8ff; + background: var(--status-done-bg); + color: var(--status-done); + border-color: var(--status-done-border); } .s-todo { - background: rgba(255, 184, 77, 0.15); - color: #ffb84d; + background: var(--status-opportunity-bg); + color: var(--status-opportunity); + border-color: var(--status-opportunity-border); } .s-blocked { - background: rgba(255, 77, 106, 0.15); - color: #ff4d6a; + background: var(--status-rejected-bg); + color: var(--status-rejected); + border-color: var(--status-rejected-border); } .s-other { - background: rgba(122, 148, 136, 0.15); - color: #7a9488; -} - -[data-theme="light"] .s-done { - background: rgba(0, 153, 92, 0.12); - color: #007a49; -} - -[data-theme="light"] .s-progress { - background: rgba(2, 132, 199, 0.12); - color: #0284c7; -} - -[data-theme="light"] .s-todo { - background: rgba(217, 119, 6, 0.12); - color: #b45309; -} - -[data-theme="light"] .s-blocked { - background: rgba(220, 38, 38, 0.12); - color: #dc2626; -} - -[data-theme="light"] .s-other { - background: rgba(83, 110, 97, 0.12); - color: #4e6b5d; + background: var(--status-delayed-bg); + color: var(--status-delayed); + border-color: var(--status-delayed-border); } .empty-cell { @@ -1009,18 +1098,68 @@ GANTT /* La barre Gantt elle-même */ .gantt-bar { position: absolute; - height: 28px; - border-radius: 4px; + height: 32px; + border-radius: 6px; display: flex; align-items: center; - padding: 0 8px; - font-size: 10px; + gap: 8px; + padding: 0 10px; + font-size: 11px; color: var(--fg); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - cursor: help; + cursor: pointer; z-index: 10; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + border: 1px solid var(--border); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +[data-theme="light"] .gantt-bar { + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); +} + +.gantt-bar:hover { + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35); + z-index: 25; +} + +[data-theme="light"] .gantt-bar:hover { + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); +} + +.gantt-status-badge { + color: #ffffff; + font-size: 9px; + padding: 3px 7px; + border-radius: 4px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.4px; + white-space: nowrap; + flex-shrink: 0; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); +} + +.gantt-type-tag { + font-weight: 800; + font-size: 10px; + padding: 2px 6px; + border-radius: 4px; + flex-shrink: 0; + font-family: 'JetBrains Mono', monospace; + letter-spacing: 0.3px; +} + +.gantt-bar-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + font-weight: 600; + color: var(--fg); } /* Colonne assigné/ticket figée à gauche */ diff --git a/public/js/script.js b/public/js/script.js index 609db12..70744aa 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -10,7 +10,192 @@ let currentSortCol = null; let isAsc = true; -// --- GESTION DU THÈME (CLAIR / SOMBRE) --- +// --- 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); @@ -31,19 +216,23 @@ function setTheme(theme, withToast = false) { if (icon && label) { if (theme === 'light') { icon.className = 'fa-solid fa-moon'; - label.textContent = 'Mode sombre'; + label.textContent = 'Dark mode'; } else { icon.className = 'fa-solid fa-sun'; - label.textContent = 'Mode clair'; + label.textContent = 'Light mode'; } } + if (document.getElementById('panel-roadmap') && document.getElementById('panel-roadmap').classList.contains('active')) { + renderRoadmap(); + } + if (withToast) { - showToast(theme === 'light' ? 'Mode clair activé' : 'Mode sombre activé'); + showToast(theme === 'light' ? 'Light mode enabled' : 'Dark mode enabled'); } } -// Sauvegarde les paramètres dans le stockage local du navigateur +// Save settings to local storage function saveSettings() { const settings = { projectKey: document.getElementById('projectKey').value, @@ -53,7 +242,7 @@ function saveSettings() { localStorage.setItem('jira_roadmap_settings', JSON.stringify(settings)); } -// Charge les paramètres au démarrage +// Load settings on startup function loadSettings() { const saved = localStorage.getItem('jira_roadmap_settings'); if (saved) { @@ -65,7 +254,7 @@ function loadSettings() { } -// --- UTILITAIRES --- +// --- UTILITIES --- function showToast(msg, type='success') { const c = document.getElementById('toastContainer'); const t = document.createElement('div'); @@ -87,13 +276,13 @@ async function pingApi() { saveSettings(); const btn = event.target.closest('.btn'); - const orig = btn.innerHTML; btn.innerHTML = ' Test...'; btn.disabled = true; + 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('API locale connectée !', 'success'); - else showToast('API répond : ' + r.status, 'success'); + if (r.status === 400) showToast('Local API connected!', 'success'); + else showToast('API responded: ' + r.status, 'success'); } catch(e) { - showToast('API inaccessible. Lancez "npm start".', 'error'); + showToast('API unreachable. Please run "npm start".', 'error'); } finally { btn.innerHTML = orig; btn.disabled = false; } } @@ -107,9 +296,9 @@ async function startExtraction() { const jql = document.getElementById('jqlQuery').value.trim(); const custom = document.getElementById('customFields').value.trim(); - if (!user || !token || !project) { showToast('Champs requis manquants.', 'error'); return; } + if (!user || !token || !project) { showToast('Required fields missing.', 'error'); return; } - btn.innerHTML = ' Extraction...'; btn.disabled = true; + btn.innerHTML = ' Extracting...'; btn.disabled = true; try { const resp = await fetch(`${API_BASE}/api/extract`, { @@ -118,7 +307,7 @@ async function startExtraction() { 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 || `Erreur ${resp.status}`); + if (!resp.ok) throw new Error(data.error || `Error ${resp.status}`); allTickets = data.tickets; filteredTickets = [...allTickets]; @@ -127,11 +316,11 @@ async function startExtraction() { lastApiToken = token; renderResults(); - showToast(`${allTickets.length} tickets extraits.`, 'success'); + showToast(`${allTickets.length} tickets extracted.`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { - btn.innerHTML = ' Lancer l\'extraction'; btn.disabled = false; + btn.innerHTML = ' Start Extraction'; btn.disabled = false; } } @@ -147,7 +336,6 @@ function parseCustomFields(customFields) { } function sortTable(colKey) { - // Si on clique sur la même colonne, on inverse l'ordre, sinon on passe en croissant if (currentSortCol === colKey) { isAsc = !isAsc; } else { @@ -159,7 +347,6 @@ function sortTable(colKey) { let valA = a[colKey] || ''; let valB = b[colKey] || ''; - // Gestion spécifique pour les dates pour un tri chronologique correct if (colKey.toLowerCase().includes('date') || colKey === 'created' || colKey === 'dueDate') { valA = new Date(valA); valB = new Date(valB); @@ -173,10 +360,10 @@ function sortTable(colKey) { renderTable(); } -// --- MISE À JOUR TICKET --- +// --- TICKET UPDATE --- async function updateTicket(ticketKey, dueDate, goLiveDate) { if (!lastUsername || !lastApiToken) { - showToast('Veuillez d\'abord extraire les tickets.', 'error'); + showToast('Please extract tickets first.', 'error'); return; } @@ -198,7 +385,7 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) { }); if (!resp.ok) { const data = await resp.json(); - throw new Error(data.error || `Erreur HTTP ${resp.status}`); + throw new Error(data.error || `HTTP Error ${resp.status}`); } const ticket = allTickets.find(t => t.key === ticketKey); @@ -209,7 +396,7 @@ async function updateTicket(ticketKey, dueDate, goLiveDate) { renderTable(); renderRoadmap(); - showToast(`Ticket ${ticketKey} mis à jour !`, 'success'); + showToast(`Ticket ${ticketKey} updated!`, 'success'); } catch (err) { showToast(err.message, 'error'); } finally { @@ -235,7 +422,7 @@ function handleUpdateClick(ticketKey, goLiveColName) { updateTicket(ticketKey, dueDate, goLiveDate); } -// --- RENDU --- +// --- RENDERING --- function renderResults() { document.getElementById('results-section').classList.add('visible'); renderStats(); renderTable(); renderJSON(); renderRoadmap(); @@ -244,18 +431,17 @@ function renderResults() { function renderStats() { const unassigned = allTickets.filter(t => !t.assignee).length; const now = new Date(); - const overdue = allTickets.filter(t => t.dueDate && t.statusCategory !== 'Done' && new Date(t.dueDate) < now).length; + 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=>t.status)).size}
Statuts
-
${unassigned}
Non assignés
-
${overdue}
En retard
+
${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('fr-FR',{day:'2-digit',month:'short',year:'numeric'}); } catch { return escHtml(d); } } -function getStatusClass(s) { if(!s) return 's-other'; const l=s.toLowerCase(); if(l.includes('done')||l.includes('ferm')||l.includes('résolu')) return 's-done'; if(l.includes('progress')||l.includes('en cours')||l.includes('review')) return 's-progress'; if(l.includes('to do')||l.includes('open')||l.includes('backlog')) return 's-todo'; if(l.includes('block')) return 's-blocked'; return 's-other'; } +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; @@ -265,28 +451,26 @@ function renderTable() { ); } - // Mapping pour faire le lien entre le texte de l'en-tête et la propriété de l'objet ticket const colMapping = { 'Ticket': 'key', 'Summary': 'summary', - 'Statut': 'status', - 'Assigné': 'assignee', - 'Priorité': 'priority', + 'Status': 'status', + 'Assignee': 'assignee', + 'Priority': 'priority', 'Type': 'issueType', 'Due Date': 'dueDate' }; - const cols = ['Ticket','Summary','Statut','Assigné','Priorité','Type','Due Date']; + const cols = ['Ticket','Summary','Status','Assignee','Priority','Type','Due Date']; if (goLiveColName) { cols.push(goLiveColName); - colMapping[goLiveColName] = goLiveColName; // Ajout dynamique du champ personnalisé + colMapping[goLiveColName] = goLiveColName; } cols.push('Actions'); - // Génération des en-têtes avec gestion du clic pour le tri document.getElementById('tableHead').innerHTML = '' + cols.map(c => { const key = colMapping[c]; - if (!key) return `${c}`; // Pour la colonne 'Actions' + if (!key) return `${c}`; const icon = currentSortCol === key ? (isAsc ? ' ▴' : ' ▾') : ''; return ` @@ -297,10 +481,15 @@ function renderTable() { 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(t.status)} + + + ${escHtml(mappedStatus)} + + ${escHtml(t.assignee) || '-'} ${escHtml(t.priority)} ${escHtml(t.issueType)} @@ -317,16 +506,7 @@ function renderTable() { `; }); - document.getElementById('tableBody').innerHTML = body || 'Aucun résultat'; -} - -function getStatusColor(status) { - const s = (status || '').toLowerCase(); - if (s.includes('done') || s.includes('ferm') || s.includes('rsolu')) return '#00e68a'; - if (s.includes('progress') || s.includes('en cours') || s.includes('review')) return '#4dc8ff'; - if (s.includes('to do') || s.includes('open') || s.includes('backlog')) return '#ffb84d'; - if (s.includes('block')) return '#ff4d6a'; - return '#7a9488'; + document.getElementById('tableBody').innerHTML = body || 'No results found'; } // --- ROADMAP (GANTT) --- @@ -357,64 +537,62 @@ function updateGanttExpandState() { if (icon && label) { if (isGanttExpanded) { icon.className = 'fa-solid fa-down-left-and-up-right-to-center'; - label.textContent = 'Replier'; + label.textContent = 'Collapse'; if (btn) btn.classList.add('active'); } else { icon.className = 'fa-solid fa-up-right-and-down-left-from-center'; - label.textContent = 'Déplier'; + label.textContent = 'Expand'; if (btn) btn.classList.remove('active'); } } } -// --- FONCTIONS UTILITAIRES POUR LE GANTT --- +// --- GANTT UTILITIES --- /** - * Extrait le titre court et le type selon le pattern : "Titre - Type - Montant - ID" + * Extracts short title and project type according to the pattern: "Title - Type - Amount - ID" */ function parseTicketSummary(summary) { - if (!summary) return { shortTitle: 'Sans titre', type: 'OTHER' }; + 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'; - // On valide si c'est un type connu, sinon OTHER const type = ['PR', 'CR', 'TR'].includes(typePart) ? typePart : 'OTHER'; return { shortTitle, type }; } /** - * Définit la couleur de la barre selon le type de projet + * Defines activity type accent color */ function getTypeColor(type) { + const isLight = document.documentElement.getAttribute('data-theme') === 'light'; switch(type) { - case 'PR': return '#4dc8ff'; // Bleu info - case 'CR': return '#ffb84d'; // Orange warning - case 'TR': return '#a371f7'; // Violet - default: return '#7a9488'; // Gris par défaut + 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 } } /** - * FONCTION PRINCIPALE : RENDER ROADMAP + * MAIN ROADMAP RENDERING FUNCTION */ function renderRoadmap() { const preview = document.getElementById('roadmapPreview'); if (!filteredTickets || filteredTickets.length === 0) { - preview.innerHTML = `
Aucun ticket à afficher.
`; + preview.innerHTML = `
No tickets to display.
`; return; } - // Détection dynamique de la colonne Go Live const goLiveColName = Object.keys(filteredTickets[0]).find(k => k.toLowerCase().includes('go live')) || "Go Live Date"; - // Configuration des largeurs - const leftWidth = 280; // Un peu plus large pour les titres split 0 + const leftWidth = 280; let colWidth = 140; if (currentGanttView === 'quarter') colWidth = 200; if (currentGanttView === 'year') colWidth = 250; - // 1. Filtrer les tickets : on ne garde que ceux qui ont au moins une date + // 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; @@ -422,11 +600,10 @@ function renderRoadmap() { }); if (ticketsWithDates.length === 0) { - preview.innerHTML = `
Aucune date de Due Date ou Go Live renseignée pour les tickets filtrés.
`; + preview.innerHTML = `
No Due Date or Go Live date found for filtered tickets.
`; return; } - // Calcul des bornes du calendrier const allDates = ticketsWithDates.flatMap(t => [ t.dueDate ? new Date(t.dueDate) : null, t[goLiveColName] ? new Date(t[goLiveColName]) : null @@ -439,16 +616,15 @@ function renderRoadmap() { if (currentGanttView === 'quarter') minDate.setMonth(Math.floor(minDate.getMonth() / 3) * 3); if (currentGanttView === 'year') minDate.setMonth(0); - maxDate.setMonth(maxDate.getMonth() + 2); // Un peu de marge au bout + maxDate.setMonth(maxDate.getMonth() + 2); maxDate.setDate(0); - // Construction de l'échelle de temps const timeScale = []; let tempDate = new Date(minDate); while (tempDate <= maxDate) { let label = "", key = ""; if (currentGanttView === 'month') { - label = tempDate.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' }); + label = tempDate.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); key = `${tempDate.getFullYear()}-${tempDate.getMonth()}`; tempDate.setMonth(tempDate.getMonth() + 1); } else if (currentGanttView === 'quarter') { @@ -482,32 +658,26 @@ function renderRoadmap() { return leftWidth + (index * colWidth) + (ratio * colWidth); }; - // 2. Groupement par assigné (uniquement ceux qui ont des tickets visibles) const grouped = ticketsWithDates.reduce((acc, t) => { - const name = t.assignee || "Non assigné"; + const name = t.assignee || "Unassigned"; if (!acc[name]) acc[name] = []; acc[name].push(t); return acc; }, {}); - // Début HTML let html = `
`; - // Ligne "Aujourd'hui" const todayX = getXPos(new Date()); if (todayX > leftWidth) { html += `
`; - html += `
Aujourd'hui
`; + html += `
Today
`; } - // Header - html += `
Assigné / Ticket (Titre)
`; + html += `
Assignee / Ticket (Title)
`; timeScale.forEach(ts => { html += `
${ts.label}
`; }); html += `
`; - // Corps du Gantt Object.keys(grouped).sort().forEach(assignee => { - // Header de groupe html += `
${assignee} @@ -525,28 +695,31 @@ function renderRoadmap() { const x1 = getXPos(start); const x2 = getXPos(end); - // Largeur de barre (minimum 150px pour afficher le badge de statut et le titre proprement) - const barWidth = Math.max(150, (x2 - x1) + 10); + 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); - const statusColor = getStatusColor(t.status); html += `
-
+
${escHtml(shortTitle)}
+ title="${escHtml(t.key)}: ${escHtml(t.summary)} | Status: ${escHtml(t.status)} (${mappedStatus}) | Type: ${type}" + style="left:${x1}px; width:${barWidth}px; background:${statusCfg.bg}; border-color:${statusCfg.border}; border-left:5px solid ${statusCfg.border};"> -
- ${escHtml(t.status)} +
+ ${escHtml(mappedStatus)}
- ${type} - ${escHtml(shortTitle)} + + ${type} + + ${escHtml(shortTitle)}
${timeScale.map(() => `
`).join('')}
`; @@ -556,7 +729,6 @@ function renderRoadmap() { html += `
`; preview.innerHTML = html; - // Focus sur la date du jour setTimeout(() => { const m = document.getElementById('todayMarker'); if (m) m.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); @@ -581,9 +753,8 @@ function filterTable() { function exportDrawIOXML(mode) { try { - // 1. Sécurité : Vérification des données if (!filteredTickets || filteredTickets.length === 0) { - showToast("Aucun ticket à exporter", "error"); + showToast("No tickets to export", "error"); return; } @@ -591,18 +762,16 @@ function exportDrawIOXML(mode) { const ticketsWithDates = filteredTickets.filter(t => t.dueDate || t[goLiveCol]); if (ticketsWithDates.length === 0) { - showToast("Aucune date de fin trouvée", "error"); + showToast("No end dates found", "error"); return; } - // 2. Paramètres de vue (Zoom) const leftWidth = 250; const rowHeight = 50; - let pixelPerDay = 12; // Mois + let pixelPerDay = 12; if (currentGanttView === 'quarter') pixelPerDay = 4; if (currentGanttView === 'year') pixelPerDay = 1.2; - // 3. Calcul des bornes temporelles const allDates = ticketsWithDates.map(t => new Date(t.dueDate || t[goLiveCol])); let minDate = new Date(Math.min(...allDates)); minDate.setDate(1); @@ -617,14 +786,13 @@ function exportDrawIOXML(mode) { let xmlNodes = ''; - // 4. TIMELINE & LIGNES VERTICALES let curr = new Date(minDate); while (curr < maxDate) { const xPos = getX(curr); let label = ""; if (currentGanttView === 'month') { - label = curr.toLocaleDateString('fr-FR', { month: 'short', year: 'numeric' }).toUpperCase(); + 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(); @@ -634,34 +802,29 @@ function exportDrawIOXML(mode) { curr.setFullYear(curr.getFullYear() + 1); } - // Titres Timeline agrandis (fontSize=14) xmlNodes += ` `; - // Lignes verticales de repère xmlNodes += ` `; } - // 5. LIGNE TODAY const xToday = getX(new Date()); - xmlNodes += ` + xmlNodes += ` `; - // 6. GÉNÉRATION DES TICKETS let currentY = 80; const groups = ticketsWithDates.reduce((acc, t) => { - const a = t.assignee || 'Non assigné'; + const a = t.assignee || 'Unassigned'; if (!acc[a]) acc[a] = []; acc[a].push(t); return acc; }, {}); Object.keys(groups).sort().forEach(assignee => { - // Header Assigné xmlNodes += ` `; @@ -669,9 +832,10 @@ function exportDrawIOXML(mode) { groups[assignee].forEach(t => { const { shortTitle, type } = parseTicketSummary(t.summary); - const color = getTypeColor(type); + const mappedStatus = mapJiraStatus(t.status); + const statusDef = STATUS_DEFINITIONS[mappedStatus] || STATUS_DEFINITIONS['Opportunity']; + const barColor = statusDef.drawioColor; - // Durée réelle (Création -> DueDate) const startDate = t.created ? new Date(t.created) : minDate; const endDate = new Date(t.dueDate || t[goLiveCol]); @@ -680,14 +844,12 @@ function exportDrawIOXML(mode) { let barWidth = xEnd - xStart; if (barWidth < 50) barWidth = 140; - // Label de gauche xmlNodes += ` `; - // Barre de Gantt - const label = `[${type}] ${escHtml(shortTitle)}`; - xmlNodes += ` + const label = `[${mappedStatus}] [${type}] ${escHtml(shortTitle)}`; + xmlNodes += ` `; @@ -696,7 +858,6 @@ function exportDrawIOXML(mode) { currentY += 20; }); - // 7. ASSEMBLAGE FINAL const finalXml = ` @@ -717,12 +878,12 @@ function exportDrawIOXML(mode) { a.download = "jira_export.drawio"; a.click(); } else { - navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copié !")); + navigator.clipboard.writeText(finalXml).then(() => showToast("XML Copied!")); } } catch (error) { console.error(error); - showToast("Erreur lors de l'export XML", "error"); + showToast("Error during XML export", "error"); } } @@ -741,14 +902,13 @@ function downloadJSON() { a.download = `export-${Date.now()}.json`; a.click(); } -function copyJSON() { navigator.clipboard.writeText(JSON.stringify(allTickets, null, 2)).then(() => showToast('Copié !')); } +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()} }); -// Cet événement se déclenche quand le navigateur a fini de charger le HTML document.addEventListener('DOMContentLoaded', () => { initTheme(); loadSettings(); - console.log("Paramètres chargés depuis le stockage local."); + console.log("Settings loaded from local storage."); }); \ No newline at end of file